# Bilko — Payment-event obaveze (JOPPD): dizajn, scope odluke, runbook — MC #105742

# Bilko — Payment-event obaveze (JOPPD): dizajn, scope odluke, runbook

**MC:** #105671 (dizajn) + #105741 (Task A) + #105742 (Task B) + #105744 (Finverge BA verifikacija) + #105747 (RS redizajn, otvoreno)
**Status (2026-07-15):** Task A merged (PR 164, `b1417252`, azdo/main). Task B PR 165 open (`6b9ebe5c`), NOT merged — Proveo P2P adversarial verify još u toku (mesh thread otvoren, guard-rail auto-BLOCKED nije pravi verdikt). HR-07/RS-07 katalog i dalje `enabled: false`.

---

## 1. Šta feature radi

Payment-event obaveza je Bilko-ova podsjetnik logika koja se okida na **stvarnu isplatu plaće**, ne na kalendarski datum kao postojeći mjesečni podsjetnici (npr. MIP-1023, Obrazac 1002).

Tok (HR, implementirano):

```
POST /payroll/organizations/{orgId}/payslip-runs/mark-paid
  { periodYear, periodMonth, paymentDate }
        │  (V123: payslips.payment_date, SERIALIZABLE transakcija)
        ▼
PayrollService.markPayrollRunPaid()
        │  postavlja payment_date na SVE payslip redove org/period
        │  (ista transakcija, samo na write-grani, nikad na idempotent no-op)
        ▼
PaymentEventObligationService.onPayrollRunPaid(orgId, periodYear, periodMonth, paymentDate)
        │  HR org → čita HR-07 (JOPPD) iz kataloga SAMO za title/form metadata
        │  due_date = resolveNextBusinessDay(paymentDate)
        │  RS/BA org → no-op (vidi §2b, §2c)
        ▼
upsert ComplianceDeadlines  (keyed: org, year, deadlineType, period)
        │  korekcija datuma → UPDATE istog reda, ne duplikat
        ▼
notifyIfNeeded()  — JEDNA in-app + email notifikacija na kreiranje, VAN transakcije
        (bez T-7/T-1 kadence — nema lead-time koncepta, rok = dan isplate)
```

`resolveNextBusinessDay(date)` — čista funkcija, semantika je **roll-forward SAMO ako datum pada na neradni dan**, ne "uvijek sljedeći radni dan":
- Pon–Pet → passthrough (utorak isplata ostaje utorak rok)
- Subota → ponedjeljak (+2)
- Nedjelja → ponedjeljak (+1)

Ovo je vikend-only MVP (bez praznik-tabele — vidi §2d).

---

## 2. Scope odluke s razlozima (ključno za budućnost)

### (a) BA_FED (FBiH) i BA_RS isključeni iz payment-event tipa — Finverge verifikacija #105744, verdikt **NE**

MIP-1023 (FBiH) i Obrazac 1002 (RS-BiH) su **fiksni mjesečni agregatni izvještaji**, ne payment-event:
- MIP-1023: rok = 15. u mjesecu za prethodni mjesec (čl. 31 st. 5 Pravilnika o primjeni Zakona o porezu na dohodak FBiH), period-tag se odnosi na obračunski mjesec, **ne na mjesec isplate**.
- Obrazac 1002: rok = 10. u mjesecu za sve isplate iz prethodnog mjeseca (Zakon o poreskom postupku RS).

Oba entiteta su konfirmisana sa 7+ (FBiH) i 6+ (RS-BiH) nezavisnih izvora (FEB, paragraf.ba, advokat-prnjavorac.com, fineks.ba, poreskaupravars.org, vladars.rs). Već su ispravno modelovani u postojećem fiksno-mjesečnom katalogu (PR 157, #105641, CEO-odobreno #105640) — Task B ih namjerno NE dira. **Ne dodavati BA_FED/BA_RS u payment-event tip triger.**

### (b) RS (Srbija) isključena iz Task B builda — red-zone FAIL, #105742 redzone-tax-verdict.md

Red-zone porezna verifikacija (persona `porez-hr-105742`, web-verified 5+ nezavisnih izvora: porezionline.rs, aktivasistem.com, paragraf.rs, purs.gov.rs) je dala **FAIL** verdikt na originalni HR+RS dizajn:

- **PPP-PD (RS Srbija) se podnosi PRIJE isplate**, ne poslije. Mehanizam: prijava → PURS kontrola → **BOP broj (Broj Odobrenja za Plaćanje)** → BOP se unosi u nalog za prenos → tek onda isplata.
- Mark-paid-poslije-isplate model (kao HR-07) je **konceptualno pogrešan** za RS: ako je korisnik već kliknuo "isplaćeno" bez prethodno podnesene PPP-PD/BOP-a, klijent je već u prekršaju — podsjetnik koji stigne nakon toga je zakašnjeo po definiciji, ne samo "kasni".
- Dodatno flagovano: repo dokument `PAYROLL-PHASE2-PLAN.md` §2b sadrži interno neusklađen opis roka ("15th of month following payment") koji je u sukobu i sa web-verified pravilom i sa dizajn-katalogom RS-07 (`PER_PAYMENT_EVENT`). Fix isporučen odvojeno u **#105747** — nije popravljeno u Task B.

**Posljedica za build:** Task B je implementiran **HR-only**. RS org u `onPayrollRunPaid()` je dokumentovan no-op — nema deadline red, nema notifikaciju, log linija referencira #105747. RS-07 katalog entry ostaje netaknut (`enabled: false`).

### (c) HR-07/RS-07 katalog `enabled: false` do praznik-tabele (#105743, Lexicon gate)

`ObligationEngine.generateDeadlines()` i dalje ne emituje HR-07/RS-07 (potvrđeno testom `ObligationEngineUnaffectedTest` — 3/3 PASS, uključujući provjeru protiv REALNOG učitanog kataloga). Payment-event insertion je potpuno odvojen kod-put od kalendarskog generatora — katalog služi samo kao metadata SSOT (title/form), ne kao evaluator za ovaj tip roka. `enabled: true` čeka:
1. Praznik-tabela (#105743, MVP je trenutno vikend-only)
2. Lexicon sign-off na `requires_expert_validation` konvenciju (isti obrazac kao postojeći RS-07 flag)

### (d) Vikend-only MVP — praznici su Task C (odvojen, ne blokira A/B)

Dizajn (#105671 §4) eksplicitno je priznao ovaj gap kao MVP-known-limitation, ne skriveni nedostatak: puni praznik-kalendar zahtijeva `holidays` referentnu tabelu po jurisdikciji/godini + Lexicon/tax-expert sign-off (Zakon o blagdanima HR vs Zakon o praznicima RS se razlikuju). Cost/risk ako se ship-a bez praznika: pogrešan rok kad je isplata 1-2 radna dana prije praznika (npr. dan prije Uskrsnog ponedjeljka — vikend-only logika kaže "sljedeći dan" = sam praznik, što je netačno). Task C nosi ovu tabelu odvojeno, ne blokira A/B ship.

**Bitna napomena (red-zone stavka 1):** AC1 pseudokod (Mon-Fri passthrough, Sat/Sun roll-forward) je **ispravan izvor istine** za JOPPD pravilo. Prozna formulacija u originalnom dizajn-sažetku ("next business day after payment date") je dvosmislena i može zavesti na pogrešnu "uvijek +1" implementaciju — kod treba pratiti AC1, ne prozni opis.

---

## 3. Tehnika

### SERIALIZABLE transakcija (Proveo P2P race nalaz + fix)

Proveo adversarial P2P review na #105741 (`proveo-angie-105741`, verdikt PASS s jednim MEDIUM nalazom) je našao da `markPayrollRunPaid()` radi read-then-conditionally-write pod default `TRANSACTION_READ_COMMITTED` izolacijom, bez `SELECT...FOR UPDATE` i bez SERIALIZABLE. Konkretan race: dva konkurentna mark-paid poziva za isti org+period sa RAZLIČITIM datumima mogu oba pročitati pre-write stanje, oba računati `changed=true`, i kasniji UPDATE tiho pobjeđuje (lost update) — "gubitnik" dobija 200/`changed=true` iako je njegov upis pregažen.

**Fix (commit `3aa0ece2`):** `markPayrollRunPaid()` sada koristi
```kotlin
orgTransaction(
    organizationId = organizationId,
    transactionIsolation = java.sql.Connection.TRANSACTION_SERIALIZABLE,
) { ... }
```
1:1 kopija postojećeg repo presedana za isti read-then-derive-then-write oblik (`ExpenseService.kt:267-269`, `InvoiceService.kt:661-664` — broj-generacija). Bez retry-on-serialization-failure wrappera (repo-wide grep potvrdio: nijedan presedan ga nema).

Novi konkurentni test (2 real threads, `CountDownLatch`, isti pattern kao `FiscalDeviceSequenceTest`): provjerava da nakon dva istovremena poziva sa različitim datumima **svi payslip redovi u run-u nose ISTI konačni payment_date** — nema split-state. 7/7 PASS, ponovljeno 3x, 0 flake.

### Upsert keying

`ComplianceDeadlines` upsert je keyed na `(organizationId, year, deadlineType, period)` — korekcija datuma (npr. payroll admin ispravi pogrešnu isplatu) ažurira POSTOJEĆI red umjesto da duplira. Potvrđeno testom "correction updates the existing deadline row instead of duplicating" (assertion na isti `deadline id` kroz korekciju).

### Notifikacija van transakcije

`notifyIfNeeded()` se zove IZ route handler-a, NAKON što `dbQuery{}` vrati rezultat — izvan `markPayrollRunPaid()`-ove SERIALIZABLE transakcije. Fires samo ako `result.obligationResult` nije null (tj. nikad na idempotent no-op granu).

### RS/BA no-op s testovima

`onPayrollRunPaid()` grana na `country != HR` → no-op, pokriveno testovima "RS org mark-paid does not create any compliance deadline" i "does not fire any notification". Isti no-op put pokriva i BA_FED/BA_RS (§2a).

### Migracija

**Task A:** `V123__payslip_payment_date.sql` — `ALTER TABLE payslips ADD COLUMN IF NOT EXISTS payment_date DATE NULL` + indeks na `(organization_id, period_year, period_month, payment_date)`. Flyway-replay test (V121 lekcija): migracija privremeno uklonjena → test PADA (3/3 FAILED), vraćena → PASS — dokazuje da test stvarno testira migraciju, ne samo Kotlin model.

**Task B:** Nema nove migracije — `ComplianceDeadlines` (V9) je već imala sve potrebne kolone, potvrđeno čitanjem postojeće Exposed definicije prije pisanja koda.

### PR / commit trag

| Task | Repo | PR | Commit | Status |
|---|---|---|---|---|
| A (#105741) | Bilko | [PR 164](https://dev.azure.com/alai-holding/Bilko/_git/Bilko/pullrequest/164) | `d7d27d0c` + fix `3aa0ece2` | **merged** (`b1417252`) |
| B (#105742) | Bilko | [PR 165](https://dev.azure.com/alai-holding/Bilko/_git/Bilko/pullrequest/165) | `6b9ebe5c` | open, adversarial verify u toku |

---

## 4. Runbook / gotchas

### Kako se aktivira za prave klijente

1. **#105743** — sagraditi `holidays` referentnu tabelu (jurisdikcija, datum, naziv), seed za HR + RS za tekuću + narednu godinu.
2. `resolveNextBusinessDay()` proširiti da konsultuje tabelu kad postoji za org-ovu jurisdikciju, fallback na vikend-only ako nema podataka za tu godinu (isti defanzivni pattern kao `OrgComplianceProfile.default()`).
3. **Lexicon sign-off** zabilježen po `requires_expert_validation` konvenciji — TEK ONDA flip `enabled: true` na HR-07 (i RS-07 nakon #105747 redizajna, vidi ispod).
4. Bez ovog koraka HR-07/RS-07 ostaju `enabled: false` u katalogu — payment-event insertion put radi nezavisno od tog flaga (flag utiče samo na `ObligationEngine`-ov kalendarski generator, ne na `PaymentEventObligationService`), ali production-ready gate je i dalje ovaj sign-off.

### RS (Srbija) redizajn — #105747, otvoreno

RS-07 (PPP-PD) NE smije se implementirati po istom "mark-paid → deadline poslije → jedna notifikacija" šablonu kao HR-07. Minimalne opcije za redizajn (iz red-zone verdikta):
- UX kao "upozorenje PRIJE potvrde isplate — PPP-PD mora biti podnesena i BOP dobijen prije nego označite kao isplaćeno", ili
- Ako MVP ne može modelovati prijava-prije-isplate tok, notifikacioni tekst mora eksplicitno reći da rok prethodi isplati, ne "X dana poslije".

Ne kopirati HR-07 `resolveNextBusinessDay(paymentDate)` šablon direktno na RS-07 bez ove korekcije.

### PAYROLL-PHASE2-PLAN.md §2b — zastario opis

Repo dokument `docs/regulatory/PAYROLL-PHASE2-PLAN.md` §2b sadrži netačan/zastario opis PPP-PD roka ("Due by the 15th of the month following payment") koji je u sukobu i sa web-verified pravilom i sa RS-07 katalog tipom (`PER_PAYMENT_EVENT`). Isti tekst identifikovan identičan u više worktree kopija (`angie-105568`, `codecraft-105355`, `codecraft-105193/4`, `codecraft-105192`, `proveo-105276`, `codecraft-105687`). Fix pripada #105747 — sinhronizovati sve kopije nakon RS redizajna, ne prije.

### Prije `mc.js ready`/`done` na #105742

- PR 165 mora biti merged (trenutno open).
- Proveo adversarial P2P verdikt na #105742 mora doći kroz stvarni verifier, ne mesh guard-rail auto-BLOCKED poruku (poznat lažni pozitiv, viđen i na #105741 i #105742 threadovima — `eval` agent eksplicitno kaže da nije pravi verifier response).
- #105747 (RS redizajn) je odvojen task, ne blokira HR-only #105742 merge.

---

## Izvori (evidence)

- `~/system/evidence/105671/design-proposal.md` — Petter Graff arhitekturni dizajn, MC #105671
- `~/system/evidence/105741/build-evidence-2026-07-15.md` — Task A build (codecraft-hadi-105741), SERIALIZABLE fix
- `~/system/evidence/105741/proveo-p2p-verdict.md` — Proveo adversarial P2P (proveo-angie-105741), PASS + MEDIUM race nalaz
- `~/system/evidence/105742/build-evidence-2026-07-15.md` — Task B build (codecraft-hadi-105742), HR-only scope
- `~/system/evidence/105742/redzone-tax-verdict.md` — red-zone porezna verifikacija (porez-hr-105742), FAIL na RS
- `~/system/evidence/105744/finverge-ba-payment-event.md` — Finverge BA verifikacija (finverge-105744), NE za BA_FED/BA_RS