CODEX // oaoisme wiki

§ unswayed-backend

UAT round 3 — localhost checkout fix & the export PDF's Candidate section

updated 2026-07-09

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: userIdApplicant.idJobApplication 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.