06 · Visa
VISA-001 · One visa case per travelling passenger
Status: PROPOSED · Owner: Visa Every ACTIVE passenger who needs a visa has exactly one visa case, linked to that passenger (a real database link, not a copied name). Whole-booking visa cases are no longer created.
VISA-002 · Cases open automatically
Status: PROPOSED · Owner: Visa A visa case opens when a passenger who needs a visa is confirmed by operations, and in the visa group of the passenger's travel group.
VISA-003 · Status flow
Status: PROPOSED · Owner: Visa
NOT_STARTED → APPLIED → SENT_TO_EMBASSY → UNDER_PROCESS → ISSUED → COLLECTED, or → REJECTED from APPLIED / SENT_TO_EMBASSY / UNDER_PROCESS. Other jumps are refused. Every change is recorded with who and when. This governs every live case; a visa the system never worked is written down once under VISA-004 instead, and is marked as a recorded fact so the two can never be confused.
Enforced by: change_visa_status + VisaCase_guard trigger + VisaStatusHistory.changedBy/reason (20260918130000_tickets_visa_comms.sql), test supabase/tests/tickets_visa_comms.sql. The customer is e-mailed on ISSUED and REJECTED only, by the database (VISA-005); "documents needed" is e-mailed when operations approval opens the cases.
VISA-004 · Record an end state as at a date
Status: PROPOSED · Owner: Visa · Source: GROUP RECORD 2026-27 operations workbook (the VISA STATUS column says ISSUED and nothing else)
Some visas were never worked in this system: a departure entered after it flew, a case handled entirely by a sub-agent or an embassy portal. For those there is one entry point — record the end state (ISSUED, COLLECTED or REJECTED) as at a date, with the source of the fact. It writes a single transition, marked as a recorded historical fact rather than a workflow step, and it keeps both the date it was true as at and where it came from. It is offered only on a case at NOT_STARTED: once operations has moved a case, VISA-003 owns it and there is no short cut out of the state machine. Recording a fact after the event never emails a customer.
Enforced by: 20260924103000_visa_recorded_outcome.sql — record_visa_outcome() (SECURITY DEFINER, visa.edit, idempotent), VisaStatusHistory.isRecordedFact / recordedSource / effectiveAt with the VisaStatusHistory_recorded_fact_shape CHECK, VisaCase.statusSource (workflow | recorded) and visa_is_end_state(). visa_status_transition_allowed() and change_visa_status() are unchanged. Tests: supabase/tests/visa_recorded_outcome.sql, src/lib/api.visaRecordedOutcome.test.ts.
VISA-005 · Every door tells the customer
Status: DECIDED (2026-09-27) · Owner: Management · Source: owner, 27 Sep 2026: "give staff more options to work from phone" When a visa case moves to ISSUED or REJECTED the customer is e-mailed, whichever screen made the change — the desktop, the phone, or anything that calls the database later. The booking's partner, when there is one, is e-mailed too. The message says the visa was issued or refused, with the traveller, the application number and the booking number. It never carries the reason staff typed (REQ-002). A customer who opted out of e-mail gets nothing (AUD-022). One message per case, status and recipient: a double tap or a retry sends nothing more. Internal steps (APPLIED, SENT_TO_EMBASSY, UNDER_PROCESS, COLLECTED) and a recorded end state (VISA-004) e-mail nobody. The screen says "customer notified" only when the e-mail really was queued; otherwise it says why not, so staff can call.
Until 27 Sep 2026 the desktop browser sent this e-mail after the status change, and a change made on the phone told nobody.
Enforced by: 20261001140000_every_door_tells_the_visa_customer.sql — change_visa_status() calls visa_status_notify() (internal, no EXECUTE for signed-in users), which queues visa_status_change rows in CommunicationQueue for the communications-dispatcher; a unique index on the queue (case, status, recipient kind). A failure to queue never undoes the status change. Test: supabase/tests/every_door_tells_the_visa_customer.sql.
Not built: a WhatsApp message for the visa milestone. It needs a template approved by Meta; until then it is e-mail only.
VISA-010 · Travel readiness depends on the visa
Status: PROPOSED · Owner: Operations
A passenger whose visa is not ISSUED (or COLLECTED) is shown as not ready to travel. A REJECTED visa never counts as covered in group readiness.
Enforced by: per-passenger readiness in src/lib/groupIntelligence.ts (display rule; no money/data impact).
VISA-011 · A refused visa is refused
Status: DECIDED (2026-09-23) · Owner: Management An embassy's refusal stands. A rejected visa case is not applied for again: REJECTED is the end of that case. The way forward is to cancel the traveller under the normal cancellation policy, which keeps the visa fee because a visa was applied for.
This is the owner's rule, confirmed on 2026-09-23. It is what the system has always enforced — visa_status_transition_allowed has no way out of REJECTED — and it is now written down rather than left open.
Still to fix: the traveller's journey card says "Visa rejected — re-apply or cancel", which tells staff to do something that is not allowed. It should say cancel.
Enforced by: 20260918130000_tickets_visa_comms.sql — visa_status_transition_allowed admits no move out of REJECTED, and change_visa_status refuses CANCELLED outright. Test: supabase/tests/tickets_visa_comms.sql.
VISA-020 · Cancellation closes the case
Status: PROPOSED · Owner: Visa Cancelling a passenger closes their visa case (kept for history, never deleted). A cancelled passenger never gets a new visa case.
VISA-021 · Transfer moves the case
Status: DECIDED (LC-030) · Owner: Visa A transferred passenger's visa case moves to the new group's visa group.
VISA-030 · Visa documents are never destroyed by sync
Status: PROPOSED · Owner: Visa
Merging or cleaning up duplicate cases never deletes uploaded documents or status history.
Enforced by: delete_visa_document (soft delete, case-checked, reason), no DELETE policy on VisaDocument, history append-only for client sessions (20260918130000_tickets_visa_comms.sql).
VISA-031 · A visa document can be filed from the phone
Status: DECIDED (2026-09-27) · Owner: Visa · Source: as VISA-005
Visa staff (visa.edit) add the visa copy, the passport or another paper to a case from the phone — a photo or a PDF — and it lands on the company Shared Drive under the case like one added on the desktop (ACC-074). Only a file the server stored for that case can be recorded on it. Anyone with visa.view opens a case's documents in the app, through a ten-minute link (ACC-073).
Enforced by: drive-upload (kind visa_doc, visa.edit, the case must exist), then add_visa_document() in 20261001140000_every_door_tells_the_visa_customer.sql — visa.edit, the file must be in DriveFile as visa_doc for that case, idempotent per case and file. drive-file kind visa_file (visa.view) opens one. Test: supabase/tests/every_door_tells_the_visa_customer.sql.
VISA-032 · The phone's visa list finds a case
Status: DECIDED (2026-09-27) · Owner: Visa · Source: as VISA-005
The phone lists visa cases by stage (in progress, issued, rejected, all) and by departure, and finds one by the traveller's name, passport number (spaces and dashes ignored), application number, booking number, group code or customer code. Each case shows its next step. Row security decides what a person sees.
Enforced by: visa_phone_cases() in 20261001140000_every_door_tells_the_visa_customer.sql (SECURITY INVOKER, refuses without visa.view). Test: supabase/tests/every_door_tells_the_visa_customer.sql.
VISA-040 · Public visa intake keeps what the customer submitted
Status: PROPOSED · Owner: Visa
Converting a public intake request carries across passport number and expiry, date of birth, nationality, visa type, travel dates and attachments, and matches an existing customer by passport number before creating a new one.
Enforced by: convert_visa_intake (normalized passport match, reuses an open case for the same passport/customer), visa-intake edge function (captcha fail-closed, rate limit, issued upload keys only, masked passport in email); test supabase/tests/tickets_visa_comms.sql. Without a passport number no customer is created (Customer.passportNo is mandatory).
VISA-050 · Visa fees
Status: OPEN · Owner: Finance Decide whether visa fees are a separate invoice line, whether they are refundable after submission (PRC-023), and whether a group invoice charges visa for passengers whose case is rejected or cancelled.