Files
atomic-design-poc/docs/project/archive/refactor-backlog-setup/refactor-backlog/implementation/rb-29.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

7.7 KiB

RB-29 — Thread at through LetterHtml.ResolveAuto's datum case

Status: implemented · 2026-08-27 · Source findings: 02-testability.md TE-007 · 99-backlog.md RB-29

What was wrong

LetterHtml.Render(BriefEntity brief, OrgTemplateDto template, string at, bool watermark) (backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs) already took the letter's instant and used it correctly for the letterhead date (sb.Append(Enc(FormatDatumNl(at)))). The body's datum placeholder resolved through the private ResolveAuto(string key, string label), which ignored at and called FormatDatumNl(DateTimeOffset.UtcNow.ToString("o")) instead — a pure Domain/ rule class reading the wall clock. ResolveAuto is reached only through the private chain RenderNode ← RenderParagraphs ← Render, so no caller outside this file could pin the value a test would see.

The ticket read as filed against the current code: Render's signature, the letterhead's correct use of at, and ResolveAuto's UtcNow read were all exactly as TE-007 described (line numbers had moved — CC around the file has grown since the finding was written — but the code shape had not). One thing TE-007 named as the visible symptom also checked out: LetterHtmlTests.cs already declares a new PlaceholderDefDto("datum", "Datum", true) in its golden-file fixture, but no RichTextNodeDto in that fixture's Sections actually references the datum key in the body — it is declared but never rendered there, so the existing golden-file test could not have caught this even if it asserted on dates (which it does not either).

What changed

File Change
backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs ResolveAuto(string key, string label) → ResolveAuto(string key, string label, string at); "datum" => FormatDatumNl(at). at threaded down through the two private call sites in the chain: RenderParagraphs and RenderNode both gained an at parameter, passed from Render's own at.
backend/tests/BigRegister.Tests/LetterHtmlTests.cs New fixture FixtureBriefWithDatumInBody() — a minimal brief whose body actually references the datum placeholder (the golden fixture never does). Two new [Fact]s (see below) plus two small extraction helpers.
docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md RB-29 status cell: open → implemented.

Render's own signature is unchanged — TE-007's "zero public API change, zero call-site change" held exactly. Program.cs:697, :708, and BriefStore.cs:120 (the three callers) needed no edit.

What the fix looks like

private static void RenderParagraphs(
    StringBuilder sb, IReadOnlyList<ParagraphDto> paragraphs, IReadOnlyDictionary<string, PlaceholderDefDto> defs,
    string at)
{
  // ... unchanged body, forwards `at` to RenderNode ...
}

private static void RenderNode(
    StringBuilder sb, RichTextNodeDto node, IReadOnlyDictionary<string, PlaceholderDefDto> defs, string at)
{
  // ... unchanged body, forwards `at` to ResolveAuto ...
}

private static string ResolveAuto(string key, string label, string at) => key switch
{
  "naam_zorgverlener" => SeedData.Registration.Naam,
  "big_nummer" => SeedData.Registration.BigNummer,
  "datum" => FormatDatumNl(at),
  _ => label,
};

Both call sites already had at in scope (Render's own parameter), so this is a pure threading change — no new state, no new dependency.

Tests added

TE-007 named the exact gap: the golden-file fixture declares the datum placeholder but never renders it in the body, so no existing assertion could catch a body/letterhead mismatch. A new fixture and two focused tests close it:

  1. Render_resolves_the_body_datum_placeholder_from_the_given_at_not_the_wall_clock — renders FixtureBriefWithDatumInBody() with a fixed historical at (2019-03-14T08:00:00.0000000+00:00) and asserts the body's rendered paragraph is the exact string "14 maart 2019". A test using today's date would have passed before and after the fix and proven nothing — this one pins a date nowhere near "now", so it fails whenever the resolver reads the wall clock instead of at.
  2. Render_keeps_the_letterhead_date_and_the_body_datum_in_agreement_for_a_historical_at — same fixture and historical at, asserts the letterhead <dd> date and the body's rendered datum paragraph are equal. This is TE-007's stated payoff: not a shipped bug today (every current caller passes Now() at render time, so the two dates always coincided even with the bug present), but a latent one — the moment Render is ever called with a historical at (re-rendering an archive, back-dating a letter), the letterhead and body would disagree within a single document. This test is the one that would have caught that.

Both tests use the repo's one date formatter (FormatDatumNl, already used by both call sites under test) only indirectly, through the literal expected string "14 maart 2019" — no second hand-rolled ToString format was introduced in the test file either.

Verification

  • Verified red without the fix. Reverted only the ResolveAuto expression (via Edit, not git checkout) back to "datum" => FormatDatumNl(DateTimeOffset.UtcNow.ToString("o")), leaving the new tests and the threaded signatures in place. Ran the two new tests:
    Render_resolves_the_body_datum_placeholder_from_the_given_at_not_the_wall_clock [FAIL]
    Assert.Equal() Failure: Strings differ
    Expected: "14 maart 2019"
    Actual:   "27 augustus 2026"
    
    Render_keeps_the_letterhead_date_and_the_body_datum_in_agreement_for_a_historical_at [FAIL]
    Assert.Equal() Failure: Strings differ
    Expected: "14 maart 2019"
    Actual:   "27 augustus 2026"
    
    Both failures show the body rendering the run's actual wall-clock date (today, 2026-08-27) instead of the pinned historical at — the precise defect TE-007 describes. Restored the fix with a second Edit and re-ran: all 4 tests in LetterHtmlTests green (2 pre-existing + 2 new).
  • grep -n "UtcNow\|DateTime.Now\|DateTime.Today\|DateTimeOffset.Now" backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs — no matches. No ambient clock read remains anywhere in the file.
  • npm run ci (foreground, no background/Monitor): see result reported alongside this ticket.

What this ticket did not touch

LetterHtml.cs's overall structure and CC (TE-007 records it at 21, the third-highest in the backend) are unchanged — reducing that is out of scope for this ticket, per its own text. Data/BriefStore.cs and any BriefRules.cs file were not touched — a concurrent ticket owns that file.