Data Model
Reference for the core entities that run the ERP — what they represent, the primary key / main foreign keys, life-cycle states, and the permissions that gate reads and writes. Source of truth is supabase/migrations/*.sql; the TypeScript projection lives in src/integrations/supabase/types.ts (generated) and the domain types in src/types/index.ts.
Naming conventions
- Tables use
PascalCase, quoted:public."Booking". - Columns are
camelCase, also quoted in SQL:"bookingNo","totalAmount". - Primary keys are
TEXTwith app-generated UUIDs (except a few newer tables that defaultgen_random_uuid()::text). - Timestamps are
timestamp(3) without time zoneon legacy tables,TIMESTAMPTZon newer ones.
Entity map (top ~10 entities)
erDiagram
Customer ||--o{ Booking : "places"
Agent ||--o{ Booking : "sells (optional B2B)"
TravelGroup ||--o{ Booking : "groups together"
Booking ||--o{ BookingPassenger : "has passengers"
Booking ||--o{ Payment : "receives"
Booking ||--o{ Invoice : "bills"
Booking ||--o{ VisaCase : "triggers visa"
Booking ||--o{ TicketRecord : "triggers tickets"
TravelGroup ||--o{ GroupInvoice : "issues to payers"
TravelGroup ||--o{ GroupHotelAssignment : "assigns hotels"
Supplier ||--o{ SupplierTransaction : "booked against"
Account ||--o{ JournalLine : "debited/credited"
JournalEntry ||--|{ JournalLine : "has 2+ balanced lines"
Payment }o--|| Account : "lands in"
AccountingPeriod ||--o{ JournalEntry : "contains (by date)"
BankStatementImport ||--o{ BankStatementLine : "has lines"
BankStatementLine }o--o| JournalEntry : "matched to"
TDSRate ||--o{ TDSDeduction : "applied via"
TDSDeduction }o--|| Payment : "deducted on"
HotelInventory ||--o{ GroupHotelAssignment : "allocated to groups"
Core entities
Booking
Represents: One customer's (or partner's) purchase of travel services, attached to a travel group.
| Aspect | Detail |
|---|---|
| Table | public."Booking" — supabase/migrations/20260113130121_remote_schema.sql:91 |
| PK | id TEXT |
| FKs | customerId → Customer, groupId → TravelGroup, agentId → Agent (nullable), quotationId → Quotation, payerId → Customer |
| Status fields | status ∈ pending | pending_finance | confirmed | cancelled | needs_correction, plus opsApprovalStatus and financeApprovalStatus (both pending | approved | rejected | cancelled | needs_correction) |
| Lifecycle | Draft → PendingOpsReview → PendingFinanceApproval → Approved, with branches to OnHold / Cancelled. See docs/ALHUDA_ERP_STATE_MACHINES.md §Booking. |
| Read | bookings.view |
| Write | bookings.create, bookings.edit, bookings.delete (all RLS-enforced via auth_user_has_permission()) |
Customer
Represents: An end-traveller — whether they booked directly or through a B2B partner. Stores passport + KYC fields, GSTIN, Aadhaar, PAN, plus optional agent linkage.
| Aspect | Detail |
|---|---|
| Table | public."Customer" — supabase/migrations/20260113130121_remote_schema.sql:157 |
| PK | id TEXT |
| FKs | agentId → Agent (optional — which partner introduced them), sourceAgentId (original source) |
| Lifecycle | No explicit states — soft-deleted via deletedAt. Status is implicit through related bookings. |
| Read | customers.view |
| Write | customers.create, customers.edit, customers.delete |
Supplier
Represents: A third-party vendor (airline, hotel, visa consultant, food caterer, transport, general). Has its own ledger (SupplierTransaction) for AP aging.
| Aspect | Detail |
|---|---|
| Table | public."Supplier" — supabase/migrations/20260330110000_suppliers.sql:2 |
| PK | id TEXT |
| Categories | ticketing | visa | hotel | food | transport | general (src/types/index.ts:102) |
| Child | SupplierTransaction (debit/credit/refund/adjustment) |
| Read | inventory.view (see Sidebar drift note in docs/PERMISSIONS.md §7 — intended target is suppliers.view) |
| Write | suppliers.create, suppliers.edit, suppliers.delete |
Partner (Agent)
Represents: A B2B partner — a travel sub-agent who books on behalf of their own customers. Distinct from the staff User who logs in — a partner typically has one User account whose userId is linked here.
| Aspect | Detail |
|---|---|
| Table | public."Agent" — supabase/migrations/20260113130121_remote_schema.sql:34 |
| PK | id TEXT |
| FKs | userId → User (optional — the portal login) |
| Status | active | inactive | suspended | pending |
| Fields | commissionRate, creditLimit, totalBookings, totalRevenue |
| Read | agents.view |
| Write | agents.create, partners.edit, partners.delete (legacy names retained for RLS; see docs/PERMISSIONS.md §8.1) |
TravelGroup (Group)
Represents: A scheduled group departure — the aggregate that flights, hotels, ground transfers, and passengers attach to. A booking is always for a group.
| Aspect | Detail |
|---|---|
| Table | public."TravelGroup" — supabase/migrations/20260113130121_remote_schema.sql:593 |
| PK | id TEXT |
| FKs | payerCustomerId → Customer (the customer who funds the whole group, e.g. a VIP family); many child tables (GroupHotelAssignment, GroupFlightOption, GroupInvoice, etc.) |
| Group types | UMRAH | ZIYARAT | UMRAH_ZIYARAT | HAJ | HAJ_ZIYARAT | GENERAL_DOMESTIC | GENERAL_INTERNATIONAL (src/types/index.ts:252) |
| Status | planning | open | full | departed | completed | closed | cancelled, with an override field (statusOverride) for manual intervention |
| Read | groups.view |
| Write | groups.create, groups.edit, groups.delete, group_pricing.edit (rate sheet), group_invoices.* (invoice lifecycle) |
HotelInventory (Hotel)
Represents: A hotel room-night inventory record — typically leased from a supplier for a date range and a city (Makkah / Madinah / elsewhere).
| Aspect | Detail |
|---|---|
| Table | public."HotelInventory" — supabase/migrations/20260113130121_remote_schema.sql:285 (plus hotel-specific migrations 20260331100000_hotel_inventory_supplier_link.sql, 20260331130000_hotel_inventory_lease_dates.sql, 20260403000000_hotel_assignment_dates.sql) |
| PK | id TEXT |
| FKs | supplierId → Supplier, accountId → Account (GL cost account) |
| Child | GroupHotelAssignment — which group is placed in which hotel for which date range |
| Read | hotels.view |
| Write | hotels.create, hotels.edit, hotels.delete, hotels.export |
Voucher (numbered JournalEntry)
Represents: A finance voucher — receipt, payment, contra, or manual journal. In this schema there is no separate Voucher table; a voucher is a JournalEntry with voucherNo populated.
| Aspect | Detail |
|---|---|
| Table | public."JournalEntry" — base at supabase/migrations/20260113130121_remote_schema.sql:323; voucherNo added by supabase/migrations/20260330170000_account_hierarchy_and_vouchers.sql |
| PK | id TEXT |
| Key fields | voucherNo, memo, referenceType, referenceId, approvalStatus, currency, fxRate, originalAmount |
| Idempotency | memo may be tagged [idem:<key>] — see src/lib/api.ts:2128-2155 |
| Read | finance.view |
| Write | finance.create, finance.edit; approval / reversal gated by finance.journals.approve (default) and the break-glass finance.journals.approve_own / finance.journals.reverse_own — see maker-checker in supabase/migrations/20260418150000_journal_maker_checker_overrides.sql |
JournalEntry
Represents: The header of a double-entry ledger posting. Paired with 2+ JournalLine rows that sum to zero.
| Aspect | Detail |
|---|---|
| Table | public."JournalEntry" — supabase/migrations/20260113130121_remote_schema.sql:323 |
| PK | id TEXT |
| Extensions | currency + fxRate (20260412050000_journal_entry_currency.sql, 20260418060325_journal_fx_rate.sql), originalAmount (20260412060000), approvalStatus (20260412070000, default pending per 20260418110000), reversedEntryId, reversalReason (20260330180000_journal_reversal_tracking.sql) |
| Period lock | Enforced by trigger — supabase/migrations/20260418150000_journal_period_lock_trigger.sql |
| Maker-checker | Creator cannot approve / reverse own entries without override permission |
JournalLine
Represents: One debit-or-credit line on a journal entry. Enforces double-entry via the sum-to-zero invariant.
| Aspect | Detail |
|---|---|
| Table | public."JournalLine" — supabase/migrations/20260113130121_remote_schema.sql:334 |
| PK | id TEXT |
| FKs | entryId → JournalEntry, accountId → Account |
| Extensions | originalDebit / originalCredit for multi-currency (20260418150000_journal_line_original_amounts.sql) |
Account
Represents: A chart-of-accounts ledger account. Hierarchical (group vs. ledger) with parentId self-reference. Canonical chart seeded by supabase/migrations/20260403040000_seed_chart_of_accounts.sql + canonicalised by 20260412020000_chart_of_accounts_canonical.sql.
| Aspect | Detail |
|---|---|
| Table | public."Account" — base at supabase/migrations/20260113130121_remote_schema.sql:22, extended with parentId, isGroup, groupType, openingBalance, openingBalanceSide, sortOrder, etc. |
| PK | id TEXT |
| Unique | code |
| Types | ASSET | LIABILITY | EQUITY | INCOME | EXPENSE (see AccountType enum) |
| Read | finance.view |
| Write | finance.edit (RLS — supabase/migrations/20260330150000_account_rls_policies.sql) |
FinancialAccount vs Account
There is a separate FinancialAccount table (supabase/migrations/20260401100000_accounts.sql) representing payment instruments (bank account, card, cash, digital wallet). It is linked to a GL Account via glAccountId. Think of FinancialAccount as "the physical bank account where money actually sits" and Account as "the GL line that tracks it".
AccountingPeriod
Represents: A calendar window (usually a month) that can be open, locked, or closed for posting. Journal entries are assigned to a period by date.
| Aspect | Detail |
|---|---|
| Table | public."AccountingPeriod" — supabase/migrations/20260330190000_accounting_periods.sql:2 |
| PK | id TEXT |
| Status | open | locked | closed |
| Trigger | supabase/migrations/20260418150000_journal_period_lock_trigger.sql rejects posts into locked/closed periods |
| Read | finance.view |
| Write | Coarse finance.edit today; granular finance.periods.create | .lock | .unlock | .close and finance.years.create | .close are seeded but not all wired in code yet — see docs/PERMISSIONS.md §4.4 and §8.10 |
Invoice
Represents: A customer-facing bill for a booking. Installment-capable (installmentNo).
| Aspect | Detail |
|---|---|
| Table | public."Invoice" — supabase/migrations/20260113130121_remote_schema.sql:305 |
| PK | id TEXT |
| FKs | bookingId → Booking |
| Status | InvoiceStatus enum — OPEN | PAID | PARTIAL | VOID (see migration 20260411070000 for B2B extensions) |
| Sibling tables | GroupInvoice (for group-level billing to a payer, supabase/migrations/20260418000000_group_pricing_and_invoices.sql:25), AgentInvoice (B2B partner invoices, supabase/migrations/20260409140000_agent_portal_v2.sql:7) |
| Read / Write | Bookings → bookings.view; Group invoices → group_invoices.view | .create | .edit | .issue | .cancel; Agent invoices follow partner portal ownership checks |
Payment
Represents: A cash / bank / card / UPI / cheque receipt against a booking.
| Aspect | Detail |
|---|---|
| Table | public."Payment" — supabase/migrations/20260113130121_remote_schema.sql:444 |
| PK | id TEXT |
| FKs | bookingId → Booking, invoiceId → Invoice (optional), sourceAccountId (20260418123000_payment_source_account.sql) |
| Method | cash | bank_transfer | card | cheque | upi | wallet |
| Status | pending | verified | rejected | refunded |
| Razorpay | razorpayPaymentId UNIQUE (dedup key for webhook — supabase/migrations/20260418114458_razorpay_payment_fields.sql) |
| Read | finance.view |
| Write | finance.create (record), finance.payments.verify | .reject | .refund (workflow — many still coarse-gated today; see docs/PERMISSIONS.md §8.10) |
BankReconciliation
Represents: A bank statement import and its parsed lines, each of which can be matched to a JournalEntry / Payment / LedgerEntry.
| Aspect | Detail |
|---|---|
| Tables | public."BankStatementImport", public."BankStatementLine" — supabase/migrations/20260418150000_bank_reconciliation.sql:15-68 |
| PK | id TEXT on each |
| FKs | Import.financialAccountId → Account, Line.importId → BankStatementImport, Line.matchedEntryId → (journal_entry | payment | ledger_entry) discriminated by matchedEntryType |
| Status | Import: imported | reconciled | partial; Line: unmatched | matched | ignored |
| Read | finance.view (RLS — supabase/migrations/20260418150000_bank_reconciliation.sql:93) |
| Write | finance.edit |
TdsChallan
Represents: TDS (Tax Deducted at Source) infrastructure — rate master, per-deduction rows, and the plumbing for 26Q quarterly filing. Per-challan header is part of TDSDeduction.
| Aspect | Detail |
|---|---|
| Tables | public."TDSRate", public."TDSDeduction" (+ a couple of columns on Supplier for defaults) — supabase/migrations/20260418150000_tds_scaffolding.sql |
| PK | id TEXT (uuid default) on each |
| TDSRate keys | section (e.g. 194C, 194J, 194Q), deducteeType (individual | huf | company | any), effectiveFrom — combined unique |
| TDSDeduction | links to the Payment / SupplierTransaction that was deducted on, plus Form 281 challan + Form 16A certificate refs |
| GL | 2401 TDS Payable under Duties & Taxes |
| Read | finance.tds.view |
| Write | finance.tds.deduct; export 26Q via finance.tds.export (permissions migration 20260418150100_tds_permissions.sql) |
GstReturn
Represents: GST (India) compliance surface. As of 2026-04-18 the schema provides primitives — GSTIN on Customer / Supplier, hsnCode / sacCode on line-item-like tables, GST amount + rate on Booking, and per-line GST on group expenses. A dedicated GstReturn table is not yet present; returns (GSTR-1 / GSTR-3B) are computed on demand from the journals via src/lib/gstReports.ts. Filing to the GSTN portal is a stub (src/lib/gstnApiClient.ts).
| Aspect | Detail |
|---|---|
| Primitive columns | Customer.gstNumber, Supplier.gstNumber, QuotationLineItem.hsnCode / sacCode, SupplierTransaction.hsnCode / sacCode, Invoice.hsnCode / sacCode — supabase/migrations/20260418150000_gst_primitives.sql |
| Other GST migrations | 20260415090000_booking_gst_enabled.sql, 20260415100000_gst_input_credit_and_supplier_gst.sql, 20260415110000_group_expense_gst_and_journal.sql |
| Read | finance.reports.gst_summary.view (seeded, per docs/PERMISSIONS.md §4.4) |
| Write | Coarse finance.create / finance.edit today; dedicated return-filing permissions are future work |
Supporting entities (quick reference)
| Entity | Table | Purpose | Read | Write |
|---|---|---|---|---|
| Quotation | public."Quotation" + QuotationLineItem |
Price quote that can become a booking | quotations.view |
quotations.create |
| Lead | public."Lead" + LeadAttachment |
Inbound enquiry | leads.view |
leads.edit |
| VisaCase | public."VisaCase" + VisaDocument + VisaStatusHistory |
Per-passenger visa processing | visa.view |
visa.create, visa.edit |
| TicketRecord | public."TicketRecord" + TicketDocument + TicketIssueApproval |
Air ticket issuance | tickets.view |
tickets.edit, tickets.approve |
| BookingPassenger | public."BookingPassenger" + BookingPassengerFlight + BookingPassengerHotel |
Passenger manifest per booking | bookings.view |
bookings.edit |
| CustomerRequest | public."CustomerRequest" + attachments + notes |
Service requests (customer / ops) | requests.view |
requests.create, requests.edit |
| FinancialAccount | public."FinancialAccount" + FinancialAccountTransaction |
Physical bank / wallet / card instruments | finance.view |
finance.edit |
| LedgerEntry | public."LedgerEntry" |
Per-booking running ledger (mirrors journal but booking-scoped) | finance.view |
via finance.create / .edit postings |
| GroupInvoice | public."GroupInvoice" |
Payer-level invoices at group level | group_invoices.view |
group_invoices.create / .edit / .issue / .cancel |
| CommunicationsLog | public."CommunicationsLog" |
Any outbound message (WhatsApp, SMS, email) | Bookings / customer views | module-specific |
State machines (reference)
Formal state-machine rules live in docs/ALHUDA_ERP_STATE_MACHINES.md:
- Booking —
Draft → PendingOpsReview → PendingFinanceApproval → Approved, withOnHoldandCancelledbranches. - Visa —
NotStarted → Applied → UnderProcess → Issued | Rejected, plusSentToEmbassydetour. - Ticket —
NotReady → NameUpdatePending → OnHold | FinanceCleared → Issued; issuance blocked unless finance-cleared or a GM exception proof exists. - GroupInvoice —
DRAFT → ISSUED → PAID, withCANCELLEDterminal (credit note posts reversal). Seesrc/types/index.ts:369.
Respect the server-side validators
canTransitionBooking() / canTransitionVisa() / canTransitionTicket() and the policy validators in docs/ALHUDA_ERP_STATE_MACHINES.md §Server-Side Validators are not advisory — the API handlers call them before permitting a state change, and every transition writes to the *StatusHistory table for audit.
Drift notes
- Legacy permission names like
partners.create | .edit | .deleteare retained for RLS compatibility (docs/PERMISSIONS.md§8.1); new code should prefer theagents.*family. - A number of granular permissions are seeded but not wired yet (finance report exports, workflow splits, admin splits) —
docs/PERMISSIONS.md§4.4 and §8.10 list them. Until wired, coarse permissions (finance.create,finance.edit,admin.view,admin.edit) still gate those actions. AgentWallet/ wallet payment methods were removed insupabase/migrations/20260411060000_remove_wallet_system.sql; thewalletenum value onPayment.methodremains for historical rows.
Further reading
supabase/migrations/*.sql— authoritative schema.src/types/index.ts— domain TypeScript types.src/integrations/supabase/types.ts— generated Supabase types (do not hand-edit).docs/ALHUDA_ERP_PRD.md— product-requirements ground truth.docs/ALHUDA_ERP_STATE_MACHINES.md— canonical state rules.