Step 2 (code quality): dedup + stop FE recomputing a server rule
- H1: tasksFromProfile takes the server's eligibleForHerregistratie decision
instead of recomputing isHerregistratieEligible — the FE renders the rule,
doesn't own it (ADR-0001). Policy reference impl kept for tests.
- M1: one shared runSubmit(fn, fallback) wrapper; the 4 submit-* commands keep
only their payload mapping. +spec.
- M2: whenTag() kernel helper removes 10 repeated `as Extract<U,{tag}>` casts
across the wizard/form components.
M4 (shared JA_NEE) folded into the upcoming i18n pass (clean dedup needs
$localize labels to sit in shared without breaking the English-shared-UI rule).
L1 already resolved by the restyle commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
23
src/app/shared/application/submit.spec.ts
Normal file
23
src/app/shared/application/submit.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { runSubmit } from './submit';
|
||||
|
||||
describe('runSubmit', () => {
|
||||
it('folds a resolved call into ok(value)', async () => {
|
||||
const r = await runSubmit(async () => 'BIG-123', 'fallback');
|
||||
expect(r).toEqual({ ok: true, value: 'BIG-123' });
|
||||
});
|
||||
|
||||
it('maps a ProblemDetails rejection to err(detail)', async () => {
|
||||
const r = await runSubmit(async () => {
|
||||
throw { detail: 'Aanvraag afgewezen.' };
|
||||
}, 'fallback');
|
||||
expect(r).toEqual({ ok: false, error: 'Aanvraag afgewezen.' });
|
||||
});
|
||||
|
||||
it('falls back when the rejection has no detail', async () => {
|
||||
const r = await runSubmit(async () => {
|
||||
throw new Error('network');
|
||||
}, 'fallback');
|
||||
expect(r).toEqual({ ok: false, error: 'fallback' });
|
||||
});
|
||||
});
|
||||
20
src/app/shared/application/submit.ts
Normal file
20
src/app/shared/application/submit.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { problemDetail } from '@shared/infrastructure/api-error';
|
||||
|
||||
/**
|
||||
* Run a mutating API call and fold it into a `Result` — the one place the
|
||||
* try/catch + ProblemDetails-mapping lives, so every `submit-*` command is just
|
||||
* its own payload mapping. The backend re-validates and returns a 422
|
||||
* ProblemDetails on rejection, surfaced here as the error string.
|
||||
*/
|
||||
export async function runSubmit<T>(fn: () => Promise<T>, fallback: string): Promise<Result<string, T>> {
|
||||
try {
|
||||
return ok(await fn());
|
||||
} catch (e) {
|
||||
return err(problemDetail(e, fallback));
|
||||
}
|
||||
}
|
||||
|
||||
// ponytail: i18n in Step 3/M3 wraps this in $localize with a stable @@id, which
|
||||
// dedupes it at the translation layer; for now it's the single shared default.
|
||||
export const SUBMIT_FAILED = 'Het indienen is niet gelukt. Probeer het later opnieuw.';
|
||||
@@ -21,3 +21,13 @@ export const err = <E>(error: E): Result<E, never> => ({ ok: false, error });
|
||||
/** Nominal typing: Brand<string, 'Postcode'> is assignable from a plain string
|
||||
only through an explicit cast — so a smart constructor is the only minter. */
|
||||
export type Brand<T, B extends string> = T & { readonly __brand: B };
|
||||
|
||||
/** Narrow a tagged union to one variant by its `tag`, or null. The single place
|
||||
the cast lives — TS can't narrow through a runtime tag argument, so callers get
|
||||
`whenTag(state, 'Editing')?.foo` instead of repeating `as Extract<…>`. */
|
||||
export function whenTag<U extends { tag: string }, K extends U['tag']>(
|
||||
u: U,
|
||||
tag: K,
|
||||
): Extract<U, { tag: K }> | null {
|
||||
return u.tag === tag ? (u as Extract<U, { tag: K }>) : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user