Status: Implemented. Procurement, sales, payments, POS, fixed assets and expense claims post through this kernel. Stock valuation records stay separate from journals and reconcile through the posting services.
The finance posting kernel turns Merconiq’s chart-of-account configuration into an operational general ledger. It commits immutable, balanced journals exactly once per business action, gated by company fiscal periods, and answers statement and trial-balance reads from the posted ledger.
| Component | Location | Responsibility |
|---|---|---|
JournalEntry, JournalLine |
Merconiq.Core/Entities |
Posted, append-only general-ledger rows. The DbContext rejects updates and deletes; corrections are new journals, never edits. |
FiscalYear, FiscalPeriod |
Merconiq.Core/Entities |
Company-owned posting-date gates. Closing is audited; reopening requires a recorded reason and a separate capability. Period 13 (IsAdjustment) is the year-end adjustment period. |
FiscalPeriodAccountLock |
Merconiq.Core/Entities |
Per-period posting lock on one chart account. A locked account rejects every journal that touches it inside that period, reversals included. |
AccountPostingRole, CompanyAccountMapping |
Merconiq.Core/Entities |
Named control-account roles mapped to company chart accounts by configuration. Roles: Inventory, GoodsReceivedNotInvoiced, AccountsPayable, AccountsReceivable, Bank, CashOnHand, CostOfGoodsSold, SalesRevenue, TaxPayable, Rounding, OpeningEquity, StockInTransit, CashOverShort, FixedAssets, AccumulatedDepreciation, DepreciationExpense, GainOnDisposal, LossOnDisposal, EmployeeExpensesPayable. |
JournalPostingRules |
Merconiq.Core/Services |
Pure invariants: 2–500 lines, each line exactly one side positive, non-negative amounts already at the company currency scale, exact debit/credit equality, non-zero total, bounded source fields, and the deterministic request hash. |
JournalPostingService |
Merconiq.Infrastructure/Services |
The posting transaction: validation → replay/conflict check → period gate (regular or adjustment) → account lock and account checks → journal number → commit. Also source-linked reversals and the ledger-backed trial balance. |
FiscalCalendarService |
Merconiq.Infrastructure/Services |
Fiscal-year creation with monthly periods, close/reopen, the year-end adjustment period, and the open-period checks used inside the posting transaction. |
FiscalAccountLockService |
Merconiq.Infrastructure/Services |
Applies and releases per-period account locks under the finance-period lock. |
AccountPostingPolicyService |
Merconiq.Infrastructure/Services |
Resolves a role to an active, posting-capable account of the calling company. Posting adapters must use this instead of hard-coded or user-supplied control accounts. |
StatementRollupCalculator |
Merconiq.Core/Services |
Pure hierarchy rollup for group accounts in the P&L and balance sheet. |
Company.CurrencyScale, default 2). Amounts must arrive pre-rounded; the kernel
never rounds implicitly.(TenantId, CompanyId, SourceDocumentType, SourceDocumentId,
SourceAction, SourceVersion) is a database-unique key. A retry with the same payload replays the
committed result (Replayed = true); a retry with a different payload is rejected with a conflict
(the committed journal’s RequestHash distinguishes the two). A request targeted at an adjustment
period appends that target to its hash, so regular journals keep their committed hashes unchanged.finance-period:{companyId}) as period close/reopen, so a close cannot race a posting.BaseCurrency.ReverseAsync posts a new journal with swapped debit/credit lines
that references the original (ReversalOfJournalEntryId) and carries a mandatory reason. A
reversal inherits the original’s adjustment-period target. Reversals cannot be reversed, and
fixed-asset journals cannot be reversed generically (see below).All routes are company-capability gated and versioned (api/v1):
| Endpoint | Capability | Behavior |
|---|---|---|
GET /companies/{companyId}/fiscal-years |
View |
Fiscal years with ordered periods, including IsAdjustment. |
POST /companies/{companyId}/fiscal-years |
Administer |
Create a fiscal year and monthly periods; ranges may not overlap. |
POST /companies/{companyId}/fiscal-years/{fiscalYearId}/adjustment-period |
Administer |
Create period 13 for the year; one per year, dated on the year end. |
POST /companies/{companyId}/fiscal-periods/{periodId}/close |
Approve |
Close an open period. |
POST /companies/{companyId}/fiscal-periods/{periodId}/reopen |
Administer |
Reopen with a mandatory reason (audited). |
GET /companies/{companyId}/fiscal-periods/{periodId}/account-locks |
View |
Account lock rows for one period. |
POST /companies/{companyId}/fiscal-periods/{periodId}/account-locks |
Approve |
Lock one posting account for the period with a reason. |
POST /companies/{companyId}/fiscal-periods/{periodId}/account-locks/{chartOfAccountId}/release |
Administer |
Release a lock with a reason (audited). |
GET /companies/{companyId}/journals |
View |
Paged journal headers, optional posting-date window. |
GET /companies/{companyId}/journals/{id} |
View |
One journal with its lines. |
POST /companies/{companyId}/journals |
Post |
Commit one journal for a named source action; optional fiscalPeriodId targets the adjustment period. Replays return Replayed: true. |
POST /companies/{companyId}/journals/{id}/reversals |
Reverse |
Post one source-linked reversal. |
GET /companies/{companyId}/journals/trial-balance |
View |
Ledger-backed trial balance; debits always equal credits. |
GET /companies/{companyId}/statements/profit-and-loss |
View |
Income statement with group rollups. |
GET /companies/{companyId}/statements/balance-sheet |
View |
Balance sheet with group rollups; must balance. |
GET/PUT /companies/{companyId}/account-posting-mappings |
View / Administer |
Configure role-to-account mappings. |
Posting validation failures return 400 with the kernel’s message; conflicting retries return
409.
Journal numbers come from the existing tenant-scoped document sequences with document type
JournalEntry, prefix JV, and period = posting year (for example JV2026-000001).
| Document | Journal effect | Service |
|---|---|---|
| Goods receipt (approved PO) | Dr Inventory / Cr GRNI, exact moving-average value booked to stock | GoodsReceiptService |
| Purchase invoice (matches receipt lines once) | Dr GRNI / Cr AP — GRNI clears to zero, payable recognized | PurchaseInvoiceService |
| Supplier payment (fully allocated) | Dr AP / Cr Bank or Cash | PaymentService |
| Customer receipt (fully allocated) | Dr Bank or Cash / Cr AR | PaymentService |
| Delivery (approved sales order) | Dr COGS / Cr Inventory at the exact issued moving-average value | SalesService |
| Sales invoice (matches delivery lines once) | Dr AR / Cr Sales revenue; blocked over a customer’s credit limit unless a reviewer overrides it with a reason | SalesService |
| Supplier and sales credit notes | Dr AP / Cr Inventory; Dr Sales / Cr AR plus COGS reversal | CreditNoteService |
| POS checkout (open shift, mixed tenders) | Dr Cash/Bank per tender (card and bank transfer post to Bank) / Cr Sales revenue, plus exact COGS | PosService |
| POS refund (bounded by sold balance) | Dr Sales / Cr Cash, plus Dr Inventory / Cr COGS for restocked value | PosService |
| POS shift close variance | Dr or Cr Cash over/short against Cash on hand, once. A variance above the register’s threshold is held and posts only when a reviewer approves it | PosService, TillApprovalService |
| Fixed asset capitalization | Dr Fixed assets at cost / Cr the chosen payment or payable account | FixedAssetService |
| Fixed asset monthly depreciation (per whole month, full-month convention) | Dr Depreciation expense / Cr Accumulated depreciation, keyed by asset and month so reruns replay | FixedAssetService |
| Fixed asset disposal | Dr proceeds, Dr accumulated depreciation, Dr loss / Cr cost, Cr gain; requires depreciation through the disposal date | FixedAssetService |
| Expense claim approval | Dr each expense account / Cr Employee expenses payable | ExpenseClaimService |
| Expense claim payment | Dr Employee expenses payable / Cr the chosen bank account | ExpenseClaimService |
Every document is immutable once posted, idempotent per Idempotency-Key/request key or source action,
gated by open fiscal periods and company base currency, and reserves account assignments through the
posting policy. Receipt, delivery, invoice and payment rows are append-only at the persistence layer.
Purchase-order approval, sales-order creation, CRM activity, and bank or card reconciliation never post.
These read posted ledger lines or record reviewer decisions, and never write journals:
Whole-unit base stock with integral purchase factors; full-line invoice matching (partial billing = split the receipt/delivery); payments fully allocated (advance-to-invoice allocation is a future increment). Fixed-asset depreciation is straight-line by whole months only; impairment, revaluation and custody transfers are not implemented. Expense claims carry the claimant’s name, because no employee master exists.
PostgreSQL integration tests (opt-in via RUN_POSTGRES_TESTS=true) cover the kernel
(FinancePostingPostgreSqlTests), goods receipts (GoodsReceiptPostgreSqlTests), invoice/payment
(PurchaseInvoicePaymentPostgreSqlTests), order-to-cash (SalesOrderToCashPostgreSqlTests), period 13
(AdjustmentPeriodPostgreSqlTests), account locks (FiscalAccountLocksPostgreSqlTests), statement
rollups (FinanceStatementRollupsPostgreSqlTests), bank reconciliation (BankReconciliationPostgreSqlTests),
fixed assets (FixedAssetPostgreSqlTests), expense claims (ExpenseClaimPostgreSqlTests), CRM
(CrmPipelinePostgreSqlTests), and the POS receipt, card-settlement and till-approval cases in
PosRetailPostgreSqlTests. Pure invariants are covered in tests/Merconiq.Tests/Core/Services/, including
JournalPostingRulesTests, JournalRequestHashCompatibilityTests, StatementRollupCalculatorTests,
FixedAssetDepreciationScheduleTests, BankReconciliationRulesTests, CardSettlementRulesTests,
CustomerCreditRulesTests, PosReceiptFormatterTests and ExpenseClaimRulesTests.
The kernel suite covers balanced persistence with snapshots, unbalanced rejection with nothing persisted,
missing/closed-period rejection and authorized reopen, group/foreign-account rejection, idempotent replay,
conflicting-payload conflict, concurrent duplicate action producing exactly one journal, single source-linked
reversal with re-reversal blocked, independently calculated trial-balance fixture, append-only enforcement,
fault injection after the journal insert rolling back all effects, and tenant isolation.