Skip to content

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

  1. Finance → Journal tab.
  2. Your newly-posted voucher is at the top of the list.
  3. Status chip reads Pending (amber). The memo includes the narration you typed.
  4. 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

  1. Finance → Approvals tab. The tab header shows a number badge with the current pending count.
  2. Scroll past the Bookings Awaiting Finance Approval and Cancellation Approvals sections (those are separate queues — see §4 and §5 below).
  3. The bottom section is Journal Approvals.

Review a single journal

Each row carries:

  • VoucherRV-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

  1. Click View to see the full voucher detail in a dialog — party GSTIN, reference number, all lines with account codes, totals.
  2. 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?
  3. Close the dialog.
  4. Click Approve.
  5. The row disappears from the queue. The journal's status flips to approved. Ledger balances move immediately.

Reject

  1. Click View. Read the voucher.
  2. You're rejecting because something is wrong — write down what. Rejection is terminal (see the state machine) so you cannot "unreject" later.
  3. Click Reject.
  4. The row disappears from the queue. Status flips to rejected.
  5. 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):

  1. At the top of the Journal Approvals section, click Approve All.
  2. A confirmation dialog lists the count.
  3. Confirm. The system runs POST /finance/journals/approve-bulk and 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.approve with bulk: true tagged.

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

  1. Hits POST /finance/journals/:id/approve (src/lib/api.ts:22021).
  2. Checks you hold approvals.approve.
  3. Fetches the entry's current status and createdBy.
  4. If status is already approved → returns { ok: true, alreadyApplied: true } (idempotent, safe to retry).
  5. If status is rejected → throws 409 Cannot approve a rejected journal.
  6. Maker-checker guard — if createdBy === your user id and you don't hold finance.journals.approve_own, throws 403.
  7. Conditional UPDATEUPDATE 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.
  8. Writes an audit log row finance.voucher.approve with previousStatus.

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.
  • skippedIds with 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 typejournal_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.