Handover — continuing this work on another machine
Written 19 September 2026, updated 20 September 2026. Read this first, then
docs/rules/PROGRAM.md for the plan and docs/audits/2026-09-18-real-group-accounting-test.md
for where the accounting stands.
Where things are
Production is live. The enterprise operations upgrade was released on 18 September:
migrations applied, 24 edge functions deployed, frontend merged to main (PR #375), Cloudflare
Pages publishes from main. The release closed a live data leak — the audit trail was readable by
every signed-in account, including partner and customer portal logins, because of a policy applied
to the project by hand that no migration created.
Active branch: integration/ops-upgrade. Everything since the release lands there first and is
verified locally before it goes near main. At 20 September: 337 migrations, 1,664 unit tests,
38 SQL suites, all green.
Production carries one real departure. The 12 Aug 2026 Umrah group (16 pilgrims, 13 on the
block and 3 on individual tickets) was entered through the app as the people who would have entered
it. Its cost came to ₹16,51,274.01 against the owner's Busy ledger of ₹16,51,274. Production is
ahead of main by the migrations on this branch: supabase db push has been run against it, while
Cloudflare Pages still serves the frontend from main.
The work in progress: feat/money-shape (merged 20 September) gave six business facts a home —
free seats as an input rather than a write-off, the split between a cancellation fee and recovered
cost, the ₹700 service charge on its own income head, one receipt across several bookings, a
supplier bill that names its departure, and a receipt that must state its value date, mode and
instrument. See "The recurring fault" below — it still matters more than any individual defect.
The recurring fault (read this before fixing anything in finance)
src/lib/api.ts runs in the browser. Roughly 47 places in it build an accounting voucher. A
posting written there runs under the acting user's own row-level security, so it is refused for the
very people whose job it is — an operations executive assigning a seat does not hold finance.create
— and the refusal is then either queued or swallowed.
Three rounds of fixes each moved a few sites and left the rest, and each round's test exposed the next one. Round 3 (19 Sep) still lost ₹13,42,260: the flown cost of a departure's air never posted.
The rule: a posting belongs in a SECURITY DEFINER function that checks the caller's
operational permission and posts as the system. Never widen a finance permission to make a posting
succeed — that is the shortcut that produced self-verified receipts and the old "mark paid" button.
Pattern to copy: supabase/migrations/20260922120000_posting_in_db_functions.sql.
Related: JournalEntry.groupId is what lets a departure's ledger reconcile against its cost report.
It must be set by construction (derived inside the posting function), not by 47 call sites
remembering to pass it.
How the accounting is judged
Not by unit tests. By re-entering a real departure and comparing against the company's own books:
- The data:
~/Downloads/29 AUG GRP.xlsx(27 pilgrims, ₹28,91,501 billed, 1 cancellation, five sub-agents) andAUG-29REPORT.xlsx(the Busy ledger, accountEXP AUG 29 UMR GRP 19D ( 26-27 ), closing ₹25,87,546 Dr — the authoritative cost). Both are the owner's real data: keep them out of the repo, and keep names, passports and phone numbers out of reports. - The scoreboard: refusals for ordinary operational work (0), unposted entries waiting (0), value
left in Stock-in-Hand for a departure that has flown (0), group reconciliation
unexplained(0), and the departure's cost against Busy's ₹25,87,546. - The scripts:
scripts/dev/— extraction and entry, driving the app's own API as each role.
Rebuilding the environment
git clone <repo> && cd alhuda-travel-planner && npm ci
supabase start --workdir <a scratch dir with a copied config.toml> # shift ports, own project_id
supabase db reset --workdir <same> # builds from migrations
node scripts/dev/seed-demo.mjs # logins + demo data
psql <db> -f scripts/dev/purge-business-data.sql # keeps logins/accounts/settings
npm run dev -- --mode preview --port 5180 --strictPort --host 127.0.0.1
Details and the credentials file location: docs/operations/local-preview.md.
Checks before any commit (all local — CI is skipped on draft PRs to save runner minutes):
CI=true npx vitest run # NOT `npm test` — that is watch mode and hangs
npm run typecheck:ratchet # must report no NEW errors
node scripts/check-migrations.mjs
node scripts/check-docs.mjs
scripts/db-test.sh # against a pristine stack; 8 suites fail on demo rows
npm run build # bundle budgets run via npm run analyze:bundle
Traps that have cost time
scripts/dev/seed-demo.mjsreplays grant migrations in filename order and has broken the access model three separate ways — resurrecting revoked grants, and once an obsoleterecord_paymentoverload that made every receipt fail. After seeding, checkSELECT * FROM public.rls_restrictive_only_commands();returns nothing and runscripts/db-test.sh supabase/tests/access_model.sql.- A freshly built database is not production. Production accumulated policies and rows that no migration creates. Two faults were invisible until a backup was restored locally and tested: the audit-trail policy, and 9 tables whose only policies denied everything on a fresh build. When in doubt, restore the latest backup artifact and test against that.
- Backups omit
supabase_migrations.schema_migrations, so a restored database cannot be migrated forward as-is. - A departure cannot be deleted out of production through the API key. Posted vouchers refuse
deletion (FIN-031, correctly) and inventory a person has touched refuses it too (AIR §26). A
half-finished teardown leaves bookings without invoices and vouchers without documents. To
re-enter a departure, run
scripts/dev/purge-business-data.sqlin the Supabase SQL editor — it suspends the triggers for one transaction, which no API key can do — and load again from the workbooks.scripts/dev/remove-departure.mjsremoves what it can and stops at those two walls. - A failed load leaves rows behind. The block, the FIT row, the hotels, the suppliers and the
departure's cancellation policy are created before the first refusal. Unique codes and PNRs then
block the retry.
scripts/dev/purge-failed-load-rows.mjsnames each row by id and archives what cannot be deleted, moving its code aside. npm testis watch mode. It will hang a background run forever.- Long-running foreground commands stall background agents; prefer short, separate commands.
Decisions the owner still owes
- ~~Cancellation policy as a rule the system can compute.~~ Decided 2026-09-19: kept flexible — policies are configuration (flat + % bands, kept items such as the visa once applied for), with starter policies to review under Operations → Cancellation policies (PRC-020 / PRC-023).
- Unsold bought seats: charge them to the departure that bought them (default, and the truthful
treatment — it turns that group's ₹1.78 lakh reported profit into a ₹21,545 loss) or hold them
centrally.
FinanceConfig.unsoldSeatTreatment. - Two secrets on the production Supabase project, or the features stay dormant:
TURNSTILE_SECRET_KEY(public visa intake form) andWHATSAPP_APP_SECRET(inbound WhatsApp). - Working defaults adopted on the owner's instruction and awaiting confirmation — approval limits,
data scope, incident response times, signal thresholds:
docs/rules/decisions-log.md.
Standing instructions from the owner
- Never commit directly to
main; push freely, but do not open PRs without being asked. - Do not deploy to production without an explicit yes.
- No permission widening to make a posting work.
- Not being built, by instruction: WhatsApp ordering. The webhook, inbox, templates and consent tables exist from earlier work and stay dormant.
- The owner's live data is the yardstick. Where the system and the spreadsheets disagree, say which is right and why — their own records disagree with each other by ₹6,440 on this group, and the Busy ledger contains a ₹5,400 "reconciliation adjustment" that is a plug.
What does not travel between machines
Git carries the work. It does not carry: the Claude session transcript, the per-project memory notes
(~/.claude/projects/<project>/memory/), the local Supabase stacks, or any running agent. Rebuild
the stacks with the commands above; copy the memory directory if you want the accumulated context.
Starting a session on another machine
Copy the memory directory across (52 KB — it holds the owner's standing preferences and the running project log; it is not in git):
<source> ~/.claude/projects/-Users-syed44-Git-Stuff-Git-Projects-alhuda-travel-planner/memory
<destination> ~/.claude/projects/<the new repo path, slashes as dashes>/memory
On Windows use WSL2 (Ubuntu) and keep the repo in the Linux filesystem, not under /mnt/c. Set
git config --global core.autocrlf false before cloning — some files in this repo use CRLF and Git
on Windows will otherwise rewrite them and show thousands of false changes. Docker Desktop with the
WSL2 backend is required for the local Supabase stack.
Then open Claude Code in the repo and paste this:
Read
docs/operations/handover.md,docs/rules/PROGRAM.mdanddocs/audits/2026-09-18-real-group-accounting-test.mdbefore doing anything.Context: I am the owner of Alhuda Travels (Hajj/Umrah/Ziyarat tour operator). We released the operations upgrade to production on 18 September. Since then we have been proving the accounting by re-entering one real departure (29 Aug 2026, 27 pilgrims) and comparing the result against my own Busy ledger. Three runs so far; the books are close but not yet right.
How I want you to work: high autonomy — propose briefly and act, don't confirm every step. Never commit to
main; push freely but don't open PRs unless I ask. Never deploy to production without an explicit yes from me. Never widen a permission to make a posting succeed. Run all checks locally (CI is skipped on draft PRs to save runner minutes) and tell me plainly when something fails or when you are unsure — I would rather hear a problem than a reassurance.Continue the work on branch
fix/postings-one-path: funnel every accounting-entry site insrc/lib/api.tsonto one posting path, make the departure tag automatic, then re-run the real group and report the four numbers — operational refusals, unposted entries waiting, value stuck in Stock-in-Hand for a departure that has flown, and the group reconciliation's unexplained figure — against Busy's closing cost of ₹25,87,546.