Chapter 3.1 — Customers
1. Purpose
This chapter defines the Customer entity in travoBooks — the party to whom the partner sells travel services. It covers the customer master record, the corporate hierarchy model (companies → branches → travellers), credit limits and credit hold behaviour, customer-side accounting impact (AR), KYC capture, and the full lifecycle from creation through dormancy and archival.
In travoBooks terminology: - Customer = the buyer of travel from the partner (a corporate client, a retail walk-in, a travel agency sub-account, or an OTA end-user). - Traveller = the person who actually travels; may differ from customer (a corporate buys, an employee flies). - Supplier is the inverse role (airline, hotel, GDS) — covered in Chapter 3.2.
2. Why it matters in modern travel accounting
The customer record is the left side of the platform's accounts-receivable picture. Every invoice, receipt, credit note, and dunning escalation traces back to a customer record. The quality of this data determines: - whether the partner can collect on credit sales (matched payment → invoice → customer) - whether revenue is recognised against the right counterparty for IFRS 15 - whether VAT/GST invoicing meets local jurisdictional requirements (buyer tax ID, registered address) - whether AML / sanctions screening can be performed before issuance
A weak customer master breaks every downstream module.
3. Industry relevance
Travel agencies typically serve mixed customer bases: - Retail walk-ins — cash-equivalent, low data, single transaction. - Corporate accounts — credit terms, monthly invoicing, cost-centre reporting, travel-policy enforcement. - Sub-agencies / IATA cascade — wholesale customers, net-rate sales, BSP impact. - OTA / online buyer — high volume, low touch, gateway payments. - Group / event clients — one-off but large, custom invoice templates.
travoBooks' customer model accommodates all of these through a single record with type-discriminated behaviour.
4. Compliance considerations
- IFRS 15 — customer is the legal counterparty of the performance obligation; revenue is recognised against this entity.
- VAT / GST — invoice must show buyer's legal name, tax registration ID, and registered address (jurisdiction-dependent).
- AML / sanctions — corporate customers must be screened against OFAC, UN, EU, and local lists at onboarding and re-screened periodically.
- GDPR / data-protection — PII captured for travellers must have lawful basis; deletion-on-request workflow.
- IATA Resolution 850m / TAAP — for sub-agency customers, additional accreditation data may be required.
5. Business logic
5.1 Customer types
| Type | Code | Behaviour |
|---|---|---|
| Walk-in (Cash) | WALKIN |
Default to immediate payment; minimal KYC |
| Corporate | CORPORATE |
Credit terms, monthly statements, cost-centre tracking |
| Sub-Agency | SUBAGENT |
Wholesale pricing, net rates, may have own customers |
| OTA / Online End-User | OTA_END_USER |
High volume, gateway payment only, light record |
| Group | GROUP |
One-off large bookings; custom invoice |
| Internal / Staff | INTERNAL |
Used for staff travel; separate AR account |
5.2 Customer record structure
| Field | Type | Notes |
|---|---|---|
customer_id |
BIGINT PK | Stable internal ID |
partner_id |
BIGINT FK | Tenant scope |
customer_code |
VARCHAR(32) | Partner-visible code; unique per partner |
customer_type |
ENUM | See above |
legal_name |
VARCHAR(255) | For invoicing |
display_name |
VARCHAR(255) | UI-friendly |
tax_id |
VARCHAR(64) | VAT / GST / BIN |
tax_jurisdiction_code |
CHAR(2) | ISO-3166 |
billing_email |
VARCHAR(255) | Primary invoice recipient |
billing_address |
TEXT | Structured address sub-fields |
default_currency |
CHAR(3) | Invoice currency default |
payment_terms_days |
SMALLINT | NET-N days |
credit_limit |
DECIMAL(18,2) | In partner functional currency |
credit_limit_currency |
CHAR(3) | Usually partner functional |
credit_hold |
BOOLEAN | Manual block flag |
pricing_profile_id |
BIGINT FK | Markup / discount rules |
cost_centre_required |
BOOLEAN | Force cost-centre on bookings |
parent_customer_id |
BIGINT FK NULL | For branch-of-company hierarchy |
kyc_status |
ENUM(pending, verified, failed, expired) |
|
sanctions_screen_at |
DATETIME | Last screening |
sanctions_status |
ENUM(clear, hit_pending_review, hit_blocked) |
|
status |
ENUM(active, dormant, suspended, archived) |
Lifecycle |
notes |
TEXT | Internal |
created_at, updated_at, created_by, updated_by |
Audit columns |
5.3 Customer hierarchy
Corporate customers can have branches — a parent_customer_id self-reference creates a tree:
Beta Corporation (parent)
├── Beta Corp — Dhaka HQ
├── Beta Corp — Chittagong
└── Beta Corp — Singapore
└── Beta Corp — Singapore IT Dept (cost centre as branch)
Behaviour:
- Invoices may be issued to branch or parent based on customer-config flag invoice_to.
- Credit limit may be shared (parent enforces) or per-branch (each enforces its own).
- Statements roll up from children to parent on partner-config flag.
5.4 Credit management
A customer with customer_type = CORPORATE typically operates on credit. The platform enforces:
| Check | Logic |
|---|---|
| Credit limit check at booking | outstanding_ar + booking_total ≤ credit_limit |
| Credit hold | If credit_hold = TRUE, no new credit sales accepted |
| Overdue threshold | If oldest open invoice > payment_terms_days + grace_days, optional auto credit hold |
| Sanctions block | If sanctions_status = hit_blocked, no transactions at all |
| KYC block | If kyc_status IN (failed, expired) and partner.kyc_strict = TRUE, block |
Credit limit can be temporarily exceeded via the credit override workflow (maker-checker; see Ch 2.4).
Outstanding AR per customer is a real-time materialised value updated by the JE engine — every INSERT into journal_entry_lines against AR control with customer_id adjusts the customer_balances table in the same transaction.
5.5 Pricing profiles
A customer may be linked to a pricing_profile_id that overrides default markup rules:
- Markup type — fixed, percentage, tiered
- Per-product overrides — air, hotel, transfer
- Negotiated supplier rates — agreed net rates per supplier
- Discount eligibility — flat discount or promo-code allow-list
The pricing profile is read at booking-time by the offer-pricing engine.
6. Inputs → processing → outputs
Create customer
Input: form / API payload with the fields above.
Processing:
1. Validate uniqueness of (partner_id, customer_code) and (partner_id, tax_id) if provided.
2. Resolve parent_customer_id if present; verify same partner_id.
3. Insert customers row.
4. Trigger sanctions screening async (queue job).
5. Initialise customer_balances row with zero balance.
6. Emit customer.created event for webhooks.
Output: customer_id, customer_code, redirect to detail page.
Update credit limit
Input: {customer_id, new_credit_limit, reason}
Processing:
1. Permission check (customers.credit.update.partner or .branch:{id}).
2. If increase exceeds threshold (e.g., > BDT 1,000,000), require maker-checker.
3. Write customer_credit_history row capturing old/new/reason/actor.
4. Update customers.credit_limit.
5. Audit log entry.
6. Emit customer.credit_limit.changed event.
Output: Updated record.
7. Module dependencies
| Direction | Module |
|---|---|
| Depends on | Multi-Tenancy (Ch 1.1), User Management (Ch 2.x), Tax Configuration (Ch 5.9) |
| Depended on by | Bookings, Invoicing, Payments, Reporting, Webhooks, Dunning, Statements |
8. Security & permissions
| Permission | Allows |
|---|---|
customers.read.partner |
View any customer in the partner |
customers.read.own |
View customers owned (created/managed) by the user |
customers.create.partner |
Create new customer |
customers.update.partner |
Edit master data |
customers.credit.update.partner |
Adjust credit limit |
customers.credit.override.partner |
Approve a single transaction breaching limit |
customers.archive.partner |
Soft-archive |
customers.delete.platform |
Hard-delete (platform admin only) |
Tax IDs and KYC documents are sensitive — access logged separately for SOC 2.
9. Validation rules
| Code | Condition |
|---|---|
CUSTOMER_CODE_DUPLICATE |
(partner_id, customer_code) already exists |
CUSTOMER_TAX_ID_DUPLICATE |
Same tax_id used by another customer in the same partner |
CUSTOMER_PARENT_DIFFERENT_PARTNER |
Parent customer belongs to a different partner |
CUSTOMER_PARENT_CYCLE |
Hierarchy would create a cycle |
CUSTOMER_INVALID_CURRENCY |
default_currency not in partner_currencies |
CUSTOMER_NEGATIVE_CREDIT_LIMIT |
Credit limit < 0 |
CUSTOMER_KYC_REQUIRED |
Tries to transact while KYC pending in strict-mode partner |
CUSTOMER_SANCTIONS_BLOCKED |
Sanctions screening flagged the record |
CUSTOMER_ARCHIVED |
Operation on archived customer |
CUSTOMER_CREDIT_EXCEEDED |
Booking would exceed credit limit without override |
CUSTOMER_CREDIT_ON_HOLD |
Credit hold active |
CUSTOMER_OVERDUE_LOCKED |
Auto credit hold from overdue policy |
10. Error handling
Validation errors surface inline in the UI; API responses use standard 400 envelope:
{
"error": {
"code": "CUSTOMER_CODE_DUPLICATE",
"message": "A customer with this code already exists.",
"field": "customer_code",
"details": { "existing_customer_id": 12345 }
}
}
Credit-block at booking-time surfaces with the override pathway visible to authorised roles only.
11. Real-world examples
Example A — Onboarding a corporate customer
Beta Corporation onboarded:
- customer_code = BETA-DHK-001
- legal_name = Beta Corporation Ltd.
- tax_id = BD-BIN-123456789 (Bangladesh BIN)
- default_currency = BDT
- payment_terms_days = 30
- credit_limit = 5,000,000 BDT
- Hierarchy: parent record only.
Sanctions screening: clear. KYC: verified after document upload.
First booking issued 5 days later: BDT 80,000 → JE posts a debit to AR-Trade (1101) with customer_id = beta_id and credit to revenue/supplier-payable.
Example B — Credit hold from overdue
Beta has three open invoices totalling BDT 320,000 at month-end; oldest is 42 days past due. Partner policy: auto credit hold at terms + 7 days.
At 06:00 the nightly job evaluates and flips credit_hold = TRUE, posts an audit entry, and emails the accountant + a courtesy notice to Beta's billing contact. New bookings now return CUSTOMER_CREDIT_ON_HOLD until a payment clears or override is granted.
Example C — Branch invoicing
Beta has 4 branches. Configuration:
- invoice_to = branch
- statement_rollup = parent
Each branch is invoiced separately (own AR sub-ledger). Statements consolidate to parent at month-end. AR aging report can be viewed at parent level (rolled) or branch level (granular).
12. Step-by-step workflow
13. Database tables touched
| Table | Role |
|---|---|
customers |
Master record |
customer_addresses |
Multiple addresses (billing, shipping, registered) |
customer_contacts |
Multiple contact persons |
customer_credit_history |
Audit of credit-limit changes |
customer_balances |
Real-time AR balance, materialised |
customer_kyc_documents |
Uploaded KYC artefacts (S3 refs) |
customer_sanctions_screenings |
Screening run history |
customer_pricing_profiles |
Negotiated rates |
audit_logs |
Every change |
14. Future scalability
- Self-service customer portal (Phase 2) — corporates view their own invoices, statements, pay online.
- API-driven onboarding — sub-agencies provisioned via partner API.
- Continuous sanctions screening — daily delta-screen against updated watchlists, not just on creation.
- Customer scoring — payment behaviour score influencing credit-policy automation.
- Multi-partner customer linking (Phase 3) — same legal entity recognised across partners in a group for consolidated reporting.
15. Common pitfalls
- ⚠️ Don't reuse a customer code across partners as if it were global. It's scoped to
partner_id— same legal entity at two partners gets two records. - ⚠️ Don't update tax_id silently — invoices already issued under the old tax_id are immutable; new invoices use the new ID.
- 🔒 KYC documents must never be returned in list responses. Detail-fetch only, with separate audit log.
- ⚠️ Don't hard-delete customers with historical bookings. Archive only — financial records depend on the FK.
- ⚠️ Currency at customer level is a default, not a constraint. A USD-default corporate may still buy a BDT ticket; the invoice currency is per-transaction.