Skip to content

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) and AUG-29REPORT.xlsx (the Busy ledger, account EXP 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.mjs replays grant migrations in filename order and has broken the access model three separate ways — resurrecting revoked grants, and once an obsolete record_payment overload that made every receipt fail. After seeding, check SELECT * FROM public.rls_restrictive_only_commands(); returns nothing and run scripts/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.sql in 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.mjs removes 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.mjs names each row by id and archives what cannot be deleted, moving its code aside.
  • npm test is 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

  1. ~~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).
  2. 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.
  3. Two secrets on the production Supabase project, or the features stay dormant: TURNSTILE_SECRET_KEY (public visa intake form) and WHATSAPP_APP_SECRET (inbound WhatsApp).
  4. 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.md and docs/audits/2026-09-18-real-group-accounting-test.md before 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 in src/lib/api.ts onto 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.