Bilko Modul B-1 — GL Foundation Build (2026-06-13)

TL;DR

Bilko Modul B-1 GL Foundation je KOMPLETAN. Backend (commit 6efb16f9) + frontend (commiti 69c87cf3 + b1f6401a) izgradeni. GL subsystem je iza BILKO_ACCOUNTING_GL + GL_AUTO_POST flagova — obje default OFF globalno. Modul A (fakturisanje) potpuno netaknut. Proveo UAT PASS 20/20 (MC #103549), Vlado domain acceptance PRIHVACENO 19/19, Petter (lead) sign-off: merge GO. PR #369 ceka CEO merge odluku — dormantan, flag-gated, nema regresije.

B-1 ZAVRSEN (stanje 2026-06-13)

Bilko Modul B-1 — GL Foundation je KOMPLETAN. Backend + frontend izgradjeni, Proveo UAT PASS (20/20), Vlado domain acceptance PRIHVACENO (19/19), Petter (lead) sign-off: merge GO. PR #369 je spreman za CEO merge odluku — flag-gated (BILKO_ACCOUNTING_GL default OFF), Module A potpuno netaknut, nema regresije.

Sta modul radi (iza BILKO_ACCOUNTING_GL flag, default OFF)

Backend (commit 6efb16f9)

Frontend (commiti 69c87cf3 + b1f6401a)

Tok racunovodje

  1. Otvori Kontni plan — pregled konta (logicalRole / accountCode / jurisdikcija, HR RRiF)
  2. Faktura/placanje kreira DRAFT temeljnicu automatski (kada je GL_AUTO_POST ON) ili rucni unos
  3. Potvrdi DRAFT → POSTED (trajna, nepromjenjiva knjizba)
  4. POSTED entry se pojavljuje u Glavnoj knjizi (temeljnice pregled)
  5. Bruto bilanca pokazuje ΣD=ΣC — system-level invariant verificiran trigerom i API-jem
  6. Storno = nova reversing POSTED entry (append-only, original nikad ne brises; cl. 11.3 ZoR)

Validacija

SlojRezultatDetalji
Proveo UAT MC #103549 PASS 20/20 Puni lifecycle (DRAFT→POSTED→storno) + gate testovi (flag OFF → 404) + permission negatives (viewer → 403, unbalanced → 400). Bruto bilanca ΣD=ΣC rucno verificirano. Mesh: mesh-thr-proveo-103535-20260613T044650Z. Layer: integration + fe-build (browser odgodjeno — demo SSL nedostupan; spec commitan).
Vlado domain acceptance PRIHVACENO 19/19 Ovlasteni racunovoda. Posting-rule templates (6 dogadjaja, JSONB kontrakt), kontni plan HR, PDV razlaganje po stopi, reversing-entry semantika, append-only nepromjenjivost.
Petter (lead) sign-off B-1 COMPLETE, merge GO Commit 6efb16f9 backend + 69c87cf3/b1f6401a frontend. 78 testova zeleno. Scope eksplicitno zatvoreno per B-1 DoD.

Iskrene ogranicenja / odgodeno na B-2

Production-activation gates (prije pravog racunovodje)

  1. Operator ukljuci BILKO_ACCOUNTING_GL = true za imenovanog pilot org (GL_AUTO_POST ostaje OFF do gate 2)
  2. [VERIFY-NN] OBAVEZNO: pravna provjera na narodne-novine.nn.hr / porezna-uprava.gov.hr za 10 VERIFY-NN stavki (PDV stope/clanci) — nije opcionalno
  3. Imenovan racunovoda design-partner (board U2) live walkthrough sign-off

Status PR #369

Merge preporuka: GO — dormantan, flag-gated, Module A netaknut, nema regresije. Ceka CEO merge odluku.

Architecture

Database — 5 tables, Flyway migration V84

TablePurposeKey constraints
bilko_flags Platform kill-switch flags (no Unleash dependency). org_id IS NULL = global default; non-null = per-org override. Surrogate UUID PK + expression unique index uq_bilko_flags(flag_name, COALESCE(org_id, '00...00')) — required because Postgres 16 does not allow COALESCE in inline PRIMARY KEY/UNIQUE syntax.
journal_entries GL header (one row per business event). Append-only per Article 11.3 ZoR. UNIQUE(org_id, source_type, source_document_id) — idempotency invariant. Status enum: DRAFT / POSTED / REVERSED. RLS via V75 NULLIF fail-closed canonical pattern.
journal_postings GL legs (debit/credit rows). Multiple rows per entry. amount > 0, side IN ('DEBIT','CREDIT'). Immutability trigger fires if parent entry is POSTED/REVERSED.
posting_rules Config-driven Vlado JSONB posting templates. Engine reads rules from here; never hardcodes account numbers. Seeded with R-1 (taxable domestic) and R-3a (EU_41 exempt) on migration. Additional rules added without code changes.
account_mapping Org-configurable logical-role-to-account-code mapping. Global HR defaults seeded (9 entries). Orgs can override per jurisdiction. Expression unique index uq_am_org_role(COALESCE(org_id,...), jurisdiction, logical_role) — same PG16 fix pattern as bilko_flags.

Postgres Triggers — 3 enforcement points

TriggerFunctionWhat it enforcesError code
trg_je_balance_check fn_je_balance_check Sum(DEBIT) == Sum(CREDIT) at the moment status transitions to POSTED. DRAFT entries are allowed to be unbalanced during assembly. P0002 (balance violation) / P0003 (zero postings)
trg_je_immutability fn_je_immutability BEFORE UPDATE OR DELETE on journal_entries where OLD.status IN ('POSTED','REVERSED'). DRAFT rows remain mutable. P0001
trg_jp_immutability fn_jp_immutability BEFORE UPDATE OR DELETE on journal_postings — checks parent entry status; fires if parent is POSTED/REVERSED. P0004

PostingRuleEngine (Kotlin)

Located at apps/api/src/main/kotlin/no/alai/bilko/gl/PostingRuleEngine.kt. The engine is config-driven: it reads JSONB posting templates from the posting_rules table and resolves amounts from the incoming GlDocumentData struct. It never computes tax; all net/vat/gross values must originate from the Module A invoice document.

Key behaviours:

GlBridge — A to B integration point

Located at apps/api/src/main/kotlin/no/alai/bilko/gl/GlBridge.kt. Called from InvoiceService.sendInvoice() inside the existing orgTransaction after the invoice transitions to SENT status.

Posting Rules — Vlado's 6 Events

Domain contract authored by Vlado Brkanić (certified accountant, design-partner). Scope: outgoing/sales documents only, Croatian jurisdiction (HR). All amounts in EUR. Rates: 25% / 13% / 5% [VERIFY-NN čl.38].

EventDescriptionDebit legsCredit legs
1 — SALES_INVOICE_ISSUED (standard) Taxable domestic invoice, VAT 25%, buyer Croatia. accounting_date = delivery/issue date. 1200 Kupci HR (gross) 7600 Prihodi HR (net), 2400 PDV obveza (vat)
2 — Multi-rate (13%+5%) Same as Event 1 but VAT split across rates. Engine emits one CREDIT posting per rate, each carrying vat_rate. 1200 (gross) 7600 (net), 2400 @13% (vat slice), 2400 @5% (vat slice)
3a — EU exempt (čl.41) B2B EU supply, zero VAT, report_target=ZP. Precondition: valid VIES VAT-ID. 1201 Kupci EU (gross) 7610 Prihodi EU (net) — no 2400
3b — Export exempt (čl.45) Third-country export, zero VAT, not reported in ZP. Proof: customs export declaration. 1201 (gross) 7610 (net) — no 2400
3c — Exempt without deduction right (čl.39/40) Zero VAT. vat_exemption_code preserved for B-2 pro-rata coefficient calculation. 1200/1201 (gross) 7600/7610 (net) — no 2400
4 — PAYMENT_RECEIVED Cash inflow. Does not touch revenue or VAT accounts. Partial payment leaves receivable open. 1000 Žiro-račun (payment.amount); 1020 Blagajna for cash 1200 Kupci (closes receivable, closes_document_id set)
5 — CREDIT_NOTE Reversal of Event 1. Reversing entry pattern — all legs inverted. source_type=CREDIT_NOTE, reverses_document_id set. No deletion of original. 7600 Prihodi (net), 2400 PDV (vat) 1200 Kupci (gross)
6 — Advance (3 steps) 6a Advance received: D 1000 / C 2310 (net) + C 2410 (VAT on advance). VAT liability arises on receipt [VERIFY-NN čl.30/5].
6b Final invoice on delivery: D 1200 / C 7600 + C 2400. Revenue recognised once.
6c Netting: D 2310 + D 2410 / C 1200. Advance VAT reversed. Result: 2310/2410/1200 net to zero; no VAT doubling.
See 6a/6b/6c See 6a/6b/6c

JSONB Rule Format — R-1 and R-3a examples

R-1: Taxable domestic invoice (vat_exemption_code is null):

{
  "event_type": "SALES_INVOICE_ISSUED",
  "jurisdiction": "HR",
  "match": { "vat_exemption_code": null },
  "postings": [
    { "account": "1200", "side": "DEBIT",  "amount_source": "invoice.gross", "analytic": "partner:{invoice.partner_oib}" },
    { "account": "7600", "side": "CREDIT", "amount_source": "invoice.net" },
    { "account": "2400", "side": "CREDIT", "amount_source": "invoice.vat_by_rate", "split_by": "vat_rate", "carry": ["vat_rate"] }
  ],
  "balance_assert": "sum(DEBIT) == sum(CREDIT)",
  "status_on_create": "DRAFT",
  "requires_accountant_confirmation": true
}

R-3a: EU-exempt supply (vat_exemption_code = "EU_41"):

{
  "event_type": "SALES_INVOICE_ISSUED",
  "jurisdiction": "HR",
  "match": { "vat_exemption_code": "EU_41" },
  "report_target": "ZP",
  "postings": [
    { "account": "1201", "side": "DEBIT",  "amount_source": "invoice.gross", "analytic": "partner:{invoice.partner_vat_id}" },
    { "account": "7610", "side": "CREDIT", "amount_source": "invoice.net" }
  ],
  "balance_assert": "sum(DEBIT) == sum(CREDIT)",
  "status_on_create": "DRAFT",
  "requires_accountant_confirmation": true,
  "preconditions": ["partner_vat_id_valid_vies"]
}

How to Enable the Module (Operator Runbook)

The two flags

FlagDefaultEffect when ON
BILKO_ACCOUNTING_GL false (global) Enables the GL subsystem for the org. GlBridge checks this first. Without it, the engine is never called. Can be set per-org via bilko_flags (org_id non-null).
GL_AUTO_POST false (global) When both flags are ON, GlBridge calls the engine and persists a DRAFT journal entry on every invoice send. DRAFT entries appear in the accountant's queue for review and confirmation. The system never auto-transitions a DRAFT to POSTED.

What happens when both flags are ON

  1. Invoice transitions to SENT in Module A (InvoiceService).
  2. GlBridge.onInvoiceIssued() is called inside the existing orgTransaction.
  3. PostingRuleEngine evaluates the invoice against JSONB rules in posting_rules.
  4. A DRAFT journal entry + postings are persisted. Status = DRAFT, requiresAccountantConfirmation = true.
  5. Accountant reviews the DRAFT in the (future) accounting module UI and transitions to POSTED manually.
  6. If an entry already exists for this invoice (idempotency key), step 4 is a no-op.

This gate must pass before auto-post can go live in production. Vlado's domain contract contains 10 [VERIFY-NN] annotations referencing Croatian tax law articles (ZoPDV, ZoR). The exact Narodne Novine references must be verified against porezna-uprava.gov.hr / narodne-novine.nn.hr before any POSTED auto-entries are generated for real clients. This is a platform-admin review gate — do not skip.

Validation Evidence

Proveo verdict: PASS — Angie Jones, 2026-06-13. Mesh thread: mesh-thr-proveo-103535-20260613T044650Z. Validation report: /tmp/evidence-103535/VALIDATION-REPORT-v2.md.

Two validation rounds were required:

DB-invariant proofs (12 sub-tests on live Postgres 16)

InvariantProbeExpectedSQLStateResult
Balance trigger (reject) POST entry with D=1000 / C=800, transition to POSTED Exception: "balance violation" P0002 PASS
Balance trigger (allow) POST entry with D=1250 / C=1250, transition to POSTED No exception PASS
Idempotency Insert duplicate (org_id, source_type, source_document_id) Unique violation 23505 PASS
Entry immutability (UPDATE) UPDATE POSTED journal_entry Immutability violation P0001 PASS
Entry immutability (DELETE) DELETE POSTED journal_entry Immutability violation P0001 PASS
Posting immutability (UPDATE) UPDATE posting row of POSTED entry Immutability violation P0004 PASS
Posting immutability (DELETE) DELETE posting row of POSTED entry Immutability violation P0004 PASS
DRAFT mutability UPDATE on DRAFT journal_entry No exception, persisted PASS
+4 schema/flag proofs (tables present, indexes present, triggers present, flag seeds confirmed)

Test counts

Flag safety (GlBridgeTest 6/6)

What Is NOT Done Yet (Deferred to next B-1 slice)


Revision #3
Created 2026-06-13 03:20:23 UTC by John
Updated 2026-06-13 10:33:43 UTC by John