Bilko HR — Inbound e-Invoice Reject + Monthly Rejection eIzvještavanje (MC #106149 / #106146)
MC #106149 (reject action, M) + #106146 (monthly eIzvještavanje report, H, RED-ZONE HR tax/compliance). Coupled/parent #106145. Status as of 2026-07-21: built and tested in an isolated worktree, NOT merged or pushed — no PR opened yet. This page documents the built state for handoff / next-session continuation.
What was built
- Reject action on
received_einvoice(inbound/AP e-invoice): transitionsprocessing_statusand records a Bilko-internal rejection reason code, reject timestamp, and eIzvještavanje transmission tracking fields. RejectionReportService: builds the monthly batch of rejected inbound e-invoices Bilko must report to Porezna uprava under čl.52 (rok: 20th of the month, for the prior calendar month — domain-gate CONFIRMED, web-verified via 6 convergent sources, distinct from the separate NN 151/2025 PDV-filing deadline change).
IMPORTANT — N/U/O is Bilko's own taxonomy, NOT Porezna's
The domain gate (agent bilko-porez-fiskalizacija-hr) caught and corrected a first-draft modeling error before this shipped:
- Porezna's actual N/U/O are three different eIzvještavanje REPORT TYPES, not three rejection sub-reasons:
- N = Naplata — issuer reports collection/payment of an issued e-invoice (unrelated to rejection)
- U = Usluge/Isporuke — paper-fallback reporting when an e-invoice couldn't be issued (unrelated to rejection)
- O = Odbijanje — recipient reports a REJECTED inbound e-invoice under čl.52. This is the only one relevant to Bilko's use case.
- Every row Bilko reports to Porezna is type O — there is no per-rejection N/U/O choice on the Porezna side.
- Fix applied:
received_einvoice.reason_codeis documented (Kotlin KDoc + V140 SQL comment) as Bilko's own internal rejection-reason taxonomy for the UI/audit trail (satisfying čl.52 st.1's "obrazloženo"/justified requirement), reusing the N/U/O letters only for DB/product convenience — not a claim of matching Porezna's N/U/O. RejectionReportService.RECORD_TYPEis hardcoded to"O"and is never derived fromreceived_einvoice.reason_code. Tested explicitly (RejectionReportIntegrationTesttest 5:recordType == "O"whilebilkoReasonCode == "U", asserted as two independently-preserved fields).
Full domain-gate detail: ~/system/evidence/106149/domain-gate-verdict.md
Schema
| Migration | Contents |
|---|---|
V140__received_einvoice_reject_reason.sql | Adds to received_einvoice: reason_code CHAR(1) CHECK IN ('N','U','O') (Bilko-internal taxonomy, see above), rejected_at, report_transmission_status CHECK IN ('pending','sent'), report_transmitted_at, report_period. Does not edit V139. |
V141__rejection_report_rls_bypass.sql | Two bilko_auth SECURITY DEFINER functions — find_rejected_einvoices_for_period, mark_einvoice_report_transmission — plus GRANT SELECT, UPDATE ON TABLE public.received_einvoice TO bilko_admin. Same ADR-017 Pattern B shape as V139's own find_org_by_registration_number. |
Lessons — RLS/SECURITY DEFINER pattern has now recurred 6x (V33/V39/V100/V111/V139/V141)
Two real bugs were caught live by the Testcontainers integration test (real Postgres + full V1..V141 Flyway chain, no SchemaUtils/mocks) during this build — not assumed, not pre-empted from memory:
- Cross-org RLS blind spot.
RejectionReportService's monthly batch query initially used a raw Exposedtransaction{}againstreceived_einvoice(FORCE ROW LEVEL SECURITY, V139) with noapp.current_org_idset. This silently returned ZERO rows for every org, every time — not an error, because RLS here is fail-closed-permissive: the query "succeeds" with an empty result set. Fixed via V141 SECURITY DEFINER functions. - SECURITY DEFINER without table GRANT. After adding the SECURITY DEFINER functions, they still failed with "permission denied for table received_einvoice" —
bilko_adminhas BYPASSRLS (V30_1/V32) but is not superuser and never received an explicit GRANT on this specific table (V139 only grantedbilko_app). Fixed by adding the GRANT in V141, following the exact precedent already set by V33/V39/V100/V111 for other tablesbilko_adminneeded to read via SECURITY DEFINER functions.
Callout for future FORCE-RLS table + batch/admin-query work: any new table with FORCE ROW LEVEL SECURITY that needs a cross-org admin/batch read path will hit both of these unless it (a) goes through a SECURITY DEFINER function from the start, and (b) that function's owning role has an explicit table GRANT. This is now a 6-occurrence pattern — worth turning into a migration checklist item or lint rule rather than re-discovering it per-feature.
What's explicitly NOT done
- Actual HTTP submission to Porezna is not implemented.
transmitToPorezna()is a clearly-marked TODO stub that always returnsfalse(even withSVERACUN_HR_LIVE=true), with a loudlog.warnexplaining why.submitMonthlyReport()therefore never falsely claims "sent". - Reason: payload/schema for the real eIzvještavanje submission is not confirmed at field level.
https://porezna-uprava.gov.hr/fiskalizacija/api/dokumenti/157returns HTTP 200 but serves a client-rendered SPA shell ("Naslovna"), not the raw document body — the literal field-level schema needs either human/browser (JS-rendering) access to that page, or the FINA/Porezna eIzvještavanje XSD. - No cron wiring yet. The
POST /internal/compliance/rejection-reportendpoint is not wired to an active ACA scheduled trigger in this task — same "safe to deploy inert" posture as the existing send-reminders sibling endpoint. FlowForge/John to wire per the KDoc go-live note. - No UI. API-only, M-tier task, out of scope per the original brief.
Verification performed
./gradlew compileKotlin compileTestKotlin— clean.RejectionReportIntegrationTest(new, Testcontainers postgres:16-alpine, real V1..V141 Flyway chain, no mocks): 8/8 passed. Covers reject persistence + status transition, no-op same-reason re-reject vs tracked different-reason correction, org isolation both directions, rejecting an already-accepted row throws, report payload recordType=O with correct bilkoReasonCode, non-rejected rows excluded, transmission state null→pending (never falsely 'sent'), report_period stamping, and idempotency (a row already 'sent' for a period is not re-transmitted).- Regression: pre-existing
ReceivedEInvoiceIntegrationTest(MC #106162) re-run after V140/V141/Tables.kt changes — still 0 failures. - Full suite:
./gradlew test— 1808 tests, 25 failed, all 25 isolated to 3 unrelated pre-existing classes (root cause:SVERACUN_SENDER_VAT not configured, a local-env config gap in outbound sveRacun e-invoicing, grep-verified unrelated to ReceivedEInvoice*/RejectionReport*/ComplianceCron*).
Branch / merge status
- Branch:
feat/task-106149-reject-report, commitfaf3aa78. - Based on:
feat/task-106162-inbound-einvoice-persist, commit06812347(V139received_einvoicetable + persist). - Neither branch is merged or pushed to azdo main. No PR opened yet.
- Built in isolated worktree
.claude/worktrees/codecraft-106149— the main Bilko working tree was left untouched (it had unrelated in-flight work onfeat/task-106147).
Next steps
- Human/browser session to pull field-level schema from
porezna-uprava.gov.hr/fiskalizacija/api/dokumenti/157(or FINA XSD) before implementing realtransmitToPorezna(). - Proveo end-to-end verification (reject → report, real evidence).
- Open PR:
feat/task-106162-inbound-einvoice-persist→ main, thenfeat/task-106149-reject-report→ main (or squash/rebase as one PR chain). - FlowForge: wire cron trigger for
POST /internal/compliance/rejection-report. mc.js readyfor #106149/#106146 after Proveo pass.
Source evidence
~/system/evidence/106149/build-verdict.md— full build verdict (CodeCraft, agent idcodecraft-106149, PASS)~/system/evidence/106149/domain-gate-verdict.md— domain/legal correction (agentbilko-porez-fiskalizacija-hr, CORRECTION NEEDED on Q1, applied)
No comments to display
No comments to display