fix(backend): resolve the body datum placeholder from at, not UtcNow (RB-29)
LetterHtml.Render already receives the letter's instant and uses it for the letterhead date. The body's "datum" placeholder resolved through ResolveAuto, which ignored that instant and read the wall clock instead. This is not a shipped bug today, because every current caller passes Now() at render time. It becomes one the moment Render runs with a historical instant (an archive re-render, a back-dated letter): the letterhead and the body would then disagree within one document. Thread the existing "at" parameter down through RenderParagraphs and RenderNode into ResolveAuto's "datum" case. Render's own signature, and every call site, stays unchanged. Add two tests with a fixed historical "at": one pins the body's rendered date to the expected Dutch string, the other asserts the letterhead date and the body date agree. Both fail red against the old code, showing today's date instead of the pinned one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ public static class LetterHtml
|
||||
{
|
||||
sb.Append("<section><h3>").Append(Enc(section.Title)).Append("</h3>");
|
||||
foreach (var block in section.Blocks)
|
||||
RenderParagraphs(sb, block.Content.Paragraphs, defs);
|
||||
RenderParagraphs(sb, block.Content.Paragraphs, defs, at);
|
||||
sb.Append("</section>");
|
||||
}
|
||||
sb.Append("</div>");
|
||||
@@ -89,7 +89,8 @@ public static class LetterHtml
|
||||
private const string RecipientPlaceholder = "Adres van de geadresseerde\n(wordt ingevuld bij verzending)";
|
||||
|
||||
private static void RenderParagraphs(
|
||||
StringBuilder sb, IReadOnlyList<ParagraphDto> paragraphs, IReadOnlyDictionary<string, PlaceholderDefDto> defs)
|
||||
StringBuilder sb, IReadOnlyList<ParagraphDto> paragraphs, IReadOnlyDictionary<string, PlaceholderDefDto> defs,
|
||||
string at)
|
||||
{
|
||||
string? openList = null;
|
||||
foreach (var para in paragraphs)
|
||||
@@ -101,14 +102,14 @@ public static class LetterHtml
|
||||
openList = para.List;
|
||||
}
|
||||
sb.Append(openList is null ? "<p>" : "<li>");
|
||||
foreach (var node in para.Nodes) RenderNode(sb, node, defs);
|
||||
foreach (var node in para.Nodes) RenderNode(sb, node, defs, at);
|
||||
sb.Append(openList is null ? "</p>" : "</li>");
|
||||
}
|
||||
if (openList is not null) sb.Append(openList == "bullet" ? "</ul>" : "</ol>");
|
||||
}
|
||||
|
||||
private static void RenderNode(
|
||||
StringBuilder sb, RichTextNodeDto node, IReadOnlyDictionary<string, PlaceholderDefDto> defs)
|
||||
StringBuilder sb, RichTextNodeDto node, IReadOnlyDictionary<string, PlaceholderDefDto> defs, string at)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
@@ -122,7 +123,7 @@ public static class LetterHtml
|
||||
var key = node.Key ?? "";
|
||||
var def = defs.GetValueOrDefault(key);
|
||||
var label = def?.Label ?? key;
|
||||
sb.Append(def is { AutoResolvable: true } ? Enc(ResolveAuto(key, label)) : Enc($"[NOG IN TE VULLEN: {label}]"));
|
||||
sb.Append(def is { AutoResolvable: true } ? Enc(ResolveAuto(key, label, at)) : Enc($"[NOG IN TE VULLEN: {label}]"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -131,11 +132,11 @@ public static class LetterHtml
|
||||
// single demo applicant (SeedData.Registration — no per-brief resolved value is
|
||||
// ever stored, see the class doc above). Falls back to the label itself for any
|
||||
// other auto-resolvable key, mirroring the FE canvas' own `sampleFor` fallback.
|
||||
private static string ResolveAuto(string key, string label) => key switch
|
||||
private static string ResolveAuto(string key, string label, string at) => key switch
|
||||
{
|
||||
"naam_zorgverlener" => SeedData.Registration.Naam,
|
||||
"big_nummer" => SeedData.Registration.BigNummer,
|
||||
"datum" => FormatDatumNl(DateTimeOffset.UtcNow.ToString("o")),
|
||||
"datum" => FormatDatumNl(at),
|
||||
_ => label,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user