§ unswayed-backend
UAT round 3 — localhost checkout fix & the export PDF's Candidate section
UAT round 3 — two small fixes, both instructive
Round 3 (branch fix/uat-round-3, off staging) is tiny next to round 2 — two
already-root-caused fixes reapplied from an unpushed checkout on another
machine, plus one strictness catch the reapplication surfaced. Memory:
docs/ai/memory/0045-uat-round-3.md.
1 · Why every local checkout 422'd
POST /api/subscriptions/checkout rejected success_url/cancel_url with
"must be a URL address" — but only from local dev. The frontend builds both
fields from window.location.origin, which is http://localhost:3000 locally,
and class-validator's bare @IsUrl() defaults to require_tld: true: any host
without a top-level domain fails, and localhost has none. Both fields are
built the same way, which is why both failed identically — it was never about
Stripe's {CHECKOUT_SESSION_ID} placeholder.
The documented fix was necessary but not sufficient. require_tld: false
alone makes any single word a valid bare hostname — 'not-a-url' sails
through validation and reaches Stripe, surfacing as a 500. The subscriptions
e2e caught it (a pinned test expects a 422 with success_url keyed alone).
The final decorators are @IsUrl({ require_tld: false, require_protocol: true }): localhost origins still pass (an origin always carries its scheme),
protocol-less junk 422s again, and Stripe requires absolute redirect URLs
anyway. Comments in the DTO explain both options so nobody "cleans them up".
Deliberately not fixed: the same bare-@IsUrl() pattern exists on
customer-portal.dto's return_url, the google-calendar DTOs, and
meeting_link. None has a reported symptom — they get the same treatment only
if one appears.
2 · The export PDF that forgot whose data it was
The candidate-side "download application history" PDF (settings → privacy)
listed applications — job, company, applied date, status — but nothing about
the candidate: no name, location, phone, or join date. The service simply never
queried those fields: userId → Applicant.id → JobApplication rows, done.
Now it reads User (phone, createdAt) alongside an extended Applicant
select and resolves country/state/city ids to names through
MasterDataLookupService — the same lookup the profile read uses — rendering a
Candidate section (Name / Location / Phone number / Date joined, N/A
fallbacks) ahead of Applications. ApplicationsModule imports
MasterDataModule for the new dependency.
The privacy nuance worth remembering: this export is the candidate's own copy of their own data, so it prints real PII — the UCN-anonymised rendering belongs exclusively to the employer-facing resume export. The class doc-comment now says so explicitly.
The cross-checkout lesson
The original fixes lived only on a Windows checkout that was never pushed
(commits 9cf02de/04f7df1 there; b45668f/21d7810/1388b09 here).
Reapplying from the written record worked because the record captured root
causes — but re-running the WHOLE suite here, not just the spec file the
record named, is what caught the strictness hole. The Windows branch likely
carries that latent e2e failure.
Related trap, documented in memory 0045: with another branch's Prisma client
generated in node_modules, tsc shows phantom fixture errors on this
branch — regenerate the client from the checked-out schema before judging
typecheck results, and return the box to the superset (round-2) client before
restarting the service.