Chapter 5.2 — Journals & General Ledger
chapter: 05-accounting/02-journals-and-ledger
version: 1.0.0
status: stable
last_reviewed: 2026-05-26
owners: [accounting, platform-engineering]
1. Purpose
This chapter describes the heart of travoBooks: the journal entry as the atomic, immutable, balanced unit of financial change, and the general ledger as the resulting permanent record. Every other accounting concept ultimately resolves to a posting here.
2. Why this matters in modern travel accounting
The single most common defect in travel back-office systems is the gap between operational reality and the books. A booking exists; a ledger entry does not. A refund passes through the GDS; the AR balance is wrong for three weeks. A commission is accrued in a spreadsheet; nobody reverses it when the airline issues an ADM.
travoBooks eliminates this by making operations write to the ledger inside the same transaction. There is no "post to accounting" job. There is no "end-of-day sync". A booking either is in the ledger or it does not exist.
This architectural choice imposes discipline:
- Every operational action that has financial consequence must define its journal pattern.
- That journal pattern must balance to zero.
- It must reference real CoA codes the partner has configured.
- It must carry the dimensions required by those accounts (supplier_id, customer_id, branch, cost centre).
- It must capture the FX rate used.
- It must record the trigger (booking, ticket, refund, payment) so the audit trail to the operational event is one click away.
3. Industry relevance
This pattern is how serious ERP systems work (SAP, Oracle, NetSuite). It is not how most travel-specific tools work, which is why ADM disputes and BSP reconciliation are nightmare tasks at most agencies. travoBooks brings ERP discipline to an industry that has historically tolerated weaker controls.
4. Compliance considerations
| Standard | Implication |
|---|---|
| IFRS / IAS 1 | Financial statements must be free from material misstatement. Immutability of posted journals supports this. |
| IFRS 15 / ASC 606 | Revenue recognised only when control transfers. Encoded via deferred-revenue postings reversed at service date. |
| IAS 21 | FX rate at transaction date must be applied and preserved. Stored on every ledger row. |
| IATA BSP audit | Agencies must produce a complete trail from sale to settlement. Joined via the source link on each JE. |
| SOX 404 (for partners filing SEC reports) | Internal controls over financial reporting. Maker–checker + immutability + audit log = evidence. |
5. Business logic
5.1 Anatomy of a journal entry
A journal entry has a header and one or more lines.
JournalEntry
├── id
├── partner_id
├── entry_no ("JE-P001-202605-001234")
├── entry_date (date the JE belongs to, business date)
├── posting_date (timestamp it was posted)
├── period_id (resolved from entry_date)
├── source_type (booking | ticket | refund | invoice | payment | manual | system_recurring)
├── source_id (FK to source entity)
├── description
├── functional_currency (snapshot; ='partner.functional_currency' at posting time)
├── status (posted | reversed | reversed_by_id)
├── reversal_of_id (if this entry reverses another)
├── created_by_user_id
├── approved_by_user_id (nullable; for manual entries above threshold)
├── created_at
└── Lines[]
├── line_no
├── account_id (FK to CoA)
├── debit_amount (functional currency, >= 0)
├── credit_amount (functional currency, >= 0)
├── transaction_currency
├── transaction_amount
├── fx_rate
├── dimensions
│ ├── customer_id?
│ ├── supplier_id?
│ ├── booking_id?
│ ├── ticket_id?
│ ├── branch_id?
│ ├── cost_centre_id?
│ ├── project_id?
└── description
Invariants enforced at the database and application layers:
SUM(debit_amount) = SUM(credit_amount)per JE (in functional currency, to the cent).- For each line, exactly one of
debit_amountorcredit_amountis > 0; the other is 0. account_idmust reference ais_postable = true,is_active = trueaccount in the same partner.period_idmust beopen. Posting to aclosedperiod is forbidden; posting to alockedperiod requires an unlock action with audit.- Once
status = posted, no field of the JE or its lines may be updated. Period.
5.2 Reversing entries
Corrections happen via reversal. A reversal:
- Is a new JE with
reversal_of_idpointing to the original. - Has lines that exactly invert the original (debits → credits, credits → debits).
- Carries
entry_dateequal to the correction date, not the original date. - The original JE has its
statusupdated toreversedandreversed_by_idset. - The original lines remain untouched.
5.3 Sources of journal entries
| Source | Trigger | Typical lines |
|---|---|---|
| Booking issuance | Ticket issued | 🅓 AR / 🅒 BSP Payable / 🅒 Revenue / 🅒 Tax Payable / + Deferred Revenue split |
| Manual journal | Accountant creates | Any |
| Customer payment | Receipt recorded | 🅓 Cash/Bank / 🅒 AR |
| Supplier payment | Payment to BSP / hotel | 🅓 AP / 🅒 Cash/Bank |
| Refund | Refund processed | Reverses booking lines; partial refunds reverse pro-rata |
| Void | Void within window | Reverses entire issuance JE |
| Period close | Period closing job | FX revaluation, accruals, deferral releases |
| Recurring | Subscription, depreciation | Per schedule |
| ADM | Airline debit memo | 🅓 ADM Expense / 🅒 BSP Payable |
| ACM | Airline credit memo | 🅓 BSP Payable / 🅒 Other Income or original expense |
| BSP settlement | BSP file imported | 🅓 BSP Payable / 🅒 Cash + reconciliations |
5.4 The General Ledger
The GL is a logical view over the line table, not a separate physical store. A GL query "show me all activity on account 1021 between 2026-01-01 and 2026-03-31 for customer X" filters journal lines by account, date range, and dimensions.
This means:
- Posting a JE is writing to the GL.
- The GL is always exactly consistent with journal entries.
- There is no separate "GL job" that can fail.
5.5 Periods and locks
A period is typically a calendar month. Periods have states:
- Open — postings flow freely.
- Soft-closed — automatic postings still allowed; manual JEs need approver.
- Closed — no new postings except through "prior-period adjustment" with audit and management approval.
- Locked — fiscal year closed; no changes ever.
6. Inputs → processing → outputs
6.1 System-generated JE (e.g., booking issuance)
Input: the operational event (a booking issuance) with all amounts in transaction currency, plus partner functional currency and rate source.
Processing:
1. Resolve the JE pattern template for booking.issued.
2. Resolve each line's account_id from the partner's CoA mapping rules.
3. Compute functional amounts via FX rate at issuance time.
4. Validate balance (debits = credits to the cent).
5. Validate dimension requirements per account.
6. Validate period is open.
7. Allocate the next entry_no from a partner-scoped sequence.
8. Insert header + lines.
9. Within the same DB transaction, the booking row is also written.
Output: the JE id; the booking is now persisted and balanced; the audit log is updated.
6.2 Manual JE (input)
journal_entry:
entry_date: "2026-05-26"
description: "Correction of supplier invoice mis-coding"
lines:
- account_code: "6033"
debit_amount: 1500.00
currency: "BDT"
dimensions: {supplier_id: "S-100"}
description: "GDS subscription fee"
- account_code: "2014"
credit_amount: 1500.00
currency: "BDT"
dimensions: {supplier_id: "S-100"}
6.3 Outputs
- A balanced JE in
pending_approvalstate if amount > threshold. - Otherwise,
postedimmediately. - Audit log entry; notification to approver if pending.
7. Module dependencies
- Reads: Chart of Accounts, Tax profiles, FX rates, Periods, Customers/Suppliers (for dimensions), Permission catalog.
- Writes:
journal_entries,journal_entry_lines,audit_logs,notification_outbox. - Read by: every Reporting chapter, the Payments module (to apply receipts against AR balances), AP module, period-close jobs, audit packs.
8. Security & permissions
| Permission | Allows |
|---|---|
journal.read.partner |
View posted JEs |
journal.read.detail.partner |
View full line dimensions including supplier net cost |
journal.create.manual.partner |
Create manual JEs |
journal.approve.partner |
Approve manual JEs (must be ≠ creator) |
journal.reverse.partner |
Issue a reversing entry |
period.close.partner |
Run period close |
period.reopen.partner |
Reopen a closed period (logged, alerts admin) |
🔒 Direct DB write access to journal_entries is forbidden even to platform admins. All writes flow through the service that enforces invariants.
9. Validation rules
| Rule | Code |
|---|---|
| Lines balance to zero in functional currency | JE_UNBALANCED |
| At least 2 lines | JE_INSUFFICIENT_LINES |
| Exactly one of debit/credit per line | JE_LINE_AMBIGUOUS |
account.is_postable = true |
JE_ACCOUNT_NOT_POSTABLE |
account.is_active = true |
JE_ACCOUNT_INACTIVE |
| Account requires dimension X, line missing X | JE_DIMENSION_REQUIRED |
| Account is control; source is not system | JE_CONTROL_DIRECT_POST |
entry_date ≥ period's open date |
JE_PERIOD_CLOSED |
| FX rate present for any non-functional line | JE_FX_MISSING |
| FX rate's effective date ≤ entry_date | JE_FX_FUTURE |
| Reversal lines must exactly invert source | JE_REVERSAL_MISMATCH |
| Cannot reverse an already-reversed JE | JE_DOUBLE_REVERSAL |
10. Error handling
- Validation failures roll back the entire transaction (including the operational write). The user sees a clear, single error.
- Period-closed errors offer a one-click "create as prior-period adjustment with approval" path for authorised users.
- FX rate missing: the system attempts a cached rate, then the configured provider; if both fail, the post is rejected and the rate fetch is enqueued — the user is told to retry.
- An attempt to post a control account directly returns a clear remediation hint (e.g., "use Customer Payment to credit AR").
11. Real-world examples
Example A — Issuance of a domestic ticket
Partner P-001, functional BDT. Domestic Dhaka–Cox's Bazar ticket on Biman. - Sell BDT 12,500 - Net BDT 11,200 - Base commission BDT 900 (estimated; recognised at service date) - Service fee BDT 400 - VAT 15% on service fee = BDT 60
JE at issuance (BSP carrier):
| Acct | Line | 🅓 | 🅒 |
|---|---|---|---|
| 1021 AR – Walk-in | 1 | 12,560 | |
| 2011 BSP Payable | 2 | 11,200 | |
| 4031 Service Fee Revenue | 3 | 400 | |
| 2021 VAT Output Payable | 4 | 60 | |
| 2031 Deferred Air Revenue | 5 | 900 |
Lines balance: 🅓 12,560 = 🅒 12,560. ✓
At service date (flight flown), a system-recurring JE releases the deferral:
| Acct | Line | 🅓 | 🅒 |
|---|---|---|---|
| 2031 Deferred Air Revenue | 1 | 900 | |
| 4011 Air Base Commission Revenue | 2 | 900 |
Lines balance: ✓.
Example B — Cash collected from customer
Customer pays BDT 12,560 in cash.
| Acct | 🅓 | 🅒 |
|---|---|---|
| 1011 Cash – Counter | 12,560 | |
| 1021 AR – Walk-in | 12,560 |
Example C — Full refund before service date
Customer cancels; full refund. The platform issues a reversing JE for the issuance JE (and the deferral release if any). If a refund fee BDT 300 + VAT BDT 45 is retained, an additional fee JE posts.
Reversal JE (reverses Example A):
| Acct | 🅓 | 🅒 |
|---|---|---|
| 2011 BSP Payable | 11,200 | |
| 4031 Service Fee Revenue | 400 | |
| 2021 VAT Output Payable | 60 | |
| 2031 Deferred Air Revenue | 900 | |
| 1021 AR – Walk-in | 12,560 |
Refund fee JE:
| Acct | 🅓 | 🅒 |
|---|---|---|
| 1021 AR – Walk-in | 345 | |
| 4041 Cancellation Fee Revenue | 300 | |
| 2021 VAT Output Payable | 45 |
Net AR balance after both JEs: 12,560 - 345 = 12,215 to be refunded to the customer.
Example D — ADM received
Three months later the airline issues an ADM for BDT 800 (incorrect fare basis was used).
| Acct | 🅓 | 🅒 |
|---|---|---|
| 504x ADM Net Impact | 800 | |
| 2011 BSP Payable | 800 |
This hits the P&L in the current period.
12. Step-by-step — posting a manual journal entry
13. Database tables touched
journal_entries— header; append-only.journal_entry_lines— lines; append-only.journal_entry_dimensions— dimensional values (denormalised onto lines optionally for performance).periods— period state.fx_rates— rates used.audit_logs— every state change.
A complete schema and DDL appears in 10-database/03-table-reference.md.
14. Future scalability
| Pressure | Response |
|---|---|
| Very high JE volume (millions/month per partner) | Partition journal_entry_lines by (partner_id, period_id). Already keyed for it. |
| Real-time GL queries slow | Materialise per-account-per-period balances in gl_balances; rebuild on JE post via trigger or queue. |
| Sub-second close requirements | Continuous-close model: deferral schedules pre-posted; period close becomes a verification, not a computation. |
| Multi-book accounting (IFRS + local GAAP) | A book_id dimension on the JE; each operational event posts to all configured books with possibly different mappings. |
15. Common pitfalls
- ⚠️ Trying to "fix" a posted JE by editing it. Never. Always reverse and re-post.
- ⚠️ Posting at the wrong
entry_dateto slip a transaction into a friendlier period. Period locks and audit logs make this visible. - ⚠️ Forgetting that a refund processed in the GDS does not automatically write a JE — in travoBooks, the refund must be initiated in travoBooks to write the JE. A back-channel GDS refund is a discipline issue.
- ⚠️ Treating commission as recognised at issuance for international airlines with override clauses. Estimated overrides are not earned until confirmed.
- 🔒 Manual JEs that touch control accounts should never be permitted; if a workaround is needed, the right answer is to create a sub-ledger transaction (customer payment, supplier payment, refund) that legitimately drives the control account.
Next: 03-billing-invoicing.md — how invoices are generated, issued, paid, and closed.