In this volume · VOLUME 05
Accounting
Chart of Accounts Journals & Ledger Billing & Invoicing Payments & Receipts Payment Reconciliation Multi-Currency Deferred Revenue Commission Accounting Tax (VAT/GST) Period Close

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:

  1. SUM(debit_amount) = SUM(credit_amount) per JE (in functional currency, to the cent).
  2. For each line, exactly one of debit_amount or credit_amount is > 0; the other is 0.
  3. account_id must reference a is_postable = true, is_active = true account in the same partner.
  4. period_id must be open. Posting to a closed period is forbidden; posting to a locked period requires an unlock action with audit.
  5. 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_id pointing to the original.
  • Has lines that exactly invert the original (debits → credits, credits → debits).
  • Carries entry_date equal to the correction date, not the original date.
  • The original JE has its status updated to reversed and reversed_by_id set.
  • 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:

stateDiagram-v2 [*] --> Open Open --> SoftClosed: All ops finalised SoftClosed --> Open: Reopen (with audit) SoftClosed --> Closed: Period close completed Closed --> Locked: Year close Locked --> [*]
  • 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_approval state if amount > threshold.
  • Otherwise, posted immediately.
  • 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

sequenceDiagram autonumber participant U as Accountant participant UI participant API participant Svc as Journal Service participant FX as FX Service participant DB participant N as Notifications U->>UI: Accounting → Journals → "New Journal" UI->>API: GET /accounts (for picker) API->>UI: postable accounts U->>UI: Fill lines, save draft UI->>API: POST /journals/draft API->>Svc: validate(payload) Svc->>FX: resolveRates(non-functional lines) FX-->>Svc: rates Svc-->>API: validated draft API-->>UI: 201 draft id U->>UI: "Submit for approval" UI->>API: POST /journals/:id/submit API->>Svc: requires approver? Svc-->>API: yes (amount > threshold) API->>DB: status=pending_approval API->>N: notify approvers API-->>UI: 202 Note over U,N: --- different actor logs in --- U2->>UI: Approvals queue → open UI->>API: POST /journals/:id/approve API->>Svc: assert maker ≠ checker Svc->>DB: BEGIN Svc->>DB: status=posted, approved_by=actor Svc->>DB: write lines (immutable) Svc->>DB: audit_log Svc->>DB: COMMIT API-->>UI: 200

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_date to 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.