fix(audit-wave-9): PDF correctness + brand asset hardening (pdf-auditor)

Address the pdf-auditor findings that survived the 2026-05-12 PDF stack
overhaul (pdfme → react-pdf). Items C-2/C-3 (tiptap-to-pdfme bugs) were
resolved when that 571-LOC bridge was deleted; remaining items:

- **M-7 wrong-port brand fallback** — replace `'Port Nimara'` defaults
  in PDF-rendering services. `reports.service` and `expense-export`
  throw when the port row is missing (the job is FK-keyed on a real
  port, so absence = broken state, must not stamp a competitor brand).
  `record-export` uses `'(port)'` as the visible placeholder.

- **M-2 silent field drift in fill-eoi-form** — promote the
  always-silent catch in `setText` / `setCheckbox` to log a structured
  warning per missing field (mirroring the existing `setBerthRange`
  pattern). A re-cut template with drifted AcroForm field names now
  surfaces in ops logs instead of shipping with empty values.

- **M-3 form not flattened** — `fillEoiFormFields` now flattens the
  AcroForm before save. Documenso pathway flattens server-side; this
  brings the in-app pathway to parity, so the signer can't edit
  pre-filled yacht dimensions / address / berth number after the fact.

- **M-1 PDF metadata** — set Title / Author / Subject / Lang / Producer
  / Creator on the generated EOI PDF for downstream readers and a11y
  tooling.

- **M-4 noisy berth-range warnings** — downgrade per-mooring warn to
  debug; emit a single summary warn per call when any passthrough
  occurred. Multi-berth EOIs with archived/legacy moorings no longer
  spam the log on every render.

- **M-6 source PDF sha pinning** — pin
  `assets/eoi-template.pdf` sha256 via `EXPECTED_EOI_SHA256` (exported
  for tests); `loadEoiTemplatePdf` warns once per process when the
  bytes drift without an explicit hash bump. Documented the
  intentional-update workflow in `assets/README.md`.

Tests updated in `tests/unit/pdf/fill-eoi-form.test.ts` to reflect
flatten + metadata (form fields are gone after flatten; pdf-lib has no
getLanguage so we assert the other setters round-trip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:07:57 +02:00
parent c1fcc9d5c4
commit eab30c194a
7 changed files with 142 additions and 42 deletions

View File

@@ -61,11 +61,22 @@ export function formatBerthRange(mooringNumbers: readonly string[]): string {
const p = tryParse(m);
if (p) parsed.push(p);
else {
logger.warn({ mooring: m }, 'formatBerthRange: non-canonical mooring; passing through');
// Per-mooring is debug to keep logs quiet when archived rows
// (with " (archived)" / " (deleted)" suffixes) flow through a
// bundle; the single summary `warn` below makes the situation
// observable without spamming.
logger.debug({ mooring: m }, 'formatBerthRange: non-canonical mooring; passing through');
passthrough.push(m);
}
}
if (passthrough.length > 0) {
logger.warn(
{ count: passthrough.length, samples: passthrough.slice(0, 3) },
'formatBerthRange: non-canonical moorings passed through (verbatim, not range-compressed)',
);
}
// Sort canonical-form moorings by (prefix, number).
parsed.sort((a, b) => {
if (a.prefix !== b.prefix) return a.prefix < b.prefix ? -1 : 1;