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 currencyledger_accountjournal_entry,journal_line: immutable, enforced by a triggercurrency_rate
Default chart of accounts
Defined in ledger.accounts.ts:
| Type | Accounts |
|---|---|
| Assets | 1010 Cash · 1020 Bank · 1030 Card in transit · 1040 Receivable |
| Liabilities | 2010 Tax owed · 2020 Payable |
| Equity | 3010 Owner's equity |
| Revenue | 4010 Sales · 4020 Shipping income · 4090 Discounts given |
| Expenses | 7010 FX difference · 7020 COGS · 7030 Rent · 7040 Salaries · 7050 Utilities · 7060 Shipping cost · 7070 Fees · 7090 Other |
What posts
| Event | Posting | When |
|---|---|---|
| Order created | Sale, tax, discounts | In the order transaction (ledger.posting.ts) |
| Payment (including partial) | Cash / card-in-transit vs receivable | After commit, via accountingClient, or via the accounting.order.paid event when a broker exists |
| Drawer pay-out | Expense, under the chosen category | With the cash movement |
| Manual expense | Expense | record-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-entry | Expenses |
/merchant/shop/v1/set-shop-currency-rate, get-shop-currency-rates, get-trial-balance, get-journal-entries | Rates 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/andsrc/sync-rates.ts, plusnpm run sync-rates.ledgerService.syncSharedRatespullsopen.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_URLandFX_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
| Route | Guard |
|---|---|
/merchant/reports/v1/get-reports, get-report-meta, get-report | app 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.
| Group | Reports |
|---|---|
| Books | Expenses, Journals, Trial Balance, Account Statement, Receipts |
| Sales | Sales, Item Sales, Profit, Copy Type Sales |
| Inventory | Adjusts, Stock, Warehouse, Products List, Inventory By Date, Product Activity, Stock Alert |
| Customers | Customers, Customers Credit |
pending.tsis 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.financialget in, and the server filters which reports they see.
Known gaps
- Refunds don't post journal entries.
refund.service.tswritesorder_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)
| Route | Guard |
|---|---|
/merchant/payment/v1/get-plans | authenticated |
get-invoices, get-subscription-details, create-subscription | oScoped |
- 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.mjsseeds placeholders for dev). Nothing is gated by plan. - Dashboard ADR 0002: there's no web signup; paid plans are bought in the mobile app.