feat(brief): tolerate a 404 on GET /brief with a one-shot reset (RB-22)
BriefStore.load() now treats a 404 from GET /brief as "no brief exists yet" and calls the existing reset() command once, instead of showing the generic load-failed error. BriefAdapter.load() gains a BriefLoadFailure error channel (notFound | error) so the store can tell a 404 apart from every other failure; every other adapter method stays on runSubmit, unchanged. The once-only bound is a field on the store, not a comment: a second 404 (from a later load() call) always falls through to the ordinary error path, and the recovery path never calls load() again, so no loop can form. This is the expand half of CQ-007's split (04-cqrs-light.md). Today's backend never 404s GET /brief, so the new branch is dead code until RB-23 (the backend contract half) ships in a later merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { runResult, runSubmit } from '@shared/application/submit';
|
||||
import { runSubmit } from '@shared/application/submit';
|
||||
import { problemDetail } from '@shared/infrastructure/api-error';
|
||||
import {
|
||||
ApiClient,
|
||||
BriefDecisionsDto,
|
||||
@@ -33,9 +34,13 @@ import { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/ric
|
||||
* The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire
|
||||
* uses FLAT unions (a `type`/`tag` string + nullable fields, the repo convention);
|
||||
* the `parse*` boundary narrows them into the domain's proper discriminated unions
|
||||
* and rejects malformed shapes. `load` (the only read) folds through `runResult`;
|
||||
* every mutation folds through `runSubmit` (ProblemDetails → error string, plus the
|
||||
* Idempotency-Key mint), then parses the returned brief.
|
||||
* and rejects malformed shapes. Every mutation folds through `runSubmit`
|
||||
* (ProblemDetails → error string, plus the Idempotency-Key mint), then parses the
|
||||
* returned brief. `load` (the only read) does its own try/catch instead of the
|
||||
* shared `runResult` fold, because it needs one extra bit `runResult` throws away:
|
||||
* whether the failure was an HTTP 404 (see `BriefLoadFailure` — RB-22, CQ-007's
|
||||
* expand half). Today's backend never 404s `GET /brief` (RB-23 adds that), so the
|
||||
* `notFound` branch is unreached until RB-23 ships; this adapter is ready in advance.
|
||||
*/
|
||||
|
||||
export interface BriefView {
|
||||
@@ -46,16 +51,39 @@ export interface BriefView {
|
||||
readonly caseContext: CaseContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Why `load()` did not return a brief. `notFound` is a bare HTTP 404 — kept
|
||||
* distinct from every other failure so `BriefStore.load()` can tolerate it (call
|
||||
* `reset()` instead of showing an error banner) without conflating it with a real
|
||||
* failure. See the class docstring above.
|
||||
*/
|
||||
export type BriefLoadFailure =
|
||||
{ readonly tag: 'notFound' } | { readonly tag: 'error'; readonly reason: string };
|
||||
|
||||
export const BRIEF_LOAD_FAILED = $localize`:@@brief.load.failed:De brief kon niet worden geladen.`;
|
||||
export const BRIEF_ACTION_FAILED = $localize`:@@brief.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;
|
||||
|
||||
/** True when the thrown value carries an HTTP 404 status — matches both the
|
||||
generic `SwaggerException` (today's shape, since `GET /brief` declares no 404
|
||||
response yet) and a parsed `ProblemDetails` (RFC 7807 `status`, the shape once
|
||||
RB-23 gives the endpoint a documented 404 response). */
|
||||
function isHttpNotFound(e: unknown): boolean {
|
||||
return !!e && typeof e === 'object' && (e as { status?: unknown }).status === 404;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BriefAdapter {
|
||||
private client = inject(ApiClient);
|
||||
|
||||
async load(): Promise<Result<string, BriefView>> {
|
||||
const r = await runResult(() => this.client.briefGET(), BRIEF_LOAD_FAILED);
|
||||
return r.ok ? parseBriefView(r.value) : r;
|
||||
async load(): Promise<Result<BriefLoadFailure, BriefView>> {
|
||||
try {
|
||||
const dto = await this.client.briefGET();
|
||||
const parsed = parseBriefView(dto);
|
||||
return parsed.ok ? ok(parsed.value) : err({ tag: 'error', reason: parsed.error });
|
||||
} catch (e) {
|
||||
if (isHttpNotFound(e)) return err({ tag: 'notFound' });
|
||||
return err({ tag: 'error', reason: problemDetail(e, BRIEF_LOAD_FAILED) });
|
||||
}
|
||||
}
|
||||
|
||||
async save(sections: readonly LetterSection[]): Promise<Result<string, BriefView>> {
|
||||
|
||||
Reference in New Issue
Block a user