# Source-to-ledger audit matrix Scope: verified against the current Go services and journal schema. A blank journal hook is an explicit gap, not an assumed accounting event. | Source | Recognition trigger | Journal behavior | Idempotency | Correction | Verified evidence | |---|---|---|---|---|---| | Goods receipt | `purchasing.Service.CreateGR`, after committed GR | `general` draft from PO received quantities and tax; debit purchase expense/input tax, credit GR/IR | `goods_receipt` + GR ID; partial unique DB index prevents duplicate | Draft can be manually posted; no void/reversal of source receipt was verified | `internal/purchasing/service.go`, `cmd/server/main.go`, `cmd/server/accrual_lines_test.go` | | Supplier debt payment | `purchasing.Service.PayDebt`, approved PO and payment amount | `cash_disbursement` posted; debit AP `210102`, credit voucher/default cash COA | No source ID or payment row; retry behavior remains unproven | PO payment and journal use separate transactions; PO commit failure can leave posted journal | `internal/purchasing/service.go`, `cmd/server/main.go` | | Payroll | Payroll payment workflow | Posted journal; debit salary expense `5000.02`, credit configured cash account | `payroll` + payroll ID; unique DB index | Reversal supported at journal layer; payroll correction linkage not verified | `cmd/server/main.go` | | Stock opname | Inventory opname finalization with non-zero diff | Posted journal; shortage debits expense `5000.04`/credits inventory `110401`; surplus reverses with credit other income `7100.99` | `stock_opname` set but no source ID propagated | Journal reversal supported; retry identity missing | `cmd/server/main.go`, inventory accounting hook | | Advance receipt | `accounting.Service.ReceiveAdvance` transaction | Posted cash/deposit or advance-asset/cash journal plus advance and txn row atomically | Advance number unique; journal has `source_type=advance` but no source ID | Refund/allocation creates additional journals | `internal/accounting/service.go` | | Advance allocation | Locked advance mutation | Posted journal; sales deposit→AR, or AP→advance asset | `advance_allocation` + advance ID; repeats are legitimate, so not unique on source pair | New journal and advance transaction are atomic; journal reversal exists | `internal/accounting/service.go` | | Advance refund | Locked advance mutation | Posted journal; debit deposit/advance asset, credit cash | `advance_refund` + advance ID; repeated partial refunds valid | New journal and advance transaction are atomic; journal reversal exists | `internal/accounting/service.go` | | POS sale/payment | `sales.Service.ProcessPOS` | Same transaction creates confirmed SO, deducts stock, invoice and paid payment for cash/transfer/giro/cheque/card; QRIS defers payment until provider settlement | Order/invoice/payment IDs stable after creation; no GL posting | Cash/cash-drawer operational handling exists; journal/reversal absent | `internal/sales/service.go`, POS tests | | Invoice payment | `finance.Service.PayInvoice` / partial payments; gateway settlement in sales | Payment row and invoice/SO balances update transactionally; no GL debit cash/credit AR journal | Payment ID allocated per successful commit; no journal posting | Payment review is an audit note; rejection does not reverse payment | `internal/finance/service.go`, `internal/sales/service.go` | | Sales return/refund | `sales.Service.CreateReturn` | Immutable return + line rows, pro-rata refund, optional stock restock and cash drawer outflow in one transaction; no GL journal | Return ID/number stable; no GL posting key | Refund/tax allocation and damaged/quarantine policy still need business confirmation | `internal/sales/service.go`, return tests | | Inventory movement/valuation | stock mutations and movement ledger | Movement records exact quantities; reported valuation is quantity × current `products.purchase_price`; no historic unit-cost layer or per-movement GL posting | Movement/source reference varies; no universal journal key | Later product-cost changes affect displayed historical valuation; no cost replay/revaluation model | `internal/inventory`, `internal/reports`, schema | ## Database controls - `journal_entries_period_guard` rejects mutations dated in locked periods. Its `FOR SHARE` period-row lock serializes posting versus close; close locks the same row `FOR UPDATE`. - `journal_entries_single_source_uq` prevents duplicate goods-receipt and payroll journals. - The unique index intentionally excludes sources that can have multiple postings or lack a stable source ID. - `000127` audits duplicate sources before index creation and aborts rather than rewriting history. - Period close now re-runs draft-journal and balance checks inside the close transaction and reads the canonical `journal_entries`/`journal_lines` ledger. A blocking failure leaves the period open. ## Implemented reporting / close controls - Period summary calculates cashflow from `cashflows`, sales from `sales_orders`, and posted journal totals from GL header/lines. - It detects header-versus-lines and debit-versus-credit imbalance for each posted/reversed journal. - It reports draft journals as unposted and blocks final close when drafts or unbalanced journals exist. - Migration `000128` audits and constrains reversal links, validates posting totals, makes posted lines immutable and permits only exact linked reversals. - Posted-to-voided service operation now returns HTTP 409; the database mutation guard rejects status-only posted journal changes because they remove active GL value without an offset. - Reversal is currently dated today. A reversal whose original is in a locked period is still rejected by the period guard; cross-period correction remains an explicit owner policy decision. - `IsBalanced` for a period with no posted journal rows is true (empty sum); this is not an assertion that operational modules are reconciled to GL. Reversed entries remain as an original `reversed` header plus linked posted mirror. Close totals include both line sets; they net only for an exact mirror. 000128 validates the mirror relation during the transition and historical preflight, but reports still need explicit pair-level diagnostics. ## Open implementation gaps POS sales, invoice payments, sales returns/refunds, supplier payments, and inventory movements do not yet post to GL. A complete POS posting needs a selected inventory cost method, tax configuration, discount recognition policy, and cash/bank/clearing mapping. Inventory reconciliation and subledger-to-GL reconciliation remain unavailable. Current purchase-price valuation is an estimate at current cost, not FIFO or moving weighted average.