refactor(registratie): extract the read half of createDraftSync (RB-21)
createDraftSync mixed a read path (findConcept, load, the read half of resume) with its write path (ensureId, flush, submit, reset) in one 187-line function -- CQ-001's finding. Move findConcept and loadConcept into a new application/find-concept.ts as free functions that take the adapter, so they get a direct spec with no Angular TestBed. createDraftSync keeps the closure state (id, ensuring, resumeGate) and the whole write path unchanged -- this is a move, not a redesign. The resumeGate coupling that lets the write path wait for the read path stays exactly where it was. createDraftSync shrinks from 187 to 169 lines. draft-sync.spec.ts is unchanged -- it never called resume()/load() directly, and its 409 recovery test for submit() still exercises the extracted findConcept through ensureId's catch branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { AanvraagType } from '@registratie/domain/aanvraag';
|
||||
import {
|
||||
ApplicationsAdapter,
|
||||
parseApplications,
|
||||
} from '@registratie/infrastructure/applications.adapter';
|
||||
|
||||
/**
|
||||
* Read half of the Concept lookup that `createDraftSync` (`draft-sync.ts`) needs
|
||||
* before it can start writing (RB-21 / CQ-001). Free functions that take the adapter
|
||||
* as a parameter, not `inject()`, so they get a direct spec without Angular TestBed.
|
||||
* `createDraftSync` keeps the closure state (`id`, `resumeGate`) and the write path;
|
||||
* these two functions only read.
|
||||
*/
|
||||
|
||||
/** Find the user's existing Concept of a given type (at most one), if any. */
|
||||
export async function findConcept(
|
||||
adapter: ApplicationsAdapter,
|
||||
type: AanvraagType,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const parsed = parseApplications(await adapter.list());
|
||||
return parsed.ok
|
||||
? parsed.value.find((a) => a.type === type && a.status.tag === 'Concept')?.id
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Outcome of loading one Concept by id: its draft (or null when it has none), or
|
||||
`not-concept` when the id is not an editable Concept (submitted/gone) or the
|
||||
lookup failed (unknown/deleted id) — the caller treats both the same way, as
|
||||
"start fresh". */
|
||||
export type LoadedConcept = { tag: 'concept'; draft: unknown | null } | { tag: 'not-concept' };
|
||||
|
||||
/** Load a specific Concept by id and report whether it is still editable. */
|
||||
export async function loadConcept(
|
||||
adapter: ApplicationsAdapter,
|
||||
id: string,
|
||||
): Promise<LoadedConcept> {
|
||||
try {
|
||||
const dto = await adapter.detail(id);
|
||||
if (dto.status && dto.status.tag !== 'Concept') return { tag: 'not-concept' };
|
||||
return { tag: 'concept', draft: dto.draft ?? null };
|
||||
} catch {
|
||||
return { tag: 'not-concept' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user