Approving journals
Every journal entry — whether you type it yourself in the voucher form or it gets auto-posted by a booking / GST split / commission — starts in pending state. It does not affect ledger balances until a second person approves it. This is the maker-checker rule.
This runbook covers both sides of the flow:
- As maker — you create a journal, see it sit in the queue, and wait for a colleague to approve it.
- As checker — you open the queue, review pending journals, and approve or reject.
Maker ≠ checker, ever
You cannot approve your own journal. The system throws a 403 with
the message: Maker-checker: you cannot approve your own journal
entry. Ask a different approver. Only super-admins (CEO / GM /
IT_ADMIN) hold the override permission finance.journals.approve_own,
and that is for break-glass only.
The flow in one diagram
flowchart LR
Maker[Maker<br/>posts voucher] --> Pending[Pending<br/>in queue]
Pending --> Checker[Checker<br/>opens Approvals tab]
Checker --> Review{Review<br/>contents}
Review -->|Looks right| Approve[Approve]
Review -->|Wrong| Reject[Reject]
Approve --> Posted[Posted to ledger]
Reject --> Terminal[Rejected — create<br/>fresh voucher if needed]
As maker — creating a journal and waiting for approval
Create the voucher
See recording-transactions.md for the full voucher forms. Any of Receipt, Payment, Contra, Debit Note, Credit Note, Settlement, or Journal goes through the same maker-checker cycle.
See its status
- Finance → Journal tab.
- Your newly-posted voucher is at the top of the list.
- Status chip reads
Pending(amber). The memo includes the narration you typed. - No ledger balance has moved yet. The Trial Balance for today still shows the pre-voucher numbers.
If you need the voucher approved urgently
Don't ask a colleague on WhatsApp and hope. Go find them. The Approvals queue doesn't send push notifications (yet) so a pending voucher can sit there for days if nobody's looking.
If you need to recall
You can't "recall" a pending voucher. Your options:
- Wait for rejection — ask the approver to reject it, then post a fresh one.
- Wait for approval and then reverse — if it has to post today, let it be approved, then use the Reverse flow.
As checker — working the approvals queue
Open the queue
- Finance → Approvals tab. The tab header shows a number badge with the current pending count.
- Scroll past the Bookings Awaiting Finance Approval and Cancellation Approvals sections (those are separate queues — see §4 and §5 below).
- The bottom section is Journal Approvals.
Review a single journal
Each row carries:
- Voucher —
RV-00124/JV-00087/ similar. - Date — when it was posted.
- Particulars — memo + a compact one-line summary of each debit/credit ledger.
- Type — what kind of posting (booking, booking_gst,
booking_commission, journal, etc.). Auto-posted entries carry a
specific
referenceType. - Debit / Credit totals — should equal.
Approve
- Click View to see the full voucher detail in a dialog — party GSTIN, reference number, all lines with account codes, totals.
- Sanity-check:
- Memo matches reality? (e.g. the booking number, the supplier name, the date).
- Amounts match the source document? (supplier invoice PDF, customer's bank confirmation, cancellation form).
- Accounts look correct? (is revenue being debited when it should be credited?).
- FX rate on the voucher (if any) is plausible?
- Dr total = Cr total exactly?
- Close the dialog.
- Click Approve.
- The row disappears from the queue. The journal's status flips to
approved. Ledger balances move immediately.
Reject
- Click View. Read the voucher.
- You're rejecting because something is wrong — write down what. Rejection is terminal (see the state machine) so you cannot "unreject" later.
- Click Reject.
- The row disappears from the queue. Status flips to
rejected. - Tell the maker what was wrong so they can post a corrected entry.
Rejected is terminal
You cannot re-approve a rejected journal. The maker must post a
brand-new voucher with the corrected data. The rejected row stays
on record in the Journal tab with a rejected status chip, for
audit.
Bulk approve
If there are many pending entries and you are confident they are all correct (e.g. 30 auto-posted booking revenue entries from yesterday):
- At the top of the Journal Approvals section, click Approve All.
- A confirmation dialog lists the count.
- Confirm. The system runs
POST /finance/journals/approve-bulkand approves everything in one request.
What actually happens inside:
- Any journal that you created is silently skipped. The response
carries
skipped: [{ id, reason: 'maker_checker_self_approval' }]. - Any journal that another user approved or rejected in between your
click and the request landing is also skipped, with
reason: 'concurrent_transition'. No error. - The ones actually approved get one audit log row each —
finance.voucher.approvewithbulk: truetagged.
Bulk approval is a lot of reversal work if you get it wrong
Approving 50 journals individually gives you 50 chances to catch a typo. Approving 50 at once gives you one. If any one entry is wrong, you'll have to reverse it individually and re-post. Use bulk approval only when you've actually reviewed the batch.
What the system does under the hood
Single approve
- Hits
POST /finance/journals/:id/approve(src/lib/api.ts:22021). - Checks you hold
approvals.approve. - Fetches the entry's current status and
createdBy. - If status is already
approved→ returns{ ok: true, alreadyApplied: true }(idempotent, safe to retry). - If status is
rejected→ throws 409Cannot approve a rejected journal. - Maker-checker guard — if
createdBy === your user idand you don't holdfinance.journals.approve_own, throws 403. - Conditional UPDATE —
UPDATE JournalEntry SET status='approved' WHERE id=? AND status='pending' RETURNING id. This guarantees two simultaneous approvers cannot both win; whoever updates the row first has it, the other gets an idempotent "Already approved" response. - Writes an audit log row
finance.voucher.approvewithpreviousStatus.
Single reject
Mirror of the above with status='rejected' and audit action
finance.voucher.reject (src/lib/api.ts:22071). Same maker-checker
guard: you cannot reject your own.
Bulk approve
POST /finance/journals/approve-bulk (src/lib/api.ts:21670). Filters
self-authored ids into the skipped array, does one conditional UPDATE
against the rest, returns four buckets in the response:
approvedIds— actually transitioned.skippedIdswith reasons — either maker-checker or concurrent loser.skipped— the detailed reason breakdown.
One audit row per approved id.
Other approval queues in the same tab
Three queues share the Approvals tab. They look similar but do different things.
1. Bookings awaiting finance approval
Bookings where Ops has cleared the operational side (passports, itinerary, hotel assignments) but Finance still needs to sign off on the money math — package price, payment policy, GST. Approving flips the booking to financially cleared and unlocks ticket issuance.
Permission: finance.bookings.approve_finance (checker) /
finance.bookings.reject_finance (rejecter).
Actions: Approve, Reject, or Send Back (returns to Ops with a note).
2. Cancellation approvals
Two kinds mix into one table:
- Airline block cancellations — you cancelled seats on an airline quota block and the refund is pending. Once the airline actually pays the refund, you approve here and the money lands on the books.
- B2B buyer cancellations — a B2B buyer has cancelled seats and you need to confirm the credit on their ledger.
Permissions: finance.cancellations.approve_airline /
finance.cancellations.approve_b2b.
3. Journal approvals
Covered above. This is the general-purpose journal queue.
Audit trail
Every approve / reject / bulk-approve / reverse writes one audit log row (except losing-race callers in bulk — see journals.md §7). The audit row carries:
- Action — e.g.
finance.voucher.approve. - Entity type —
journal_entry. - Entity id — the journal's id.
- Actor — your user id (resolvable to a name).
- Timestamp — ISO, UTC.
- Previous status — for approves / rejects.
Access at Admin → Audit Log. Filter by entity type journal_entry to
see only finance approvals.
Permissions reference
From docs/PERMISSIONS.md:
| Permission | Who holds it | What it lets you do |
|---|---|---|
approvals.approve |
FINANCE_MANAGER, ACCOUNTANT, super-admins | Approve OR reject any pending journal entry you didn't create. |
finance.journals.approve |
Same as above | Aliased to the same queue. |
finance.journals.reject |
Same as above | Reject a pending journal. |
finance.journals.bulk_approve |
FINANCE_MANAGER and above | Bulk approve. |
finance.journals.approve_own |
CEO, GM, IT_ADMIN only | Override the maker-checker self-approval block. Break-glass only. |
finance.journals.reverse |
ACCOUNTANT and above | Post a reversal voucher against an approved entry you didn't create. |
finance.journals.reverse_own |
CEO, GM, IT_ADMIN only | Override the maker-checker self-reverse block. Break-glass only. |
If you're trying to approve your own entry
Don't ask IT to give you finance.journals.approve_own. Ask a
colleague to click the button. The override exists for fire-drills
where no other approver is reachable (e.g. a holiday Sunday), not
as a daily workaround.