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)
- GL engine: tabele
journal_entries,journal_postings,posting_rules,account_mapping,bilko_flags - PG trigeri: balance-invariant (P0001), immutability (P0002), idempotency (P0004) — okidaju uzivo na PG16
- 9 REST endpointa na
/api/v1/accounting/* - Flyway V85 — permissions seed (
accounting:view,accounting:post,accounting:manage) - PAYMENT/CREDIT_NOTE bridge (unit-tested; GL_AUTO_POST zasebni flag, default OFF)
- 68 GL unit + 10 HTTP integration testova — sve zeleno
Frontend (commiti 69c87cf3 + b1f6401a)
- 5 stranica pod
app/(dashboard)/accounting/:- kontni-plan — tabela Konto | Naziv | Uloga, HR account names (RRiF konvencija)
- temeljnice — glavna knjiga, paginirani pregled DRAFT/POSTED/REVERSED sa filterima
- temeljnica/[id] — detalj: postings Duguje/Potrazuje, akcije Potvrdi (DRAFT→POSTED) i Storno
- nova-temeljnica — rucni unos s live balance-check (ΣD=ΣC prikazano uzivo)
- bruto-bilanca — per-account totalDebit/totalCredit/saldo, grand totals, asOf filter; ΣD=ΣC indikator
- Sidebar "Knjigovodstvo" nav sekcija (gating: accountant/admin/owner rola)
- 404-graceful gating — kada je flag OFF, stranice prikazuju "Ovaj modul nije dostupan" (ne crashaju)
- 5 lokalizacijskih lokala
- Playwright spec:
apps/e2e/tests/accounting-b1.spec.ts(commitan, spreman za browser run kada je demo SSL dostupan)
Tok racunovodje
- Otvori Kontni plan — pregled konta (logicalRole / accountCode / jurisdikcija, HR RRiF)
- Faktura/placanje kreira DRAFT temeljnicu automatski (kada je GL_AUTO_POST ON) ili rucni unos
- Potvrdi DRAFT → POSTED (trajna, nepromjenjiva knjizba)
- POSTED entry se pojavljuje u Glavnoj knjizi (temeljnice pregled)
- Bruto bilanca pokazuje ΣD=ΣC — system-level invariant verificiran trigerom i API-jem
- Storno = nova reversing POSTED entry (append-only, original nikad ne brises; cl. 11.3 ZoR)
Validacija
| Sloj | Rezultat | Detalji |
|---|---|---|
| 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
- Live invoice → auto-DRAFT: bridge unit-tested; e2e nije provjereno (GL_AUTO_POST je OFF zasebni flag; aktivira se u B-2 kada aktiviramo pilot org)
- Browser-layer E2E: odgodjeno (demo SSL nedostupan za Playwright live run); spec commitan u
apps/e2e/tests/accounting-b1.spec.ts - Sekvencijalno numerisanje temeljnica: B-2
- Predujam (avans) bridge kompletno: B-2 (6c knjizenje — netiranje avansa, djelomicni avans)
Production-activation gates (prije pravog racunovodje)
- Operator ukljuci
BILKO_ACCOUNTING_GL = trueza imenovanog pilot org (GL_AUTO_POSTostaje OFF do gate 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
- 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.
Veze
- Spec/board page: page 3115 (B-1 board presuda)
- PR #369 —
feature/b1-gl-foundation - MC #103531 (parent) | #103547 (backend Proveo) | #103548 (frontend Vizu) | #103549 (UAT) | #103550 (docs)
Architecture
Database — 5 tables, Flyway migration V84
| Table | Purpose | Key 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
| Trigger | Function | What it enforces | Error 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:
- Gross check — net + vat must equal gross; mismatch returns REJECTED status (no crash).
- Rule lookup — matched by (event_type, jurisdiction, vat_exemption_code). Most-specific match wins (rule with explicit vat_exemption_code scores higher than wildcard null match).
- split_by vat_rate — for multi-rate invoices (Event 2), one CREDIT posting is emitted per distinct VAT rate line, each carrying vat_rate for the B-2 VAT return.
- No rule found — returns ZA_KONTIRANJE status (no crash, Module A unaffected, accountant must manually post).
- All drafts — status=DRAFT, requiresAccountantConfirmation=true always set on output.
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.
- If
BILKO_ACCOUNTING_GL=false: early return, engine is never called (MockK verify exactly=0 confirmed by Proveo). - If
GL_AUTO_POST=false: early return. - Both ON: engine called, result persisted as DRAFT via
GlRepository.persistDraft(). - Idempotent: if a journal entry already exists for the same (org, source_type, source_document_id),
persistDraftreturns null (no duplicate, no exception). - Non-throwing: all GL errors are caught and logged; Module A (invoice flow) is never interrupted.
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].
| Event | Description | Debit legs | Credit 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
| Flag | Default | Effect 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
- Invoice transitions to SENT in Module A (InvoiceService).
- GlBridge.onInvoiceIssued() is called inside the existing orgTransaction.
- PostingRuleEngine evaluates the invoice against JSONB rules in
posting_rules. - A DRAFT journal entry + postings are persisted. Status = DRAFT, requiresAccountantConfirmation = true.
- Accountant reviews the DRAFT in the (future) accounting module UI and transitions to POSTED manually.
- If an entry already exists for this invoice (idempotency key), step 4 is a no-op.
[VERIFY-NN] legal-review gate
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:
- v1 (PARTIAL) — Commit 464c3d14: V84 migration blocked with SQLState 42601. Postgres 16 does not allow COALESCE expressions inside inline PRIMARY KEY / UNIQUE table constraint syntax. Affected 4 locations across bilko_flags and account_mapping.
- v2 (PASS) — Commit 687f1d0b (fix): Replaced inline COALESCE constraints with surrogate UUID PKs + separate expression unique indexes. All 4 locations corrected.
DB-invariant proofs (12 sub-tests on live Postgres 16)
| Invariant | Probe | Expected | SQLState | Result |
|---|---|---|---|---|
| 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
- 16 unit tests: PostingRuleEngineTest (10/10) + GlBridgeTest (6/6) — MockK, no DB.
- 12 DB-invariant tests: GlInvariantDbTest — full Flyway V1-V84 on postgres:16-alpine Testcontainer.
- SelfPostingConstraintTest (2/2) — migration regression guard.
- Pre-existing 4 failures in CountryPlugin XML tests — same on main, unrelated to B-1.
Flag safety (GlBridgeTest 6/6)
- BILKO_ACCOUNTING_GL=false: engine never called (MockK verify exactly=0).
- GL_AUTO_POST=false: engine never called.
- Both ON + DRAFT: engine called x1, persistDraft called x1.
- Both ON + ZA_KONTIRANJE result: no persist, no crash.
- Engine exception: swallowed, Module A unaffected.
What Is NOT Done Yet (Deferred to next B-1 slice)
- Frontend kontni-plan UI — accountant-facing account plan management screen.
- Glavna knjiga / bruto bilanca API endpoints — general ledger summary and trial balance REST endpoints.
- Accountant DRAFT → POSTED screen — UI for accountant to review and confirm draft journal entries.
- Remaining posting rule seeds — Events 3b (EXPORT_45), 3c (EXEMPT_39/40), 4 (PAYMENT_RECEIVED), 5 (CREDIT_NOTE), 6a/6b/6c (advance/advance-settlement) are domain-specified but not yet seeded as active rules in posting_rules table.
- Live end-to-end test — full invoice send with both flags ON, confirming a real DRAFT entry lands in the DB. Recommended for B-2 gate (Proveo open item).
- [VERIFY-NN] legal review — porezna-uprava.gov.hr article verification gate before production auto-posting is enabled.
Cross-links
- Spec and board decision: BookStack page 3115 — Bilko Modul B (Knjigovodstvo) — Spec + Board presuda
- PR #369:
feature/b1-gl-foundation(not yet merged) - MC #103531 (parent build task)
- MC #103535 (Proveo validation task)
- Domain contract:
/tmp/evidence-knjigovodstvo/vlado-posting-rule-templates-B1.md - Validation report v2:
/tmp/evidence-103535/VALIDATION-REPORT-v2.md
No comments to display
No comments to display