API (api-v2)

Ledger, FX & reports

Every shop has its own double-entry ledger, seeded at shop creation. Sales, payments, refunds and pay-outs post to it automatically. Reports read it alongside orders and inventory.


Ledger

Tables:

  • shop_ledger_config: base currency
  • ledger_account
  • journal_entry, journal_line: immutable, enforced by a trigger
  • currency_rate

Default chart of accounts

Defined in ledger.accounts.ts:

TypeAccounts
Assets1010 Cash · 1020 Bank · 1030 Card in transit · 1040 Receivable
Liabilities2010 Tax owed · 2020 Payable
Equity3010 Owner's equity
Revenue4010 Sales · 4020 Shipping income · 4090 Discounts given
Expenses7010 FX difference · 7020 COGS · 7030 Rent · 7040 Salaries · 7050 Utilities · 7060 Shipping cost · 7070 Fees · 7090 Other

What posts

EventPostingWhen
Order createdSale, tax, discountsIn the order transaction (ledger.posting.ts)
Payment (including partial)Cash / card-in-transit vs receivableAfter commit, via accountingClient, or via the accounting.order.paid event when a broker exists
Drawer pay-outExpense, under the chosen categoryWith the cash movement
Manual expenseExpenserecord-expense

Entries are corrected with reverse-journal-entry, never edited.

Posting call sites: ledgerService.postSale in order.service.ts and pos.service.ts, accountingClient.recordOrderPayment in order.service.ts, and ledgerService.postEntry in accounting.service.ts (expenses, including drawer pay-outs).

Routes

All are owner-only:

Route
/merchant/accounting/v1/get-expense-categories, record-expense, get-expenses, get-expense-summary, reverse-journal-entryExpenses
/merchant/shop/v1/set-shop-currency-rate, get-shop-currency-rates, get-trial-balance, get-journal-entriesRates and books

Currencies & FX

  • 14 seeded currencies. USD is the default; others include LBP, EUR and AED.
  • A rate entered on a date stays in force until a newer one replaces it.
  • A shop's own rate overrides the shared rate.
  • Catalogue prices are stored in USD and converted with the effective rate on every read.

Daily FX sync (in progress)

On branch feat/fx-rate-sync (uncommitted at the time of writing):

  • src/shared/fx/ and src/sync-rates.ts, plus npm run sync-rates.
  • ledgerService.syncSharedRates pulls open.er-api.com. That feed was chosen because it quotes LBP at the market rate, not the defunct 1,507.5 peg.
  • It writes shared USD rates daily. It's configured by FX_SYNC_URL and FX_SYNC_TIMEOUT_MS.
  • Scheduled with Heroku Scheduler at 06:00 UTC.

Run it once after deploy

The job is inert until the scheduler entry exists. Run heroku run npm run sync-rates once by hand after the first deploy. Until a rate row exists, a shop trading in anything but USD gets an error where its catalogue should be.

Reports

RouteGuard
/merchant/reports/v1/get-reports, get-report-meta, get-reportapp reports + (reports.view or reports.financial)
  • Reports are defined on the server in src/modules/reports/definitions/*.report.ts, which registers each report's filters, columns and export flag. The dashboard renders whatever the server describes.
  • Reports were delivered on 2026-09-06.
GroupReports
BooksExpenses, Journals, Trial Balance, Account Statement, Receipts
SalesSales, Item Sales, Profit, Copy Type Sales
InventoryAdjusts, Stock, Warehouse, Products List, Inventory By Date, Product Activity, Stock Alert
CustomersCustomers, Customers Credit
  • pending.ts is the mechanism for listing a not-yet-implemented report with its reason. It's empty today.
  • Item Shipments is deliberately absent because there's no purchasing module. Purchase columns are always zero.
  • Customers Credit is derived from orders; no legacy balances were imported.
  • Staff with only reports.financial get in, and the server filters which reports they see.

Known gaps

  • Refunds don't post journal entries. refund.service.ts writes order_transaction (the order's money record) but never calls the ledger, so the books overstate sales by refunded amounts.
  • No COGS posting, so Profit doesn't reconcile with the Trial Balance.
  • The planned write-restricted database role for the ledger was never added, so immutability rests on the trigger alone.

Billing

Tables: plans, plan_prices, subscriptions, transactions, invoice, provider_events (+ feature tables)

RouteGuard
/merchant/payment/v1/get-plansauthenticated
get-invoices, get-subscription-details, create-subscriptionoScoped
  • Apple App Store and Google Play in-app purchase verification. Stripe is seeded as a provider but not implemented.
  • The plan catalogue starts empty (scripts/seed-dev-plans.mjs seeds placeholders for dev). Nothing is gated by plan.
  • Dashboard ADR 0002: there's no web signup; paid plans are bought in the mobile app.
Previous
Customers, staff & shop