Bilko Modul B-1 — GL Foundation Build (2026-06-13)
TL;DR
Bilko Module B-1 GL Foundation is built on branch feature/b1-gl-foundation (commit 687f1d0b, PR #369).
The GL subsystem is gated behind two feature flags — BILKO_ACCOUNTING_GL and GL_AUTO_POST — both seeded OFF globally.
Module A (invoicing) is completely untouched when either flag is off.
Proveo independent validation returned PASS (mesh-thr-proveo-103535, 2026-06-13).
PR #369 is not yet merged or deployed; it awaits operator sign-off and the [VERIFY-NN] legal-review gate before production auto-posting can be enabled.
Progres & Resume (stanje 2026-06-13)
CEO je pauzirao build ovdje. Ova sekcija dokumentira tacno stanje na dan pauze kako bi svaka buducna sesija mogla nastaviti bez gubitka konteksta.
Napredak — ~60% do radnog B-1
| Slice | Status | Detalji |
|---|---|---|
| Strategija (board) | DONE | 4/4 GO glasova. Board presuda na page 3115. |
| Vlado posting-pravila | DONE | 6 dogadjaja, JSONB templates, domain kontrakt potpisan. |
| Slice 1-3 — GL motor (DB + engine + bridge) | DONE + Proveo PASS | Commit 687f1d0b (fix od 464c3d14). 16 unit + 12 DB-invariant testova. Trigeri (P0001/P0002/P0004) okidaju uzivo na PG16. Mesh: mesh-thr-proveo-103535-20260613T044650Z. |
| Slice 4 — Backend kontrolna ploca (9 endpoinata) | DONE + Proveo PASS | Commit 6efb16f9. 68 GL + 10 HTTP integration testova. Bruto bilanca SD=SC rucno provjereno. Verdict: /tmp/evidence-103547/verdict-v2.json. |
| Slice 5 — Frontend UI | PREOSTAJE | Vizu, MC #103548. Kontni plan, glavna knjiga, bruto bilanca, temeljnice DRAFT->POSTED ekran. |
| Slice 6 — Live e2e | PREOSTAJE | Proveo, MC #103549. |
| Slice 7 — Docs | PREOSTAJE | Skillforge, MC #103550. |
| Merge PR #369 | PREOSTAJE | Nakon Slice 6 Proveo PASS gate. |
Branch / Deploy stanje (na pauzi)
- Branch:
feature/b1-gl-foundation@ origin6efb16f9ea0d1a01c88383a75963ad9241294ff0 - PR #369: NEMERGAN. NE na main. Nista deployano u produkciju.
- Flagovi:
BILKO_ACCOUNTING_GL+GL_AUTO_POST— default OFF globalno. Modul A (fakturisanje) potpuno netaknut.
Slice 4 — Backend kontrolna ploca (summary)
9 REST endpoinata na /api/v1/accounting/*, Flyway V85 (permissions):
| Endpoint | Opis |
|---|---|
GET /api/v1/accounting/accounts | Kontni plan (account mapping za org) |
GET/PUT /api/v1/accounting/account-mapping | Konfiguracija logical-role → account-code |
GET /api/v1/accounting/journal | Glavna knjiga — lista journal_entries |
GET /api/v1/accounting/entries/{id} | Detalj jednog temeljnicnog zapisa |
GET /api/v1/accounting/trial-balance | Bruto bilanca (ΣD=ΣC provjereno) |
POST /api/v1/accounting/entries/{id}/confirm | Knjizenje: DRAFT → POSTED (accountant akcija) |
POST /api/v1/accounting/entries/{id}/reverse | Storno — append-only, ne brise original |
POST /api/v1/accounting/entries/manual | Rucni unos temeljnice |
- V85 permissions:
accounting:view,accounting:post,accounting:manage_accounts - Bridge eventi:
PAYMENT_RECEIVED+CREDIT_NOTE_ISSUED— seeded, bridge za B-2 odlozen. - Avans pravila: seeded ali bridge odlozen u B-2.
- Validacija: 68 GL testova + 10 HTTP integration testova. MC #103547.
RESUME-HERE — Sljedeci korak
Slice 5 frontend (Vizu, MC #103548) je tacka nastavka.
- Build: kontni-plan UI, glavna knjiga view, bruto bilanca view, temeljnice DRAFT->POSTED ekran.
- Iza
BILKO_ACCOUNTING_GLflaga (gated nav — pokazi GL navigaciju samo ako flag ON). - Trosi:
GET /api/v1/accounting/{accounts,journal,trial-balance,entries/{id}},POST .../confirm,.../reverse,.../manual. - Pattern za Vizu:
apps/web/app/(dashboard)/reports/*(kir/kpr/vat/profit-loss),apps/web/lib/api-base.ts(hostname routing app.bilko.cloud → app-api.bilko.cloud). - Nakon Slice 5: Slice 6 Proveo live e2e (#103549) → Slice 7 Skillforge docs (#103550) → merge PR #369.
Gotche (nepregovorljivo na nastavku)
- HTTPS-PAT push: SSH push pada za uid-501 ("No user exists for uid 501"). Agent gura iskljucivo preko HTTPS-PAT:
git push origin HEAD:feature/b1-gl-foundation. Verifikacija:git ls-remote origin feature/b1-gl-foundationili GitHub API. - git-author-guard: Hook blokira john@ ancestry false-positive na branch push. CEO override:
/tmp/git-author-override-<hash>— kreirati u zasebnom bash pozivu PRIJE push poziva (<60s TTL, single-use). Preporuka: neka agent gura, ne John/orchestrator. - session-task-lock (#103522): Moze blokirati
mc.js start/update. Koristitimc.js addilitouch /tmp/session-override-approved-103522. - Proveo dirty working-tree: Cisti checkout
git checkout 6efb16f9 -- .prije testa (testira commit sadrzaj, ne local dirty state). - Jedan repo-mutating agent istovremeno: Nema paralelnih agenata koji mutiraju isti repo (parallel-collision lekcija).
- CI scope:
npx turbo run build+gradlew test+gradlew integrationTest. CI NE pokracegradlew build/detekt. 4 pre-existing PluginHR XML faila su OK i na main.
Gateovi prije produkcije (ne preskakati)
- Operator flip flagova za pilot org.
- [VERIFY-NN] porezna-uprava.gov.hr provjera PDV stopa / clanaka ZoPDV prije auto-post live.
- Vlado posting-rules kontrakt:
/tmp/evidence-knjigovodstvo/vlado-posting-rule-templates-B1.md
Cross-linkovi za nastavak
- Spec + board presuda: BookStack page 3115
- PR #369:
feature/b1-gl-foundation(NEMERGAN) - MC #103531 (parent build task)
- MC #103548 (Slice 5 — RESUME-HERE, Vizu frontend)
- MC #103549 (Slice 6 — Proveo live e2e)
- MC #103550 (Slice 7 — Skillforge docs)
- Slice 4 Proveo verdict:
/tmp/evidence-103547/verdict-v2.json - Resume state source:
/tmp/evidence-knjigovodstvo/B1-RESUME-STATE.md
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