Rename change-request.machine.ts's bare State/Msg to ChangeRequestState/ ChangeRequestMsg (the last machine not context-prefixed), document the createStore-is-the-idiom + naming convention in CLAUDE.md §3, and add the Foundations/State Machines (TEA) curriculum page. The wizard pages already wired createStore (confirmed by reading each and by git log) -- the WP's "hand-wired signal(model)" premise was stale; recorded as a deviation.
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { Result, assertNever } from '@shared/kernel/fp';
|
|
import { Postcode, parsePostcode } from '@registratie/domain/value-objects/postcode';
|
|
|
|
/** What the user is typing (raw, possibly invalid). */
|
|
export interface Draft {
|
|
straat: string;
|
|
postcode: string;
|
|
woonplaats: string;
|
|
}
|
|
|
|
/** After parsing — postcode is the branded type, so downstream can't get a raw one. */
|
|
export interface Valid {
|
|
straat: string;
|
|
postcode: Postcode;
|
|
woonplaats: string;
|
|
}
|
|
|
|
export type Errors = Partial<Record<keyof Draft, string>>;
|
|
|
|
/**
|
|
* The change-request (adreswijziging) form as one tagged union — the SAME idiom
|
|
* as the wizards, just single-step. `draft`/`errors` exist only while Editing;
|
|
* Submitting/Submitted/Failed carry the parsed `Valid`. Illegal states (submitting
|
|
* an invalid draft, a success screen with errors) are unrepresentable.
|
|
*/
|
|
export type ChangeRequestState =
|
|
| { tag: 'Editing'; draft: Draft; errors: Errors }
|
|
| { tag: 'Submitting'; data: Valid }
|
|
| { tag: 'Submitted'; data: Valid; referentie: string }
|
|
| { tag: 'Failed'; data: Valid; error: string };
|
|
|
|
export const initial: ChangeRequestState = {
|
|
tag: 'Editing',
|
|
draft: { straat: '', postcode: '', woonplaats: '' },
|
|
errors: {},
|
|
};
|
|
|
|
/** Parse via the value objects; on success hand back a Valid, else per-field errors. */
|
|
function validate(draft: Draft): Result<Errors, Valid> {
|
|
const straat = draft.straat.trim();
|
|
const postcode = parsePostcode(draft.postcode);
|
|
const errors: Errors = {};
|
|
if (!straat) errors.straat = $localize`:@@validation.straat:Vul straat en huisnummer in.`;
|
|
if (!postcode.ok) errors.postcode = postcode.error;
|
|
if (straat && postcode.ok) {
|
|
return {
|
|
ok: true,
|
|
value: { straat, postcode: postcode.value, woonplaats: draft.woonplaats.trim() },
|
|
};
|
|
}
|
|
return { ok: false, error: errors };
|
|
}
|
|
|
|
export type ChangeRequestMsg =
|
|
| { tag: 'SetField'; key: keyof Draft; value: string }
|
|
| { tag: 'Submit' }
|
|
| { tag: 'Retry' }
|
|
| { tag: 'SubmitConfirmed'; referentie: string }
|
|
| { tag: 'SubmitFailed'; error: string }
|
|
| { tag: 'Reset' }
|
|
| { tag: 'Seed'; state: ChangeRequestState }; // mount a specific state (stories/tests)
|
|
|
|
export function reduce(s: ChangeRequestState, m: ChangeRequestMsg): ChangeRequestState {
|
|
switch (m.tag) {
|
|
case 'SetField':
|
|
return s.tag === 'Editing' ? { ...s, draft: { ...s.draft, [m.key]: m.value } } : s;
|
|
case 'Submit': {
|
|
if (s.tag !== 'Editing') return s;
|
|
const r = validate(s.draft);
|
|
return r.ok ? { tag: 'Submitting', data: r.value } : { ...s, errors: r.error };
|
|
}
|
|
case 'Retry':
|
|
return s.tag === 'Failed' ? { tag: 'Submitting', data: s.data } : s;
|
|
case 'SubmitConfirmed':
|
|
return s.tag === 'Submitting'
|
|
? { tag: 'Submitted', data: s.data, referentie: m.referentie }
|
|
: s;
|
|
case 'SubmitFailed':
|
|
return s.tag === 'Submitting' ? { tag: 'Failed', data: s.data, error: m.error } : s;
|
|
case 'Reset':
|
|
return initial;
|
|
case 'Seed':
|
|
return m.state;
|
|
default:
|
|
return assertNever(m);
|
|
}
|
|
}
|