# bilko

Bilko product documentation

# Test Sweep 2026-05-15

# Bilko Comprehensive Test Sweep — 2026-05-15

**Date:** 2026-05-15  
**Verdict:** PARTIAL  
**Mission Control:** MC #100704 (preflight) through #100715 (this documentation)  
**Orchestrated by:** John (CEO request option C)  
**Executed via:** Ollama DAG (primary) + direct subagent fallback  
**Validator pattern:** /verify-fix-loop  

---

## Summary

A comprehensive test sweep across Bilko's 3 deployed domains (bilko.io stage, bilko.cloud UAT, bilko.company UAT) covering 9 test categories: backend unit, backend integration, frontend unit, end-to-end (×3 domains), SAST, DAST, and performance + accessibility.

**Consolidated verdict: PARTIAL**

- **0 critical security findings**
- **0 product regressions**
- All test failures categorized as either test infrastructure harness issues (in-scope for fix-loop) or pre-existing structural defects (out-of-scope for this sweep, tracked separately)
- Backend unit tests: 693/693 PASS after StubCountryPlugin fix
- Backend integration tests: 556/576 PASS (20 failures pre-existing, enum DDL ordering)
- Frontend unit tests: 56/67 PASS (11 harness failures: localStorage mock, i18n stub, null guard)
- E2E bilko.io stage: 3/37 PASS (auth-fresh fixture does not persist access token)
- E2E bilko.cloud: 8/8 PASS
- E2E bilko.company: 8/8 PASS
- SAST: Detekt 0 findings, npm audit 2 HIGH (devDependencies), SonarCloud + Snyk blocked on missing tokens
- DAST: 0 CRITICAL, 0 HIGH, 4 MEDIUM (missing x-frame-options, CSP on bilko.cloud + bilko.company)
- Performance + Accessibility: 100/96/96/92 (all 3 domains)

**Sweep cost:** TBD (to be extracted from MC aggregation)  
**Execution method:** DAG orchestration → task distribution → validator synthesis  
**Follow-up MCs opened:** 6 MCs for fix-loop iteration B (test harness repairs + header hardening)

---

## Scope

### Domains tested
1. **bilko.io stage** — https://bilko-web-stage-dh4m46blja-lz.a.run.app (GCP Cloud Run, Next.js 15 frontend)
2. **bilko.cloud** — https://bilko.cloud (Cloudflare Pages, HR market UAT landing)
3. **bilko.company** — https://bilko.company (Cloudflare Pages, BA market UAT landing)

### Test categories (9)
1. Backend unit tests (Kotlin/Ktor, Kotest)
2. Backend integration tests (Kotlin/Ktor, Testcontainers PostgreSQL)
3. Frontend unit tests (Next.js 15, Vitest, jsdom)
4. End-to-end bilko.io stage (Playwright, auth-fresh fixture)
5. End-to-end bilko.cloud UAT (Playwright, static landing assertions)
6. End-to-end bilko.company UAT (Playwright, static landing assertions)
7. SAST (Detekt, npm audit, SonarCloud, Snyk)
8. DAST (passive header analysis, OWASP ZAP baseline)
9. Performance + Accessibility (Lighthouse CI, 3 runs per domain)

### Blueprint references
- Test Plan: `/Users/makinja/business/ALAI-Holding-AS/products/Bilko/docs/TEST-PLAN.md` (v1.0, 2026-02-23)
- Test Strategy: `/Users/makinja/business/ALAI-Holding-AS/products/Bilko/docs/testing/TEST-STRATEGY.md`
- Build Blueprint: `/Users/makinja/business/ALAI-Holding-AS/products/Bilko/BUILD-BLUEPRINT.md` (top 50 lines reviewed)

---

## Results by Category

| Category | Verdict | Tests Passed | Tests Total | Key Numbers | Evidence File |
|----------|---------|--------------|-------------|-------------|---------------|
| BE Unit | **PASS** | 693 | 693 | 121/121 core, 29 suites | api-unit-rerun-v2.json |
| BE Integration | **PARTIAL** | 556 | 576 | 20 failures (enum DDL) | api-int-rerun-v2.json |
| FE Unit | **PARTIAL** | 56 | 67 | 14 suites pass, 6 fail | web-unit.json + core-unit.json |
| E2E bilko.io | **FAIL** | 3 | 37 | 34 unexpected (401) | e2e-io/summary.json |
| E2E bilko.cloud | **PASS** | 8 | 8 | All assertions verified | e2e-cloud/summary.json |
| E2E bilko.company | **PASS** | 8 | 8 | All assertions verified | e2e-company/summary.json |
| SAST | **PARTIAL** | Detekt 0 | — | npm audit 2 HIGH | sast-summary-100711.json |
| DAST | **PARTIAL** | 0 CRITICAL | — | 4 MEDIUM header gaps | dast-summary-100712.json |
| Perf + A11y | **PASS** | 100/96/96/92 | 3 domains | All gates pass | lhci-io/cloud/company.json |

---

## Findings

### Critical Security Findings
**Count: 0**

### Product Regressions
**Count: 0**

All test failures fall into two categories:
1. **Test infrastructure/harness issues** (in-scope for fix-loop iteration 2):
   - auth-fresh.ts fixture does not persist access token across Playwright context resets (E2E bilko.io 34/37 FAIL)
   - localStorage mock missing in Vitest jsdom setup (FE settings.test.tsx 8 failures)
   - i18n stub incomplete in invoices.test.tsx (raw key `{invoices}` rendered)
   - MarketContext.test.tsx null guard missing (1 test TypeError)

2. **Pre-existing structural defects** (out-of-scope, tracked separately):
   - InvoiceStatus enum DDL ordering in DbTestBase.setUpDatabase (BE integration 20 failures)
   - SonarCloud + Snyk tokens absent from Bitwarden (SAST coverage gap)
   - x-frame-options, CSP absent on bilko.cloud + bilko.company (DAST 4 MEDIUM)
   - npm audit 2 HIGH in devDependencies (rollup via @sentry/nextjs, tmp via @lhci/cli)

---

## Fix-Loop Iteration 2 Outcomes

**Status at publish time:** In progress (CodeCraft B-half dispatched)

Fix-loop candidates identified by validator:
1. auth-fresh.ts fixture — inject access token into Playwright storageState (Vizu/Playwright author, effort S)
2. localStorage mock — add vi.stubGlobal in vitest.config.ts setupFiles (CodeCraft/FE author, effort XS)
3. i18n stub — add i18n mock provider in test wrapper (CodeCraft/FE author, effort S)
4. MarketContext null guard — add null guard in MarketContext.tsx or test stub (CodeCraft/FE author, effort S)

**Follow-up MCs opened (6):**
1. MC TBD — Fix InvoiceStatus enum ordering in BE integration test schema (DbTestBase.setUpDatabase) — Priority H, owner CodeCraft
2. MC TBD — Provision SONAR_TOKEN in Bitwarden and enable SonarCloud SAST scan — Priority M, owner FlowForge
3. MC TBD — Provision Snyk auth token in Bitwarden and integrate Snyk into CI — Priority M, owner FlowForge
4. MC TBD — Add _headers file to bilko.cloud and bilko.company CF Pages deployments (x-frame-options, CSP, HSTS) — Priority H, owner FlowForge
5. MC TBD — Fix rollup HIGH CVE (GHSA-mw96-cpmx-2vgc) via @sentry/nextjs upgrade or exclusion — Priority M, owner CodeCraft
6. MC TBD — Execute ZAP active scan against bilko.io stage after image stabilises — Priority M, owner Securion

---

## Lighthouse Scores (Performance + Accessibility)

All 3 domains tested with Lighthouse CI (3 runs each, median scores):

| Domain | Performance | Accessibility | Best Practices | SEO |
|--------|-------------|---------------|----------------|-----|
| bilko.io | 100 | 96 | 96 | 92 |
| bilko.cloud | 100 | 96 | 96 | 92 |
| bilko.company | 100 | 96 | 96 | 92 |

**Gates:**
- perf_gt_70: **PASS** (all 3 domains)
- a11y_gt_90: **PASS** (all 3 domains)

**A11y failures (all 3 domains, same pattern):**
- aria-allowed-role: Uses ARIA roles on incompatible elements
- color-contrast: Background and foreground colors do not have sufficient contrast ratio
- label-content-name-mismatch: Elements with visible text labels do not have matching accessible names

Score remains 96/100 (within acceptable range per TEST-PLAN §7.4 target: Lighthouse Performance Score > 90).

**Note:** axe-core WCAG 2.1 AA Playwright tests (TEST-PLAN §8.9) were not executed in this sweep — blocked by e2e_io auth-fresh fixture failure.

---

## Evidence Index

All evidence files stored in `/tmp/bilko-test-sweep/` with integrity checksums:

| File | Category | SHA256 (first 16 hex) | Notes |
|------|----------|------------------------|-------|
| validator-verdict.json | Validator synthesis | (computed at publish) | Canonical consolidated verdict |
| api-unit-rerun-v2.json | BE unit | (computed) | 693/693 PASS |
| api-int-rerun-v2.json | BE integration | (computed) | 556/576 PASS, 20 failures |
| web-unit.json | FE unit | (computed) | 56/67 PASS |
| core-unit.json | FE unit (core) | (computed) | 121/121 PASS |
| e2e-io/summary.json | E2E bilko.io | (computed) | 3/37 PASS |
| e2e-cloud/summary.json | E2E bilko.cloud | (computed) | 8/8 PASS |
| e2e-company/summary.json | E2E bilko.company | (computed) | 8/8 PASS |
| sast-summary-100711.json | SAST | (computed) | Detekt 0 findings, npm audit 2 HIGH |
| dast-summary-100712.json | DAST | (computed) | 0 CRITICAL, 0 HIGH, 4 MEDIUM |
| lhci-io.json | Perf+A11y bilko.io | (computed) | 100/96/96/92 |
| lhci-cloud.json | Perf+A11y bilko.cloud | (computed) | 100/96/96/92 |
| lhci-company.json | Perf+A11y bilko.company | (computed) | 100/96/96/92 |
| mc-ids.json | MC tracking | (computed) | 12 MC IDs (#100704–#100715) |

**Screenshot evidence:**
- `/tmp/bilko-test-sweep/e2e-io/stage-login-ui.png` — bilko.io stage login UI (login succeeds)
- `/tmp/bilko-test-sweep/e2e-io/dashboard-401-failure.png` — Dashboard API 401 failure (access token not forwarded)
- `/tmp/bilko-test-sweep/e2e-cloud/bilko-cloud-home.png` — bilko.cloud home page
- `/tmp/bilko-test-sweep/e2e-cloud/bilko-cloud-pricing.png` — bilko.cloud pricing section
- `/tmp/bilko-test-sweep/e2e-company/bilko-company-home.png` — bilko.company home page
- `/tmp/bilko-test-sweep/e2e-company/bilko-company-pricing.png` — bilko.company pricing section

---

## Genesis

**CEO ask:** 2026-05-15 morning  
**Option selected:** C (comprehensive sweep across all test categories)  
**Execution method:** Ollama DAG orchestration (primary) + direct subagent fallback when DAG blocked  
**Validator pattern:** /verify-fix-loop (atomic-claim decomposition, read-only verification)  
**12 MCs spawned:** #100704 (preflight), #100705 (BE unit), #100706 (BE integration), #100707 (FE unit), #100708 (E2E bilko.io), #100709 (E2E bilko.cloud), #100710 (E2E bilko.company), #100711 (SAST), #100712 (DAST), #100713 (Perf+A11y), #100714 (validator), #100715 (this documentation)

**Blueprint compliance:**

**Met:**
- TEST-PLAN §2.1 core unit tests: 121/121 PASS (accounting, tax, multi-currency, invoicing, chart-of-accounts)
- TEST-PLAN §7.4 Lighthouse Performance Score >90: 100/100 all 3 domains
- TEST-PLAN §8.9 Lighthouse Accessibility Score >=90: 96/100 all 3 domains
- TEST-STRATEGY §6 Financial logic (VAT, double-entry, currency) tested at >95% coverage (core-unit 121/121)
- BE unit tests 693/693 PASS (Kotlin/Ktor backend, StubCountryPlugin fix applied)
- E2E UAT landing pages (bilko.cloud + bilko.company): 8/8 PASS each
- DAST: 0 CRITICAL, 0 HIGH security findings on any target
- Detekt Kotlin SAST: 0 findings
- Netty CVE pre-remediated (MC #99531)

**Gaps:**
- TEST-PLAN §3 / TEST-STRATEGY §7 PR merge gate: BE integration tests 20/576 FAIL (InvoiceStatus enum) — blocks merge gate
- TEST-STRATEGY §10 Production Deploy Gate: "All E2E tests pass on staging" — e2e_io 34/37 FAIL (auth-fresh fixture)
- TEST-PLAN §8.8 Security headers: x-frame-options, CSP absent on bilko.cloud and bilko.company
- TEST-PLAN §8 SAST: SonarCloud and Snyk not executed — token provisioning gap
- TEST-PLAN §10 coverage target: country-module unit tests (country-rs, country-ba, country-hr) explicitly listed as 0% in TEST-PLAN §10
- TEST-PLAN §2.1 bank-import.test.ts listed as MISSING in blueprint — still not present
- axe-core WCAG 2.1 AA Playwright tests (TEST-PLAN §8.9) not executed — blocked by auth-fresh fixture failure
- k6 load tests (TEST-PLAN §7.2) not executed — out of scope for this sweep (Phase 2)

---

## Related Pages

- [Bilko Test Plan v1.0](https://docs.alai.no/books/bilko/page/test-plan)
- [Bilko Test Strategy](https://docs.alai.no/books/bilko/page/test-strategy)
- [Mission Control #100704–#100715](https://docs.alai.no/books/mission-control)

---

**Published by:** Skillforge (ALAI knowledge management)  
**Reviewed by:** John (AI Director)  
**Approved for publish:** 2026-05-15

# Bilko HR Roadmap — Fiscal Compliance + Feature-Enable Architecture (2026-05-29)

# Bilko HR Roadmap — Fiscal Compliance + Feature-Enable Architecture

**Created:** 2026-05-29
**CEO directive:** B2B prvo, B2C odgođen, **citav app feature-enable based (micro-frontend)** za per-user per-plan delivery
**Status:** Active roadmap, supersedes prior B2B-only scoping

---

## 1. Executive summary

- HR ima **dva odvojena fiskalna sistema**: **F1 (B2C, SOAP)** i **F2 (B2B, Peppol)** — oba mandatorna od 01.01.2026 per Zakon o fiskalizaciji **NN 89/25** (porezna-uprava.gov.hr verified 2026-05-29)
- **CEO odluka 2026-05-29:** B2B prvo (Phase 0), B2C odgođeno
- **CEO arhitektonska direktiva 2026-05-29:** Feature-enable based / micro-frontend / per-user per-plan delivery — NE hackovati per feature
- **B2B path:** Storecove (~70% Bilko code već postoji, u pregovorima oko cijene) ili Sveračun (negotiated <€0.10/invoice, čekamo creds)
- **Multi-tenant accountant view** (1 računovođa → 30+ klijent orgs) zavisi od prvog B2B ACK
- **Direktni rizik produkta ako feature-enable ne dođe prvo:** plan tier leakage, ad-hoc feature flag hacking, market cross-contamination

---

## 2. Croatian fiscal legal framework

### Zakon o fiskalizaciji
- Aktuelni tekst: **NN 89/25** (Narodne novine)
- Source: https://porezna-uprava.gov.hr/hr/fiskalizacija/3982

### F1 — B2C fiskalizacija (POS / kasa)
- Aktivno od 2013 (Fiskalizacija 1.0), proširen 2026 (Fiskalizacija 2.0)
- Path: real-time **SOAP** ka `cis.porezna-uprava.hr`
- **JIR** (Jedinstveni identifikator računa) — generira Porezna uprava u SOAP response-u
- **ZKI** (Zaštitni kod izdavatelja) — MD5 hash `(OIB + DateTime + InvoiceNumber + PPCode + DeviceCode + TotalAmount)`, RSA-signed sa FINA cert
- **Cert obaveza:** PER-ORG FINA application certificate, **bez moguće intermedijarne organizacije**
- Receipt arhiva: **11 godina**
- Hardware fiskalni printer: **NIJE legalno mandatoran** (web POS dovoljan)
- Source: https://porezna-uprava.gov.hr/hr/fiskalizacija-racuna-u-krajnjoj-potrosnji-b2c-poslovanje/8033

### F2 — B2B eRačun
- Mandatorno od 01.01.2026 (svi B2B sa drugim PDV obveznicima)
- Format: **HR-FISK CIUS** (Peppol BIS Billing 3.0 sa hrvatskim ekstenzijama)
- Path: preko Peppol intermediara (Storecove, Sveračun, drugi)
- **Cert NIJE per-org** — intermediar pokriva (key razlika od B2C)
- UBL 2.1 XML (težak dio) — već implementiran u Bilko
- Source: https://porezna-uprava.gov.hr/hr/izdavanje-i-primanje-eracuna-i-fiskalizacija-eracuna/8047

### Terminologija
- **"F1"** = B2C sistem (ne form-type / cert-type)
- **"F2"** = B2B sistem (ne form-type / cert-type)
- Common confusion: ovo NISU schema verzije ili cert tipovi

### PDV stope
- Standard: 25%
- Reduced: 13% (turizam, hospitalitet)
- Reduced: 5% (osnovne potrebe, mediji)
- Zero: 0% (izvoz, intra-EU isporuke)

---

## 3. Phased roadmap sa MC mapingom

### Phase 0 — B2B prvo (current focus)

| Phase | MC | Naslov | Status | Vlasnik |
|---|---|---|---|---|
| 0a | **#102447** | Storecove HR-FISK 2.0 activation (FAST PATH, 4-5 dana) | open — counter-offer poslan 2026-05-28 | john |
| 0b | **#102398** | Sveračun API sandbox onboarding (negotiated <€0.10/invoice) | open — čeka CEO email za creds + računovođa intro | john |
| **0c** | **#102481** | **Feature-enable based micro-frontend architecture** (CEO 2026-05-29) | **open — kritičan prerequisite, dolazi PRIJE Phase 1** | john |
| 0d | #102401 | Bilko Phase 0 multi-org switcher + Securion RLS gate | open — zavisi od bilo koji B2B ACK | john |
| 0e | #102399 | 5 accountant pilot recruitment (90-day) | open — CEO outreach | alem |
| 0f | #102400 | HR pravna osoba tracking (background) | open — Sveračun potvrdio NIJE potrebno za B2B; **B2C TBD** | alem |

### Phase 1+ — B2C (DEFERRED per CEO 2026-05-29)

| Phase | MC | Naslov | Status |
|---|---|---|---|
| ~1~ | ~#102478~ | ~B2C fiskalizacija MVP (ZKI + JIR + SOAP + FINA cert)~ | **DEFERRED** — 21 dana effort |
| ~2~ | ~#102479~ | ~B2C POS web kasa modul~ | **DEFERRED** — 19 dana effort |
| 3 | TBD | Hardware integration (fiskalni printer, cash drawer, barcode) | not opened |
| 4 | TBD | Industry specializations (hospitality, retail, services) | not opened |

---

## 4. Feature-enable architecture directive

### CEO directive (verbatim, 2026-05-29)
> "citav app treba da je feature enable based (micro frontend) nadam se da mozemo lako features per user per pay deliver a ne da sve hakiramo"

### Problem statement
- B2B + B2C + accountant view + direct SMB + multi-market (HR/RS/BiH) — sve raste kao monolit
- Risk: per-feature ad-hoc hacking
- Risk: revenue leakage (Basic plan vidi Pro feature)
- Risk: dev na HR markeu lomi RS market

### Cilj — komponente architekture

1. **Central feature catalog** — kanonski popis svih feature-a sa metadata (market, plan tier, dependency graf)
2. **Per-user feature resolution** — funkcija od (tenant org country + plan tier + accountant role + custom overrides)
3. **Micro-frontend split** — nezavisni feature modules koji se učitavaju/ne učitavaju po feature flag-u
4. **Pricing model alignment** — Basic/Pro/Accountant tier = feature subset (definirano u catalog-u)
5. **Safe fallback** — feature flag service down → conservative defaults (Basic, ne Pro)
6. **Audit trail** — koji user kad accessovao koji feature

### Implementation phases (MC #102481)
- Phase 0: feature catalog kanonski + DB schema + resolution endpoint (~M effort)
- Phase 1: per-tenant feature flag application u backend (~S)
- Phase 2: micro-frontend split frontend (~L)
- Phase 3: pricing model wiring (~M)
- Phase 4: admin UI za enable/disable per tenant (~S)

### Build vs buy
Datavera istražuje:
- LaunchDarkly (mature, $$)
- GrowthBook (open-source self-hosted)
- Unleash (open-source self-hosted)
- ConfigCat (mid-tier)
- Build-our-own (potpuna kontrola, više rada)

---

## 5. Open decisions for CEO

1. **B2B path:** prihvatamo Storecove counter-offer (ako stigne s revidiranim quote) ili idemo Sveračun?
2. **HR pravna osoba:** potvrđeno NIJE potrebna za B2B; otvoreno za buduće B2C (kad ga otvorimo)
3. **Feature catalog vendor:** build vs buy — odluka nakon Datavera istrage
4. **Multi-market timing:** kad RS (SEF) + BiH ulaze u feature catalog scope?

---

## 6. MC indeks

| MC | Priority | Status | Vlasnik | Naslov | Rute |
|---|---|---|---|---|---|
| #100332 | H | ready_for_review | alem | Sveračun CEO outreach (parent) | bizdev |
| #102447 | H | open | john | MC-A2 Storecove FAST PATH | backend |
| #102398 | H | open | john | MC-A Sveračun sandbox onboarding | backend |
| **#102481** | **H** | **open** | **john** | **Feature-enable architecture (CEO 2026-05-29)** | **backend** |
| #102401 | H | open | john | MC-D Phase 0 multi-org switcher | backend |
| #102399 | H | open | alem | MC-B 5 accountant pilot recruit | bizdev |
| #102400 | M | open | alem | MC-C HR doo tracking | bizdev |
| #102478 | H | open (DEFERRED) | john | MC-E B2C fiskalizacija MVP | backend |
| #102479 | M | open (DEFERRED) | john | MC-F B2C POS web kasa | frontend |
| #102448 | H | open | john | Vendor email routing + auto-forward | devops |
| #102449 | H | open | john | Policy: no strategic reply bez CEO OK | bizdev |

---

## 7. Source artifacts

- `/tmp/alai/bilko-hr-fiscal-full-scope-20260529.md` — Datavera HR law research (640 linija)
- `/tmp/alai/bilko-hrfisk-audit-20260528.md` — CodeCraft FISK code path audit
- `/tmp/alai/sveracun-meeting-outcome-20260528.md` — Sveračun meeting outcome
- `/tmp/alai/bilko-accountant-gtm-spec-20260528.md` — Skybound accountant-led GTM spec
- `/tmp/alai/bilko-multitenant-accountant-gap-20260528.md` — CodeCraft multi-tenant tech gap
- `/tmp/alai/storecove-counter-reply-20260528.md` — Storecove counter-offer state

---

## 8. Decision log

| Datum | Odluka | Kontekst |
|---|---|---|
| 2026-05-28 | Bilko HR ide **dual-track GTM** (accountant + direct SMB) | Sveračun meeting strategic input |
| 2026-05-28 | **Storecove counter-offer** poslan (no name drop Sveračun) | CEO directive — leverage |
| 2026-05-29 | **B2B prvo, B2C odgođen** | CEO scope decision |
| 2026-05-29 | **Feature-enable / micro-frontend arch** je obavezan prerequisite za sve daljnje builds | CEO architecture directive |

---

## 9. Next steps

1. **Storecove reply chase** — ako nema odgovora do 2026-06-04, poslati follow-up sa ref na contract expiry 27-06-2026
2. **Sveračun follow-up email** — CEO šalje za sandbox creds + 3-5 računovođa intro (target: 2026-05-30)
3. **Datavera dispatch** za feature catalog vendor comparison (LaunchDarkly vs GrowthBook vs Unleash vs build-our-own)
4. **Mehanik gate** za MC #102481 (feature-enable arch) prije CodeCraft Phase 0 dispatch
5. **Update MEMORY.md** sa pointer-om na ovu BookStack stranicu (skip-list ako se promijeni status)

# Bilko BUG-005 — revenueMTD / credit-note documentType + RLS migration landmine

# Bilko BUG-005 — revenueMTD negative (credit-note documentType + RLS migration landmine)

**MC:** #103001 (child #102887) | **Fixed:** 2026-06-05 | **Tag:** v0.2.18 / PR #255

## Symptom
Dashboard KPI `revenueMTD` showed -457.50 on bilko-demo. CEO-visible.

## Root cause (two layers)
1. **Write-side:** `InvoiceService.createCreditNote` insert omitted `documentType`. The
   `Invoices.documentType` column has no Kotlin `.default`, so the PG column default `'standard'`
   applied. Every credit note was stored as STANDARD with a negative amount. The v0.2.17 read-side
   fix (ReportService.getRevenueForPeriod excludes CREDIT_NOTE) therefore could not exclude them.
2. **Backfill blocked by RLS:** migration V65 (`UPDATE invoices SET document_type='credit_note'
   WHERE document_type='standard' AND invoice_number LIKE 'CN-%'`) recorded `success=t` but affected
   **0 rows**. The `org_isolation` RLS policy returns FALSE when `app.current_org_id` is unset;
   Flyway runs as `bilko` with no org GUC, so RLS hid every row. No SQL error → Flyway "succeeded".

## Fix
- PR #255: `createCreditNote` now sets `it[Invoices.documentType] = InvoiceDocumentType.CREDIT_NOTE`.
- Historical backfill applied on the serving DB (tribal-sign-487920-k0:europe-north1:bilko-demo-db)
  via `ALTER TABLE invoices NO FORCE ROW LEVEL SECURITY` (table owner) → UPDATE (5 rows) → COMMIT →
  `FORCE ROW LEVEL SECURITY` restored.

## Verification (Proveo independent PASS, live)
revenueMTD 375.00 (was -457.50); CN-2026-001..007 all documentType=credit_note; freshly created CN
is credit_note; INV-2026-001 stays standard. Evidence: /tmp/verify-103001/proveo-validation.md.

## Reusable lessons
- **Data-backfill migrations on RLS tables must bypass RLS** (`ALTER TABLE … NO FORCE ROW LEVEL
  SECURITY` wrapper) or they silently no-op while reporting success. Verify by ROW COUNT, not Flyway success.
- **Verify by live outcome, not green build** — every CI/deploy signal said "shipped" while live data was wrong.
- cloudbuild.yaml coverage gsutil-upload step makes every demo deploy report FAILURE despite success (follow-up).

# Bilko Backoffice — Backend MVP (Sentry + audit request_id + support_tickets)

## 1. Overview

 This backend slice (MC #103323, branch `feat/103323-backoffice-backend`, commit `6b214a00`, PR [\#316](https://github.com/johnatbasicas/bilko/pull/316)) delivers the diagnostic and intake backbone for the Bilko support fix-loop.

 Before this slice, when a customer hit an accounting error on `app.bilko.cloud` neither the platform team nor the customer had a way to identify which request failed or why. Three components address that gap:

1. **Sentry error capture** — catch-all (INFRA-only) exception capture with PII scrub and Cloud Run release/serverName metadata. Inert until `SENTRY_DSN` secret is provisioned (OCD-1, CEO action).
2. **V71 audit\_log.request\_id** — nullable correlation column added to every audit row, threaded from a single canonical source (`call.callId`) across all route handlers.
3. **V72 support\_tickets + SupportTicketRoutes** — customer intake channel (POST) and platform-admin triage queue (GET list + GET detail + PATCH status) with RLS, idempotency, and full status-transition audit trail.

 This slice is **deploy-gated** behind prod cutover MC #103300. All three components were independently verified by Proveo (Angie Jones): 12/12 AC signals PASS, integration test 3/3 PASS, unit suite 1280/1280.

## 2. Component Map

### 2.1 Sentry capture — plugins/Sentry.kt + plugins/StatusPages.kt

- **DSN guard:** `configureSentry()` checks `SENTRY_DSN`; if absent or blank, `Sentry.init` is not called. The SDK stays in silent no-op mode. CI / Testcontainers / local dev never emit live Sentry events.
- **Cloud Run metadata:** `K_REVISION` maps to `options.release`; `K_SERVICE` maps to `options.serverName`. Fallbacks: `"local"` and `"bilko-api-local"`.
- **beforeSend PII scrub:** request body (`event.request?.data = null`) and breadcrumbs (`event.breadcrumbs?.clear()`) stripped before transmission. Extra context filtered to allowlist: `errorCode, requestId, orgId, httpStatus, instancePath`.
- **Single capture point — Throwable catch-all only:** `Sentry.captureException` is placed exclusively in the `exception<Throwable>` handler in `StatusPages.kt` (line 237). Named typed handlers (BadRequest, Conflict, Unauthorized, Forbidden, etc.) do not call captureException — those cover 4xx user-error exceptions. Ktor StatusPages dispatches named handlers first; Throwable catch-all fires only for genuine INFRA/unexpected exceptions. AC signal: grep returns count=1 in both checks.
- **Sentry scope tags:** `requestId` from `call.callId` (CallId plugin canonical source), `orgId` from `BilkoPrincipal.organizationId` (fallback `"UNKNOWN"` for pre-auth crashes — mandatory), `errorCode = INFRA_001`.

### 2.2 V71 audit\_log.request\_id — AuditLogService.kt + migration

- **Migration V71:** `ALTER TABLE audit_log ADD COLUMN request_id TEXT;` — nullable, no default. PG 11+ metadata-only operation (no table rewrite). Plain `CREATE INDEX` (not CONCURRENTLY) on partial index `WHERE request_id IS NOT NULL`. CONCURRENTLY is prohibited inside Flyway transactions (institutional memory from V70; AC signal confirms absence).
- **Column type TEXT:** chosen over UUID because clients can supply arbitrary `X-Request-ID` header values. Trust boundary: client-supplied, stored verbatim, correlation/debuggability only.
- **No idempotency constraint on audit\_log:** one HTTP request legitimately produces multiple audit rows (e.g. impersonation start + org update in same admin session). A UNIQUE constraint would reject valid multi-row sequences. Idempotency enforced at V72 layer.
- **AuditLogService.insert signature:** added `requestId: String? = null` as last parameter (default null = backward compatible). Docstring: "Correlation handle for cross-system debugging only — NOT a security control. Client-supplied value stored verbatim."
- **Single canonical requestId source:** `call.callId` (Ktor CallId plugin) is the single authoritative source across StatusPages (Throwable catch-all), AdminPortalRoutes, ImpersonationService, and SupportTicketRoutes. Typed domain handlers retain raw header for RFC 7807 echo-back to client only — these do not call captureException and do not write to audit\_log, so the split is intentional and does not break the diagnostic join. (bruce-momjian dissent resolution)

### 2.3 V72 support\_tickets + SupportTicketRoutes — routes/SupportTicketRoutes.kt

- **POST /support/tickets** (customer, JWT-scoped): `orgId` and `userId` extracted from `BilkoPrincipal` only — never from request body. `context_bundle` server-side validated against CONTEXT\_BUNDLE\_ALLOWLIST before insert. `app.current_org_id` set via `orgTransaction(principal.organizationId)` so RLS WITH CHECK passes. Idempotency: duplicate `(org_id, request_id)` returns 409.
- **GET /admin/support/tickets** (platform-admin): paginated list, `limit` (default 50, max 100) + `offset`, optional `status` and `orgId` filters. Returns `data` array + `meta.total/limit/offset`.
- **GET /admin/support/tickets/{id}** (platform-admin): single ticket detail.
- **PATCH /admin/support/tickets/{id}** (platform-admin): enforces status transition machine, requires `resolutionNote` for RESOLVED/CLOSED, inserts audit\_log row for every status change with `requestId = call.callId`. Audit write failure is non-fatal but logged to structured stderr (Cloud Logging visible).
- **Admin GUC pattern:** `transaction { exec("SET LOCAL app.is_platform_admin = 'true'") }` — SET LOCAL per transaction, pgBouncer transaction-mode pooling safe.

## 3. Data Model

### 3.1 support\_tickets columns

<table id="bkmrk-columntypenotes-iduu"> <thead><tr><th>Column</th><th>Type</th><th>Notes</th></tr></thead> <tbody> <tr><td>id</td><td>UUID PK</td><td>gen\_random\_uuid() default</td></tr> <tr><td>org\_id</td><td>UUID NOT NULL</td><td>FK to organizations(id) ON DELETE CASCADE</td></tr> <tr><td>user\_id</td><td>UUID NOT NULL</td><td>FK to users(id)</td></tr> <tr><td>error\_code</td><td>TEXT</td><td>Nullable; currently generic VAL/INFRA (OCD-2 open CEO decision)</td></tr> <tr><td>request\_id</td><td>TEXT</td><td>Correlation ID of originating failed request. NOT a FK to audit\_log.request\_id (one request\_id maps to N audit rows). Join via equality.</td></tr> <tr><td>context\_bundle</td><td>JSONB NOT NULL</td><td>CHECK jsonb\_typeof = 'object'. Allowlisted keys only (server-side enforced).</td></tr> <tr><td>customer\_description</td><td>TEXT</td><td>Free text from customer</td></tr> <tr><td>status</td><td>TEXT NOT NULL</td><td>CHECK (status IN ('OPEN','TRIAGED','IN\_PROGRESS','RESOLVED','CLOSED')). Default 'OPEN'.</td></tr> <tr><td>triage\_json</td><td>JSONB</td><td>NULL = not yet triaged. V2 AI agent writes here.</td></tr> <tr><td>created\_at</td><td>TIMESTAMPTZ NOT NULL</td><td>DEFAULT now()</td></tr> <tr><td>updated\_at</td><td>TIMESTAMPTZ NOT NULL</td><td>DEFAULT now(); maintained by BEFORE UPDATE trigger.</td></tr> <tr><td>resolution\_note</td><td>TEXT</td><td>Required (route-enforced) for RESOLVED/CLOSED transitions.</td></tr> <tr><td>external\_ref</td><td>TEXT</td><td>V2 Zendesk/Linear sync. Nullable at MVP.</td></tr> </tbody></table>

### 3.2 Indexes

- `UNIQUE (org_id, request_id) WHERE request_id IS NOT NULL` — idempotency.
- `(org_id, status, created_at DESC)` — admin list query (per-org filtered).
- `(status, created_at DESC)` — global admin list.

### 3.3 RLS policies

All GUC SET statements use `SET LOCAL` (transaction-scoped) — pgBouncer transaction-mode pooling safe.

- **support\_tickets\_customer\_insert** — FOR INSERT WITH CHECK `(org_id = current_setting('app.current_org_id', true)::uuid)`.
- **support\_tickets\_customer\_select** — FOR SELECT USING `(org_id = current_setting('app.current_org_id', true)::uuid    OR current_setting('app.is_platform_admin', true)::boolean = true)`.
- **support\_tickets\_admin\_all** — FOR ALL USING and WITH CHECK `(current_setting('app.is_platform_admin', true)::boolean = true)`. Same GUC pattern as audit\_log RLS (V51).

 **Customer UPDATE/DELETE immutability:** no UPDATE or DELETE policy for customers. RLS ENABLED with no such policy = deny-by-default. Customers cannot modify or delete submitted tickets.

 **Production code audit (Proveo-confirmed):** `orgTransaction{}` (OrgScopeSessionVariable.kt:131) always wraps `SET LOCAL app.current_org_id` inside `transaction{}`. The Testcontainers test failure (Proveo GAP-1) was caused by the test setup using a PostgreSQL superuser connection — superusers bypass RLS regardless of GUC values. Production code was never buggy.

### 3.4 Status transition machine

<table id="bkmrk-fromallowed-next-sta"> <thead><tr><th>From</th><th>Allowed next states</th></tr></thead> <tbody> <tr><td>OPEN</td><td>TRIAGED, CLOSED</td></tr> <tr><td>TRIAGED</td><td>IN\_PROGRESS, CLOSED</td></tr> <tr><td>IN\_PROGRESS</td><td>RESOLVED, CLOSED</td></tr> <tr><td>RESOLVED</td><td>CLOSED</td></tr> <tr><td>CLOSED</td><td>(no further transitions)</td></tr> </tbody></table>

Invalid transitions return HTTP 422 with `code: "INVALID_TRANSITION"` and `allowedNext`.

### 3.5 context\_bundle allowlist

Allowed keys (server-side enforced, rejection = HTTP 422): `requestId, errorCode, httpStatus, instancePath, orgId, userId,  appRoute, planTier, country, auditRef`. IDs and codes only — never invoice content, names, amounts, or email addresses.

## 4. Diagnostic Join

```
SELECT al.*
FROM audit_log al
JOIN support_tickets st ON al.request_id = st.request_id
WHERE st.id = '<ticket-uuid>';
```

 **Framing (martin-kleppmann dissent):** `request_id` is a *correlation handle for cross-system debugging only* — NOT tamper-evidence. The append-only guarantee for `audit_log` comes from the `block_audit_mutation()` trigger (V51), not from request\_id. Platform-admin direct DB access is outside the threat model of this column.

## 5. Known Gaps and Follow-ups

<table id="bkmrk-itemdetailstatus-ocd"> <thead><tr><th>Item</th><th>Detail</th><th>Status</th></tr></thead> <tbody> <tr> <td>OCD-1: Sentry DSN</td> <td>`bilko-sentry-dsn` / `bilko-web-sentry-dsn` must be provisioned in GCP Secret Manager. Inject via `--update-secrets` (never `--set-env-vars`). Sentry code is fully inert until then.</td> <td>CEO action required. Blocks production deploy; does not block feature branch merge.</td> </tr> <tr> <td>OCD-2: error\_code taxonomy</td> <td>Domain errors currently fall into generic VAL/INFRA codes, making ticket triage partly blind. Domain-specific codes are V2 scope (MC #103333). CEO confirmed proceed with V72 before those codes land.</td> <td>Open CEO decision. V2 follow-on MC #103333.</td> </tr> <tr> <td>OCD-3: merge-order vs #103300</td> <td>V71/V72 migration numbers must be confirmed/renumbered after #103300 merges.</td> <td>Open. Blocking deploy only.</td> </tr> <tr> <td>Positive-path RLS assertion</td> <td>Integration test confirms negative proof (wrong-org INSERT rejected). Positive proof (correct-org INSERT succeeds) not explicitly asserted. Proveo: completeness gap, not safety-weakening gap.</td> <td>Follow-up test enhancement. Non-blocking.</td> </tr> <tr> <td>CI runner quota</td> <td>Tracked as MC #103304.</td> <td>Separate MC.</td> </tr> <tr> <td>Deploy gate</td> <td>Deploy-gated behind MC #103300 prod cutover.</td> <td>Dependent on #103300.</td> </tr> </tbody></table>

## 6. Verification Evidence

- **Proveo P2P Final Verdict: PASS** — commit `6b214a00`, 12/12 AC signals pass, integration test 3/3 PASS (BUILD SUCCESSFUL in 35s, tests="3" failures="0"), unit suite 1280/1280. Evidence: `/tmp/alai/p2p-pairing-evidence/proveo-103323-verdict-final.md`
- **Builder evidence bundle:** `/tmp/evidence-103323/verification.md`
- **PR:** [\#316](https://github.com/johnatbasicas/bilko/pull/316) on branch `feat/103323-backoffice-backend`
- **Integration test XML SHA256:** `941b588f21c8fd735c1b6f7f1b888ea2d2441ec0c5f3a2085bc00489fcc70bf7`
- **File hashes (Proveo):** StatusPages.kt `fca33115361ced358dbdc56a8fd0020bc1212d58758574f540fdc46193287284`; SupportTicketRoutes.kt `730f76a245fb0492f5f94c378e18973242e7e9a0f9c4de5353dc8be268a38b2f`; OrgScopeSessionVariable.kt `2c5c992c92c5f548c22092c171a98fb599760f3ce827d1e72db26d901c0c89f2`

# Bilko Backoffice — Ops Infra (Logging Views + support@ Forwarding + Preflight)

# Bilko Backoffice — Ops Infra (MC #103325)

**Branch:** feat/103325-backoffice-infra | **PR:** #317 | **Proveo verdict:** PASS (2026-06-10) | **Sibling:** [Backoffice Backend MVP (page 3100)](/books/bilko/page/3100)

---

## 1. Cloud Logging Saved Views

**GCP project:** `tribal-sign-487920-k0`  
**Bucket:** `_Default` (global)  
**Verified via:** `gcloud logging views list --bucket=_Default --location=global --project=tribal-sign-487920-k0`

<table id="bkmrk-view-idscope-filteri"><thead><tr><th>View ID</th><th>Scope filter</th><th>Intended use / Log Explorer query to add</th></tr></thead><tbody><tr><td>`bilko-error-by-org`</td><td>`resource.type="cloud_run_revision" AND resource.labels.service_name=~"bilko-api-(demo|stage)"`</td><td>Add query `severity>=ERROR`. Group results by `orgId` (parse via `JSON_EXTRACT(textPayload, "$.orgId")` — orgId lives in textPayload JSON, not jsonPayload).</td></tr><tr><td>`bilko-request-trace`</td><td>`resource.type="cloud_run_revision" AND resource.labels.service_name=~"bilko-(api|web)-(demo|stage)"`</td><td>Add query `logName=~"stdout" OR logName=~"requests"`. Correlate requests end-to-end by `requestId` field in textPayload.</td></tr><tr><td>`bilko-5xx-demo`</td><td>`resource.type="cloud_run_revision" AND resource.labels.service_name=~"bilko-(api|web)-demo"`</td><td>Add query `httpRequest.status>=500`. Scoped to demo environment only.</td></tr></tbody></table>

### GCP constraint — view filter expressiveness

GCP `gcloud logging views create --log-filter` only accepts log source, resource type, appHub fields, user labels, and log ID conditions. Severity comparisons (`severity>=ERROR`) and field comparisons (`httpRequest.status>=500`) are **not valid in view filters** — they must be added as Log Explorer query refinements on top of the saved view scope. This is a documented GCP platform limitation. Each view description in GCP documents this explicitly.

**Log schema note:** Bilko API logs structured data as JSON inside `textPayload` (not `jsonPayload`). The textPayload schema is: `{"requestId":"...","method":"...","path":"...","status":N,"durationMs":N,"userId":"...","orgId":"...","ip":"..."}`. ERROR logs are stack traces in textPayload; `orgId` is present on request-completion log lines, not on exception lines.

---

## 2. support@bilko.cloud Email Forwarding

### MX provider — IMPORTANT

**bilko.cloud MX = Migadu** (`aspmx1.migadu.com` + `aspmx2.migadu.com`, confirmed via `dig MX bilko.cloud`). The **CF Email Routing section in DEPLOY-MAP.md is STALE** and must be corrected — Cloudflare does not handle bilko.cloud email.

### Implemented forwarding

Mail flow: `support@bilko.cloud` (Migadu mailbox, `may_receive=true`, `may_send=false`) → Migadu forwarding → `alem@alai.no`

**Key Migadu design constraint:** Alias `destinations` only accept same-domain addresses — external addresses are silently rewritten to `<localpart>@<same-domain>`. The correct mechanism for external cross-domain delivery is a **forwarding on a mailbox object** (not an alias).

**Implementation steps taken:**

1. Confirmed `GET /v1/domains/bilko.cloud/mailboxes/support/forwardings` — support@ was alias-only (no mailbox).
2. Created `support@bilko.cloud` mailbox: `may_receive=true`, `may_send=false`, IMAP/POP3 disabled (receive-only).
3. Added forwarding via `POST /v1/domains/bilko.cloud/mailboxes/support/forwardings {"address":"alem@alai.no"}` — response: `is_active: true`, `confirmed_at: 2026-06-10T08:17:01Z`, no confirmation email required.
4. Deleted the old support@ alias (superseded by mailbox).
5. Removed investigation-only forwarding from `sales@bilko.cloud` — **sales@ is left untouched** (`forwardings: []`).

**Verified state (Proveo independent GET):**

```
GET /v1/domains/bilko.cloud/mailboxes/support/forwardings
{"forwardings":[{"address":"alem@alai.no","confirmed_at":"2026-06-10T08:17:01Z","blocked_at":null,"is_active":true}]}

GET /v1/domains/bilko.cloud/mailboxes/sales/forwardings
{"forwardings":[]}
```

### Migadu admin path (for future changes)

To modify forwarding: [admin.migadu.com](https://admin.migadu.com) → bilko.cloud → Mailboxes → support → Forwardings. Do **not** use the Aliases section for external cross-domain targets.

---

## 3. Preflight Rollback Script

**File:** `scripts/ops/bilko-support-fix-preflight.sh` (committed at `67ed0ce5`, PR #317, mode `100755`)

### What it does

1. **STEP 1 — Cloud SQL backup** (write, skipped in dry-run): Takes an on-demand Cloud SQL backup of the Bilko DB before any deploy action. Provides a restore point.
2. **STEP 2 — Capture current Cloud Run revision** (read-only always): Records the live revision name and image SHA for both `bilko-api-demo` and `bilko-web-demo`.
3. **STEP 3 — Print rollback commands** (print only, never executes): Outputs the exact `gcloud run services update-traffic` commands needed to roll back to the captured revisions. These are `echo`-wrapped — they are never executed by the script.

### How to run

```
# Dry-run (safe, no writes — use to confirm rollback targets before deploy)
bash scripts/ops/bilko-support-fix-preflight.sh --dry-run

# Live run (takes SQL backup, captures revisions, prints rollback cmds)
bash scripts/ops/bilko-support-fix-preflight.sh
```

### Deploy-fragility rule enforced

All example re-deploy commands in the printed output use `--update-secrets`. The script documents and enforces: **NEVER use `--set-env-vars` for Bilko Cloud Run deploys** — it overwrites the Secret Manager binding and exposes secrets as plaintext environment variables.

---

## 4. Known Follow-up

<table id="bkmrk-itemstatusowner-depl"><thead><tr><th>Item</th><th>Status</th><th>Owner</th></tr></thead><tbody><tr><td>DEPLOY-MAP.md CF Email Routing section for bilko.cloud is stale (lists Cloudflare; MX is Migadu)</td><td>Open</td><td>John / next infra PR</td></tr><tr><td>PR #317 bundles MC #103323 application code (Sentry, SupportTickets, DB migrations V71+V72) — confirm separate QA validation for that scope</td><td>Open (Proveo advisory)</td><td>John</td></tr><tr><td>Merge PR #318 (smoke-test fix) before PR #317, or close #318 as superseded if #317 merges first</td><td>Open</td><td>John</td></tr></tbody></table>

---

*Created by Skillforge for MC #103325. Evidence: /tmp/evidence-103325/verification.md + /tmp/alai/p2p-pairing-evidence/proveo-multi-317-318-verdict.md. All facts machine-verified.*

# MC #105153 — API write bucket stage E2E rate-limit override

# MC #105153 — Bilko API write bucket 429 stage E2E

## Scope
Fix authenticated `api` rate-limit bucket 429s on stage E2E for sentinel contact create and instant-demo write-protection probes, while preserving production defaults.

## Changes verified on disk
- `apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt`
  - Added `apiRateLimitConfig(perMinuteEnv)` parser.
  - `RateLimitName("api")` now uses `API_RATE_LIMIT_PER_MINUTE` with default `100`.
- `apps/api/src/test/kotlin/no/alai/bilko/plugins/RateLimitTest.kt`
  - Added tests for default `100`, stage override `500`, invalid fallback `100`.
- `azure-pipelines.yml`
  - Stage ACA deploy sets `API_RATE_LIMIT_PER_MINUTE=500` for `bilko-api-stage`.
- `.github/workflows/azure-stage.yml`
  - Stage ACA deploy sets `API_RATE_LIMIT_PER_MINUTE=500`.
- `.env.example`
  - Documents `API_RATE_LIMIT_PER_MINUTE`, `AUTH_RATE_LIMIT_PER_MINUTE`, `DEMO_RATE_LIMIT_PER_MINUTE`, `DEMO_RATE_LIMIT_PER_HOUR` defaults.
- `infrastructure/gcp/ENV-MATRIX.md`
  - Documents stage-only `API_RATE_LIMIT_PER_MINUTE=500`; demo/prod default/absent.

## Verification — 2026-07-09T23:07Z
Command run from `apps/api`:

```bash
./gradlew test --tests no.alai.bilko.plugins.RateLimitTest --rerun-tasks
```

Result: `BUILD SUCCESSFUL in 21s`, `7 actionable tasks: 7 executed`.

JUnit XML evidence:

```text
apps/api/build/test-results/test/TEST-no.alai.bilko.plugins.RateLimitTest.xml
<testsuite name="no.alai.bilko.plugins.RateLimitTest" tests="22" skipped="0" failures="0" errors="0" ...>
```

Static presence check:

```bash
grep -n "API_RATE_LIMIT_PER_MINUTE" \
  apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt \
  apps/api/src/test/kotlin/no/alai/bilko/plugins/RateLimitTest.kt \
  azure-pipelines.yml .github/workflows/azure-stage.yml \
  infrastructure/gcp/ENV-MATRIX.md .env.example
```

Observed paths/lines:
- `apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt:115`
- `apps/api/src/main/kotlin/no/alai/bilko/plugins/RateLimit.kt:125`
- `azure-pipelines.yml:527`
- `.github/workflows/azure-stage.yml:181`
- `infrastructure/gcp/ENV-MATRIX.md:36`
- `.env.example:70`

## Production-safety note
Production default remains unchanged: absent/invalid `API_RATE_LIMIT_PER_MINUTE` resolves to `100`, and only stage deploy config sets `API_RATE_LIMIT_PER_MINUTE=500`.


## P2P pre-verifier
- PASS: mesh-thr-77df628b-c7b4-4c5e-a3ff-1825826bb7f9 / mesh-msg-f642fa23-cb94-476f-af4b-01a63f386be2
- Evidence JSON: /tmp/alai/company-mesh-auto-responder/2026-07-09T23-10-17-743Z-mesh-msg-f1bd06a1-d6a6-4b9d-84b8-e60304ecfc39.json

# Bilko sveRačun Faza 1 — TEST-live aktivacija na demou (MC #103450, 2026-07-11)

# Bilko sveRačun Faza 1 — TEST-live aktivacija na demou (MC #103450, 2026-07-11)

## Šta je urađeno
HR eRačun (sveRačun/PostLink) submit **aktiviran na demou** (app.bilko.cloud / app-api.bilko.cloud, ACA `bilko-api-demo`, rg-bilko-demo) protiv **TEST sandboxa** `test.sveracun.hr` — bez stvarne fiskalizacije, reverzibilno.

## Stanje (tool-verifikovano)
- **Env** (`bilko-api-demo` rev 0000043): `SVERACUN_HR_LIVE=true`, `SVERACUN_API_KEY=secretref:sveracun-api-key`, `SVERACUN_SENDER_VAT=HR91276104352`, `DEMO_MODE=true`. `SVERACUN_BASE_URL` nije postavljen — kod defaultira na TEST (`SveRacunHrEInvoiceAdapter.kt:70`), a per-profile `api_base_url` u DB je također TEST.
- **DB flip** (bilko-demo-pg, `hr_einvoice_issuer_config`): `enabled=true` za tačno 2 test org-a — HR demo org `00000000-0000-0029-c000-000000000001` + E2E org `1f9811d2-af38-482d-91d9-229e1acbb37e` (2026-07-11 16:33 UTC). Tabela ima SAMO ta 2 reda — nijedan customer org.
- **Gate semantika**: submit path gate-uje `hr_einvoice_issuer_config.enabled` (`IssuerProfileRepository.kt`) + `SVERACUN_HR_LIVE` env. `adapter_config` tabela gate-uje samo Storecove (mrtav put). Stale GCP secret-ref u DB redovima je inertan (`EnvVarSecretManagerClient` parsira samo ime → env var).

## Proveo E2E — PASS (2026-07-11)
- Login CIAM E2E user → org 1f9811d2; faktura INV-2026-054 → `POST /invoices/{id}/submit-to-sveracun` → **HTTP 200, documentId 6a527117f0525843a744646a, fiscalNumber 2026-000002, SUBMITTED**.
- `GET /invoices/{id}/sveracun-status` + `POST poll-sveracun-status` → PENDING, lastPolledAt se osvježava (živi sandbox roundtrip).
- UI dugme "Pošalji eRačun" (`sveracun-submit-button`) postoji i wired.
- Evidence: `/tmp/evidence-103450/` (screenshotovi, HTTP odgovori, pre/post SELECT, verdict.json).

## Rollback
```sql
UPDATE hr_einvoice_issuer_config SET enabled=false
WHERE org_id IN ('00000000-0000-0029-c000-000000000001','1f9811d2-af38-482d-91d9-229e1acbb37e');
```
(+ opcionalno `SVERACUN_HR_LIVE=false` na bilko-api-demo za potpuni STUB.)

## Nalazi / follow-up
- **MC #105259**: invoice GET/list serializer ne vraća `sveracunDocumentId/InternalStatus/ExternalStatus` → UI kartica pokazuje "not submitted" nakon uspješnog submita; dugme ostaje klikabilno (backend B4 idempotency brani dupli submit, UX vara). + E2E fixture treba kompletnu HR adresu.
- **CI napomena**: main-branch Bilko-CI-CD buildovi padaju na E2E UAT stage-u (poznati rep E2E sprinta 2026-07-10, 7 failova) — nevezano za ovu aktivaciju (nije bilo deploya).
- Prod aktivacija (stvarna fiskalizacija) = zaseban track #103443: FINA cert + prod secret + `SVERACUN_HR_LIVE=true` deploy odluka CEO.

## Kontekst lanca
#104171 (Azure secret+env+runbook) → #104172 (arhiva Azure Blob) → #103452 (submit path + B4 idempotency) → **#103450 (ovo)** → #103443 (live aktivacija, parent).

# Bilko — Minimax Gap Program (2026-07-20)

## 1. CEO odluka i kontekst

**Datum CEO odluke:** 2026-07-20 — "Sve odobreno" (svih 6 stavki gap analize).

**Povod:** CEO ask — "vidi jesmo li pokrili sve iz Minimax dokumentacije; eRačun = top seller; storno; plate/odsustva; mob slikanje računa; mob time tracking."

**Metod:** 4 paralelna read-only istraživačka agenta nad kanonskim Bilko repo-om (John, 2026-07-20), rezultat u `docs/product/MINIMAX-GAP-ANALIZA-2026-07-20.md`.

**Izvorna dokumentacija (Minimax-derived):** `docs/product/MINIMAX-MIGRATION-UX-BRIEF.md` (P0/P1/P2 backlog), `docs/product/BILKO-HR-KNOWLEDGE-ARTICLE-PLAN.md` + 33 draft članka, `docs/COMPETITIVE-RESEARCH.md` §2.8.

**Program parent:** MC #106102 — "PROGRAM parent — 6 CEO-odobrenih stavki gap analize 2026-07-20".

**Red-zone stavke (iz MC #106102):** storno (novac/zakon), eRačun prod send (fiskalno).

**Phase-gate napomena (ključno — vidi §5):** spec taskovi (#106108 payroll odsustva, #106110 time tracking, #106111 migracioni wizard) čekaju CEO potvrdu prije nego se otvore build taskovi. Nijedan od ta tri specA nije autorizovan za gradnju — samo dizajn/plan.

## 2. MC taskovi programa — status i vlasnik

<table id="bkmrk-mc-%23naslovprioritets"><thead><tr><th>MC #</th><th>Naslov</th><th>Prioritet</th><th>Status</th><th>Vlasnik</th><th>Napomena</th></tr></thead><tbody><tr><td>\#106102</td><td>PROGRAM parent — 6 CEO-odobrenih stavki</td><td>H</td><td>in\_progress</td><td>john</td><td>Parent svih ispod</td></tr><tr><td>\#106103</td><td>Storno eRačuna dovršetak: SefHttpClient.cancelInvoice (RS), HR FISK storno submission, skini PRO+ gate sa INVOICE\_CREDIT\_NOTE</td><td>H</td><td>in\_progress</td><td>john</td><td>🔴 Red-zone (novac/zakon)</td></tr><tr><td>\#106104</td><td>Storno E2E validacija na write org-u (HR-603 MEDIUM #6)</td><td>H</td><td>paused</td><td>john</td><td>QA za #106103</td></tr><tr><td>\#106105</td><td>eRačun top-seller put A: sveRačun/FINA produkcijski provider onboarding</td><td>H</td><td>open</td><td>john</td><td>🔴 Red-zone (fiskalno)</td></tr><tr><td>\#106106</td><td>eRačun top-seller put B: send kao primarni CTA u invoice flowu (web)</td><td>M</td><td>paused</td><td>john</td><td>Gated na #106105 aktivaciju</td></tr><tr><td>\#106107</td><td>eRačun top-seller put C: hero/landing poruka redesign</td><td>M</td><td>paused</td><td>john</td><td>NAKON prod aktivacije</td></tr><tr><td>\#106108</td><td>Payroll odsustva SPEC (godišnji/bolovanje/dopust) + JOPPD</td><td>M</td><td>paused</td><td>john</td><td>SPEC gotov — čeka CEO potvrdu za build</td></tr><tr><td>\#106109</td><td>Mobile capture fix paket (crop, OCR confidence, multi-page, telemetrija)</td><td>M</td><td>paused</td><td>john</td><td>Nalaz gotov, build task nije otvoren</td></tr><tr><td>\#106110</td><td>Time tracking SPEC (Tripletex parity)</td><td>M</td><td>paused</td><td>john</td><td>SPEC gotov — čeka CEO potvrdu za build</td></tr><tr><td>\#106111</td><td>Migracioni wizard MIG-01→08 SPEC + fazni plan</td><td>M</td><td>paused</td><td>john</td><td>SPEC gotov — čeka CEO potvrdu za build</td></tr><tr><td>\#106113</td><td>BookStack dokumentacija programa (ova stranica)</td><td>M</td><td>in\_progress→done</td><td>skillforge</td><td>Docs task (ZAKON PLAN)</td></tr></tbody></table>

*Napomena: MC #106112 (RAG-first hook Phase A recorder) i #106114 (alai-hooks CLI dispatch mismatch) su susjedni ID-jevi ali NISU dio Minimax Gap programa — to su odvojeni SYSTEM taskovi (hook infrastruktura), owner john, nepovezani s Bilko gap analizom.*

## 3. Sažetak nalaza po stavci

### 3.1 eRačun pozicioniranje — "jednostavno slanje eRačuna" kao top-seller

**Nalaz: NIJE trenutna poruka.** Hero na landing-hr je "Računovodstvo koje razumije hrvatski biznis" + "Jednostavnije nego Minimax. Jeftinije nego Pantheon." + €15/mj. eRačun je svuda označen "FISK 2026 u razvoju · Beta".

**Zašto:** produkcijsko slanje NIJE aktivno bez provider onboardinga (sveRačun aktiviran samo za 2 test org-a). U web appu "Pošalji" = email; eRačun kartica je sekundarna sekcija, ne prvi korak.

**Zaključak:** poruka zahtijeva (a) produkcijsku aktivaciju providera, (b) send kao primarni CTA, (c) redizajn hero poruke — sekvencionisano kao putevi A/B/C (#106105/#106106/#106107). Trenutni copy je pošten (SAFE-02 guardrail), ali ne prodaje ono što CEO hoće da bude top-seller. Poznati srodni blokator: rabat/AllowanceCharge gap u HR eRačun XML (nalaz #105269, čl.79 ZPDV).

### 3.2 Storno poslanog eRačuna — parcijalno

**Nalaz: PARCIJALNO.** Kreditno odobrenje (storno) postoji lokalno: `POST /invoices/credit-note`, tip `CREDIT_NOTE`, GL reversal, UBL tip 381 + BT-25/BT-26, CN-YYYY-NNN numeracija, UI dugme. HR eRačun servis MOŽE serializovati i poslati credit note kroz sveRačun.

**Rupe:**

- RS/SEF: `SefHttpClient.cancelInvoice()` NE postoji — storno se ne šalje na SEF.
- HR FISK B2C storno endpoint nije implementiran (samo metadata lista tipova).
- BA: nema credit-note podrške uopšte.
- E2E neverifikovano uživo (HR-603 MEDIUM #6).
- Feature gate `INVOICE_CREDIT_NOTE` = PRO+ tier — a storno je zakonska korekcija, ne premium feature (MC #103516 odluka visi).

🔴 Red-zone: novac + zakonska obaveza. Build task #106103, validacija #106104.

### 3.3 Plate — odmor/permisija/bolovanje — ne postoji

**Nalaz: NE POSTOJI.** Payroll MVP (V101) = registar zaposlenika + bruto→neto HR obračun + payslips. Nula podrške za odsustva: nema absence tabela, nema tipova, nema leave balansa, payslip uvijek uzima puni `gross_salary_cents`, nema JOPPD šifri za vrste odsustva. Nije ni u PAYROLL-PHASE2-PLAN.md. MARKET-READINESS-MATRIX: Payroll = NOT\_IMPLEMENTED za GA.

**Reuse mogućnost (istraženo u SPEC-u #106108):** `@alai/hr-core` extraction bio je planiran ali nikad izvršen do integracije — skeleton postoji (jedan commit, 2026-04-18), Bilko i Lobby ga nikad nisu wire-ovali. Reuse verdikt: **DJELIMIČNO** — shema/table skeleton (`LeaveTables.kt`) je reusable kao dizajn-referenca, servisna logika mora biti napisana od nule, direktno u Bilko-vom postojećem obrascu (isto kao što je payroll MVP sam građen bez hr-core).

### 3.4 Mobilna — slikanje računa — postoji sa slabostima

**Nalaz: POSTOJI ali sa konkretnim slabostima** (`apps/mobile/app/(tabs)/expenses/index.tsx`, RN+Expo v0.1.5):

- document-scanner plugin → fallback na običnu kameru sa `allowsEditing: false` (nema crop/ispravke)
- `maxNumDocuments: 1` — nema višestraničnih računa
- kompresija 0.82 prije uploada — gubi sitni tekst prije OCR-a
- OCR confidence se vraća ali se NE provjerava u UI (niska pouzdanost prolazi bez upozorenja)
- nema on-device pregleda ekstrakta prije uploada
- fallback na običnu kameru je tih (nema telemetrije koliko često scanner ne radi)

Ovo objašnjava CEO utisak "slikanje računa nije dobro". Fix paket praćen u #106109.

### 3.5 Mobilna — time tracking (Tripletex paritet) — ne postoji

**Nalaz: NE POSTOJI.** Ni u mobile appu, ni u backendu (nema time-entry ruta/tabela), ni u MOBILE-PRD fazama 0-5. Jedino srodno: putni nalozi (web, HR). COMPETITIVE-RESEARCH bilježi Tripletex time tracking kao add-on NOK 59/user/mj.

**SPEC verdikt (#106110):** Lobby `TimerService.kt`/`TimerRepository.kt`/`TimerRoutes.kt` su 85-90% portable (generic clock-in/out), ali NE pokrivaju billable-time-to-invoice linkage niti approval workflow (to dolazi iz Bilko-ovog vlastitog TravelOrder RBAC obrasca). Revidirana procjena: **~3.5-4.5 sedmica** (full-stack: backend+mobile+web+pricing gate), veće od originalne grube procjene 2-3 sedmice. Preporučeni pricing: flat org-wide add-on (ne per-user metered), usklađeno sa Bilko per-org filozofijom.

### 3.6 Migracioni wizard (MIG-01→08) — nula koda specifično za wizard

**Nalaz iz gap analize:** MIG-01→08 dokumentovano detaljno, nula koda u web appu; landing "Prebaci s Minimaxa" obećava "kontrolnu listu i pregled podataka". Takođe nedostaju: ONB-01→04 (onboarding), R1-01→05 (račun na firmu tok), MOB-01/03 (Today tab), 33 HR knowledge članka (svi draft), Minimax OIB→registar auto-fill pattern.

**SPEC korekcija (#106111, važno):** tvrdnja "nula koda" NIJE tačna za backend infrastrukturu uvoza općenito — postoji direktno primjenjiv arhitekturni template (`V87__gl_bank_import.sql` staging+idempotency+RLS obrazac, `GlBankService.kt`, `InboxRoutes.kt` accept/reject tok, `SupportTicketRoutes.kt` queue backbone). Zaključak: MVP je ~70% re-orkestracija postojećih patterna za generic CSV, ne gradnja od nule. Landing copy je već usklađen sa SAFE-01/02 (ne laže eksplicitno, ali gap je i dalje realan — nema in-app checklist/upload/wizard).

**Preporučeni MVP rez (F1):** MIG-01+02+03 (upload u support queue, BEZ auto-parse)+MIG-07 (support queue)+SAFE-01/02/03, procjena M (~1 sprint). Puni wizard sa mapping-preview/auto-validacijom (F2/F3) zahtijeva security review (malware scan, CSV injection) i čeka odgovor na Open Question #1 (koji export formati su stvarno dostupni).

## 4. Linkovi i putanje na specove i evidence

<table id="bkmrk-dokumentputanja-glav"><thead><tr><th>Dokument</th><th>Putanja</th></tr></thead><tbody><tr><td>Glavna gap analiza (6 stavki)</td><td>`~/business/ALAI-Holding-AS/products/Bilko/docs/product/MINIMAX-GAP-ANALIZA-2026-07-20.md`</td></tr><tr><td>Payroll odsustva spec (#106108, CodeCraft/Petter Graff)</td><td>`~/system/evidence/106108/PAYROLL-ABSENCE-SPEC.md`</td></tr><tr><td>Time tracking spec (#106110, Skybound)</td><td>`~/system/evidence/106110/TIME-TRACKING-SPEC.md`</td></tr><tr><td>Migracioni wizard plan (#106111)</td><td>`~/system/evidence/106111/MIGRATION-WIZARD-PLAN.md`</td></tr><tr><td>Izvorna Minimax UX brief (MIG/ONB/SAFE kodovi)</td><td>`docs/product/MINIMAX-MIGRATION-UX-BRIEF.md` (Bilko repo)</td></tr><tr><td>HR knowledge article plan</td><td>`docs/product/BILKO-HR-KNOWLEDGE-ARTICLE-PLAN.md` (Bilko repo)</td></tr><tr><td>Competitive research (Tripletex time tracking add-on)</td><td>`docs/COMPETITIVE-RESEARCH.md` §2.8 (Bilko repo)</td></tr><tr><td>Payroll Phase 2 plan (JOPPD, RS/BA sekvencioniranje)</td><td>`docs/regulatory/PAYROLL-PHASE2-PLAN.md` (Bilko repo)</td></tr><tr><td>hr-core extraction plan (Sprint 0, orphaned)</td><td>`docs/research/hr-core-extraction-plan.md` (Bilko repo)</td></tr></tbody></table>

## 5. Phase-gate napomena

**Specovi #106108 (payroll odsustva), #106110 (time tracking) i #106111 (migracioni wizard) su SPEC/PLAN ONLY — nema koda, nema build taskova otvorenih.** Sva tri dokumenta eksplicitno navode na vrhu: "Status: SPEC ONLY, no code, no build tasks created" / "PHASE-GATE: ovaj dokument ide CEO-u na potvrdu prije nego se otvori bilo koji build task."

Prije otvaranja build taskova za bilo koju od te tri stavke, potrebno je:

- **Payroll odsustva (#106108):** CEO odluka o self-service RBAC (ko označava odsustvo), approval workflow (da/ne), i \[TREBA HR POREZNA VALIDACIJA\] na svim statutarnim brojkama (dani bolovanja na teret poslodavca, HZZO split, carryover pravila) prije F2 (uticaj na obračun). F1 (samo evidencija, bez uticaja na obračun) može ići provizorno.
- **Time tracking (#106110):** CEO sign-off na pricing model (flat add-on vs per-user metered) i potvrda RBAC gap-a (novi "staff" role).
- **Migracioni wizard (#106111):** CEO potvrda na F1 rez (support-led, bez auto-parse) i odgovor na Open Question #1 (stvarni Minimax export format) prije F2 investicije u parser.

Storno (#106103/#106104) i eRačun top-seller put A (#106105) su VEĆ u toku (in\_progress/open) jer su H-prioritet red-zone stavke koje CEO odluka od 20.07 direktno pokriva bez dodatnog spec-gate koraka — ali ostaju pod istim red-zone verifikacionim standardom (adversarial review + Proveo E2E prije done).

## Povezani dokumenti

Vidi takoder istrazivanje drzavnih poticaja i uslova zaposljavanja za novi HR d.o.o. entitet (Smart Forge): [Smart Forge (HR) — drzavni poticaji i zaposljavanje — istrazivanje 2026-07-21](https://docs.alai.no/books/bilko/page/smart-forge-hr-drzavni-poticaji-i-zaposljavanje-istrazivanje-2026-07-21) (odvojena tema, isti book).

# Smart Forge (HR) — državni poticaji i zapošljavanje — istraživanje 2026-07-21

## 1. Kontekst

**Smart Forge** = novi d.o.o. u registraciji (~25.07.2026), tech/SaaS (NKD J62 racunalno programiranje), vlasnistvo ALAI grupa/Alem. Ovo je novi hrvatski (HR) entitet za Bilko.

Sve nize je web-verifikovano 21.07.2026, izvori navedeni na dnu stranice (sekcija 6).

*Agent verdikt izvornog istrazivanja: PASS (web-verifikovano 2026-07-21). Izvor: `~/system/evidence/hr-poticaji-smartforge/RESEARCH-2026-07-21.md`*

## 2. Programi — MOZEMO / NE MOZEMO

### MOZEMO aplicirati / koristiti

<table id="bkmrk-%23programiznos-%2F-uslo"><thead><tr><th>\#</th><th>Program</th><th>Iznos / uslov</th><th>Napomena</th></tr></thead><tbody><tr><td>1</td><td>HAMAG-BICRO Program akceleracije</td><td>~3M EUR bespovratnih za inovativna poduzeca</td><td>KANDIDAT #1 (Bilko = AI/SaaS inovacija). Pratiti rok.</td></tr><tr><td>2</td><td>HAMAG-BICRO zajmovi za pocetnike</td><td>25.001–100.000 EUR @ 0,5% fiksno, 10 god., pocek 12 mj.</td><td>TRENUTNO ZATVOREN (utrosena sredstva) — pratiti novi krug (program do 2029). Uslov: vlasnik/suvlasnik zaposlen puno radno vrijeme u firmi (strukturna odluka!).</td></tr><tr><td>3</td><td>Jamstva kroz banke (HAMAG + EU)</td><td>do 300.000 EUR</td><td>Novi krug otvoren.</td></tr><tr><td>4</td><td>Podrska digitalizaciji MSP-ova</td><td>do 120.000 EUR (50–85% sufinanciranje)</td><td>Sjediste mora biti NUTS2 Panonska/Sjeverna/Jadranska Hrvatska — **GRAD ZAGREB ISKLJUCEN**. Strateski: registracija Smart Forge van Zagreba (npr. Rijeka/Osijek gdje su pilot partneri Terra/Maricic, ili Dubrovnik — vidi sekciju 3) otvara ovaj poziv.</td></tr><tr><td>5</td><td>Porezno automatski (bez posebne prijave)</td><td>Novoosnovani d.o.o. NE placa akontacije poreza na dobit prvu godinu; porez na dobit 10% (&lt;1M EUR prihoda); oslobodjenje doprinosa 1 god. za osobu koja se PRVI PUT zaposljava (neodredjeno); HZZ potpore za zaposljavanje/pripravnistvo</td><td>Automatsko pravo, nije natjecaj.</td></tr></tbody></table>

### NE prolazimo (sada)

<table id="bkmrk-%23programiznos-%2F-uslo-1"><thead><tr><th>\#</th><th>Program</th><th>Iznos / uslov</th><th>Zasto ne prolazimo</th></tr></thead><tbody><tr><td>6</td><td>HZZ potpora za samozaposljavanje</td><td>do 15.000 + 5.000 digitalno/zeleno + 7.000 povratnici = do 27.000 EUR; rok 30.09.2026</td><td>Trazi NEZAPOSLENU osobu u HZZ evidenciji koja osniva firmu. Alem nije u HZZ evidenciji. (Opcija bi postojala samo ako bi HR suosnivac iz HZZ evidencije osnivao — nije nasa postavka.)</td></tr><tr><td>7</td><td>Digitalni vauceri 2026</td><td>fond 3,92M EUR, otvaranje ~rujan</td><td>Min 5 zaposlenih + pozitivna EBITDA — nova firma ne prolazi jos.</td></tr><tr><td>8</td><td>Start-up/spin-off mladih istrazivaca</td><td>66k–200k EUR</td><td>Prozor prijava 15.04–15.06.2026 PROSAO; eventualno 2027.</td></tr></tbody></table>

### Napomene o podobnosti

- Strano vlasnistvo: nije prepreka za EU/HR MSP programe (bitno: HR registracija + MSP status). ALI MSP status se racuna S POVEZANIM poduzecima (ALAI grupa) — grupa je mala pa MSP status ostaje, dokumentovati strukturu pri prijavi.
- Iskljucene djelatnosti u zajmovima: trgovina, FINANCIJSKE djelatnosti, kripto... — Bilko/Smart Forge je SOFTVER (J62), NE financijska djelatnost — prolazi, ali paziti na NKD izbor pri registraciji.
- \[TREBA POTVRDA konzultanta/HAMAG\] tacni uvjeti povezanosti i da li holding struktura utice na "pocetnik" status.

## 3. Dubrovnik / NUTS2 nalaz (dopuna, CEO pitanje 21.07)

**Dubrovacko-neretvanska zupanija = NUTS2 JADRANSKA Hrvatska → KVALIFIKUJE se za MSP digitalizacijski poziv (do 120k EUR, 50–85%). Jedini iskljucen je Grad Zagreb.**

- Svi nacionalni programi (HAMAG akceleracija, zajmovi, jamstva, porezne olaksice) vaze jednako — sjediste irelevantno za njih.
- Lokalno/zupanijski (skromno): DNZ program razvoja MSP 2025-2027 (npr. Aktivnost 4: sufinanciranje izrade poslovnog plana 50% do 1.000 EUR); Grad Dubrovnik potpore fokusirane na tradicijske obrte (nisu IT fit). DUNEA (regionalna agencija) = kanal za info/EU projekte.
- Napomena: Dubrovnik je RAZVIJENO podrucje (visok indeks razvijenosti) — nema uvecanih intenziteta potpore kakve dobijaju potpomognuta podrucja, ali NEMA nikakvog iskljucenja.
- **Zakljucak: Dubrovnik radi jednako dobro kao Rijeka/Osijek za sve bitne programe; razlika je samo u sitnim lokalnim mjerama.**

## 4. Zaposljavanje — EGP drzavljani i Djani scenariji

Web-verifikovano (hzz.hr sluzbeno):

- NE MORA biti hrvatski drzavljanin. Drzavljani EU/EGP (**NORVESKA JE EGP**) su po zakonu IZJEDNACENI s hrvatskim drzavljanima za upis u HZZ evidenciju nezaposlenih — uslov je boraviste/prebivaliste u HR (prijava po mjestu boravista).
- Drzavljani trecih zemalja (npr. samo BiH pasos): upis u evidenciju moguc SAMO ako mogu raditi u HR bez dozvole za boravak i rad; inace ide sustav dozvola (test trzista rada).
- Kad je osoba u HZZ evidenciji, poslodavac (Smart Forge) moze koristiti HZZ potpore za zaposljavanje/pripravnistvo za tu osobu.
- **BONUS "Biram Hrvatsku":** povratak iz EGP zemalja (ukljucujuci Norvesku) nosi fiksnih 7.000 EUR (povratak iz inostranstva) uz samozaposljavanje/mobilnost.
- Doprinos-olaksica "prvo zaposljavanje" (1 god. oslobodjenja doprinosa na osnovicu, neodredjeno): vezana za prvo zaposlenje — osoba bez ranijeg HR staza vjerovatno kvalifikuje \[TREBA POTVRDA racunovodje: da li se gleda samo HR staz ili i inostrani\].

### Djani scenario (dolazak iz Norveske, EGP pasos) — putevi

**REDOSLIJED JE KRITICAN: boraviste → HZZ evidencija → tek onda zaposlenje/osnivanje. Pogresan redoslijed = gubitak prava.**

<table id="bkmrk-scenarioputiznosi-%2F-"><thead><tr><th>Scenario</th><th>Put</th><th>Iznosi / uslovi</th></tr></thead><tbody><tr><td>**A) ZAPOSLENIK** u Smart Forge</td><td>prijavi boraviste u HR → upis u HZZ evidenciju → Smart Forge ga zaposli uz HZZ potporu za zaposljavanje + moguca doprinos-olaksica</td><td>Jednostavnije, manji iznosi.</td></tr><tr><td>**B) SUVLASNIK/OSNIVAC** koji se samozaposljava</td><td>potpora za samozaposljavanje (do 15k+5k digitalno/zeleno) + 7.000 EUR povratnik iz EGP; ISTOVREMENO zadovoljava HAMAG uslov "vlasnik/suvlasnik zaposlen puno radno vrijeme" za pocetnicki zajam 0,5%</td><td>Potencijalno do ~27.000 EUR. \[TREBA PROVJERA\]: uvjeti vlasnickog udjela za potporu za samozaposljavanje + TIMING (osoba mora biti u HZZ evidenciji PRIJE osnivanja/zaposlenja — Smart Forge je vec u registraciji, mozda treba drugi subjekt ili naknadni ulazak — konsultovati HZZ/konzultanta PRIJE poteza).</td></tr></tbody></table>

## 5. \[TREBA PROVJERA\] — otvorene stavke

- Tacni uvjeti povezanosti (ALAI grupa) i da li holding struktura utice na "pocetnik" status — \[TREBA POTVRDA konzultanta/HAMAG\].
- Doprinos-olaksica "prvo zaposlenje": da li se gleda samo HR radni staz ili i inostrani — \[TREBA POTVRDA racunovodje\].
- Scenario B (Djani suvlasnik): uvjeti vlasnickog udjela za potporu za samozaposljavanje + tacan TIMING (HZZ evidencija PRIJE osnivanja/zaposlenja) — \[TREBA PROVJERA HZZ/konzultanta prije poteza\].

## 6. Izvori

**Osnovno istrazivanje (fetch 2026-07-21):**

- [hamagbicro.hr — Program akceleracije (~3M EUR)](https://hamagbicro.hr/gotovo-3-milijuna-eura-bespovratnih-sredstava-za-inovativna-poduzeca-kroz-program-akceleracije/)
- [hamagbicro.hr — novi krug jamstava do 300.000 EUR](https://hamagbicro.hr/hamag-bicro-i-banke-otvaraju-novi-krug-financiranja-za-male-poduzetnike-do-300-tisuca-eura-uz-jamstvo-eu/)
- [eufondovi.hr — mali zajmovi za pocetnike](https://eufondovi.hr/hamag-bicro-mali-zajmovi-za-zene-poduzetnice-i-poduzetnike-pocetnike/)
- [mjere.hzz.hr — Potpore za samozaposljavanje (PDF)](https://mjere.hzz.hr/app/uploads/2026/01/5.-Potpore-za-samozaposljavanje.pdf)
- [fiskai.hr — vodic potpora za samozaposljavanje](https://www.fiskai.hr/vodic/potpora-samozaposljavanje/)
- [fiskai.hr — vodic porez na dobit](https://www.fiskai.hr/vodic/porez-na-dobit/)
- [esavjetovanja.gov.hr — digitalizacija MSP (NUTS2)](https://esavjetovanja.gov.hr/ECon/MainScreen?entityId=29620)
- [eurokonzalting.com — pregled najavljenih natjecaja 2026](https://eurokonzalting.com/index.php/bespovratna-sredstva-i-krediti/item/1176-pregled-najavljenih-natjecaja-i-bespovratnih-sredstva-za-poduzetnike-u-2026-godini)
- [mojafirma.hr — olaksice i potpore za zaposljavanje 2026](https://mojafirma.hr/clanci/olaksice-i-potpore-za-zaposljavanje-2026)

**Dopuna — Dubrovnik/NUTS2:**

- [glasgrada.hr — DNZ program potpore](https://glasgrada.hr)
- [dubrovnik.hr — bespovratne potpore](https://dubrovnik.hr/za-gradane/bespovratne-potpore-45)
- [likemetkovic.hr — DNZ javni poziv](https://likemetkovic.hr)
- [mjere.hr — potpore Dubrovacko-neretvanska zupanija](https://mjere.hr/potpore/dubrovacko-neretvanska-zupanija)

**Dopuna — zaposljavanje stranaca (Djani scenario):**

- [hzz.hr — prijava stranih drzavljana u evidenciju nezaposlenih](https://hzz.hr/usluge/prijava-stranih-drzavljanina-u-evidenciju-nezaposlenih-hzz-a/)
- [newbeta.hzz.hr — drzavljani EU/EGP](https://newbeta.hzz.hr/nezaposleni/strani-drzavljani/drzavljani-euegp/)
- [mjere.hzz.hr — Biram Hrvatsku (mobilnost radne snage 2026)](https://mjere.hzz.hr/mjere/biram-hrvatsku-mobilnost-radne-snage-2026/)
- [mjere.hzz.hr — Potpore za samozaposljavanje 2026](https://mjere.hzz.hr/mjere/potpore-za-samozaposljavanje-2026/)

## 7. Preporuceni koraci

1. ODLUKA PRIJE REGISTRACIJE: sjediste van Zagreba (Rijeka/Osijek ili Dubrovnik — svi rade jednako dobro za NUTS2 poziv) ako zelimo MSP digitalizacijski poziv do 120k EUR.
2. NKD: J62.01 racunalno programiranje.
3. Prijava na HAMAG mailing/pracenje: novi krug pocetnickih zajmova + Program akceleracije rok.
4. Kod prvog zaposljavanja: iskoristiti oslobodjenje doprinosa (prvo zaposlenje) + HZZ potporu.
5. Za vece bespovratne (2027): start-up istrazivaci prozor april–jun.
6. Ako se razmatra Djani scenario: prije bilo kojeg poteza konsultovati HZZ/konzultanta oko TIMING-a (evidencija prije osnivanja/zaposlenja) i uvjeta vlasnickog udjela za scenario B.

---

*Evidencija: `~/system/evidence/hr-poticaji-smartforge/RESEARCH-2026-07-21.md` (ukljucuje dopunu 1 — Dubrovnik scenario, i dopunu 2 — zaposljavanje stranaca/Djani). Objavljeno: Skillforge, 2026-07-21.*

# Bilko Operational Runbook — Azure Container Apps

# Bilko — Operational Runbook

> **Azure is canonical.** Bilko runs on **Azure Container Apps** (swedencentral, resource group
> `rg-bilko-demo`, subscription `5b0b4d9b`). The previous non-Azure environment this
> runbook described is **dead** — legacy billing was exhausted 2026-06-14 (MC #103633).
> Platform-specific legacy references have been removed below.
>
> **Deep architecture reference (source of truth for topology/diagrams):** `DEPLOY-MAP.md` (root)
> **Environment model:** `docs/infrastructure/ENVIRONMENTS.md`
> **Live CI/CD config:** `azure-pipelines.yml` (root)

**Version:** 4.0 (Azure Container Apps era — B4 doc cleanup, MC #101088)
**Last updated:** 2026-07-28
**Supersedes:** v3.1 (2026-04-30, pre-Azure era)

---

## Table of Contents

1. [System Overview](#1-system-overview)
2. [Pre-Deployment Checklist](#2-pre-deployment-checklist)
3. [Deploying the API](#3-deploying-the-api)
4. [Deploying the Web Frontend](#4-deploying-the-web-frontend)
5. [Database Operations](#5-database-operations)
6. [Rollback Procedures](#6-rollback-procedures)
7. [Database Server Operations (Azure PostgreSQL)](#7-database-server-operations-azure-postgresql)
8. [Troubleshooting Guide](#8-troubleshooting-guide)
9. [Monitoring and Alerts](#9-monitoring-and-alerts)
10. [Emergency Contacts and Escalation](#10-emergency-contacts-and-escalation)

---

## 1. System Overview

### Architecture

Bilko is a Turborepo monorepo with two deployed apps, both running as **Azure Container Apps**:

- **Web** (`apps/web/`) — Next.js 15 frontend, ACA app `bilko-web-demo` (+ `bilko-web-stage`)
- **API** (`apps/api/`) — Kotlin/Ktor + Exposed backend, ACA app `bilko-api-demo` (+ `bilko-api-stage`). This is the sole canonical backend (ADR-020/ADR-021) — the old Express `apps/api-express/` was deleted 2026-05-02 (MC #10493).
- **Database** — Azure PostgreSQL flexible server `bilko-demo-pg` (PostgreSQL 16, swedencentral)

Full topology diagram (Cloudflare edge → ACA → Azure PG) lives in `DEPLOY-MAP.md` — do not duplicate it here; that file is kept current on every infra change.

### Apps in This Monorepo

| App | Path        | Port (dev) | Deployed to (ACA app name)           |
| --- | ----------- | ---------- | ------------------------------------ |
| Web | `apps/web/` | 3000       | `bilko-web-demo` / `bilko-web-stage` |
| API | `apps/api/` | 8080       | `bilko-api-demo` / `bilko-api-stage` |
| E2E | `apps/e2e/` | —          | CI only (Playwright, stage FQDNs)    |

### Key Packages

| Package  | Path                                        | Purpose                                         |
| -------- | ------------------------------------------- | ----------------------------------------------- |
| Database | `apps/api/src/main/resources/db/migration/` | Flyway SQL migrations (canonical schema source) |
| UI       | `packages/ui/`                              | Shared UI components                            |

### CI/CD Pipeline

**Single canonical pipeline:** `azure-pipelines.yml` (root), Azure DevOps `Bilko-CI-CD` (definitionId=1), org `alai-holding`, project `Bilko`, git remote `azdo`. Triggers on push to `main` and tag `v*`. GitHub Actions is dead/abandoned — do not rely on `.github/workflows/*` for deploys.

Stages: `CI_Gates` (8 jobs) → `Build` → `Flyway_Migrate` → `Deploy_Stage` → `E2E_UAT` → `Promote_Demo` (manual CEO approval). Full stage-by-stage detail: `DEPLOY-MAP.md` § CI/CD Pipeline.

---

## 2. Pre-Deployment Checklist

Run this before EVERY production (`Promote_Demo`) deployment:

- [ ] `CI_Gates` stage is green on the branch (Prettier, TSC, Vitest, Kotest, Trivy FS, Gitleaks, Semgrep)
- [ ] `Build` stage produced digest-pinned images in `bilkodemo.azurecr.io`
- [ ] `Flyway_Migrate` succeeded against `bilko-demo-pg` — no failed migrations in `flyway_schema_history`
- [ ] `Deploy_Stage` + `E2E_UAT` passed against `bilko-*-stage`
- [ ] If schema changed: new `V{N+1}__description.sql` exists in `apps/api/src/main/resources/db/migration/` — NEVER edit an existing migration
- [ ] No open Trivy/Semgrep HIGH/CRITICAL findings introduced by this change
- [ ] Environment variables/secrets verified in Azure Key Vault `kv-bilko-demo2` (via ADO variable group `bilko-kv-demo`)
- [ ] ZAKON PI2 six hard checks ready to run post-deploy (see `~/system/rules/zakon-pi2-deploy-verification.md`)

---

## 3. Deploying the API

**API source:** `apps/api/` (Kotlin + Ktor + Exposed)
**Deploy target:** Azure Container App `bilko-api-demo` (+ `bilko-api-stage`) — resource group `rg-bilko-demo`, environment `bilko-demo-env`
**Build pipeline:** `azure-pipelines.yml` `Build` stage — `docker buildx` linux/amd64 → Azure Container Registry
**Docker image:** `bilkodemo.azurecr.io/bilko-api:demo-{SHA}` (production) / `bilkodemo.azurecr.io/bilko-api:stage-{SHA}` (stage)

### 3a. Automatic Deploy (Stage — on every `main` push)

`Deploy_Stage` runs automatically on the `bilko-selfhosted` pool (FORGE agent `bilko-forge-1`) after `Build` + `Flyway_Migrate` succeed. It updates `bilko-api-stage` and `bilko-web-stage` to the digest-pinned image from `Build`.

Monitor: `az pipelines runs list --org https://dev.azure.com/alai-holding --project Bilko --pipeline-ids 1 --top 5` or the Azure DevOps pipeline UI.

### 3b. Promote to Production (CEO-gated manual approval)

`Promote_Demo` requires manual CEO approval in the ADO environment `bilko-demo`, then rebuilds `demo-{SHA}` images from the same commit and shifts `bilko-api-demo` ingress traffic 100% to the new revision (multiple-revisions mode, MC #105368).

```bash
# Trigger a manual pipeline run when Promote_Demo did not auto-open (BatchedCI can skip it)
az pipelines run --org https://dev.azure.com/alai-holding --project Bilko --branch main --id 1

# Approve via Azure DevOps UI (Pipelines > Environments > bilko-demo) once queued,
# or via API per CEO chat-authorization (see project memory for the API-approve pattern).
```

### 3c. Verify API Deployment

```bash
# Azure Container Apps status + FQDN
az containerapp list -g rg-bilko-demo \
  --query "[].{name:name, state:properties.provisioningState, fqdn:properties.configuration.ingress.fqdn}" \
  -o table

# Latest revision
az containerapp revision list -n bilko-api-demo -g rg-bilko-demo --query "[0].name" -o tsv

# Health check (brand domain, via CF Worker)
curl -s https://api.bilko.cloud/api/v1/health
curl -s https://api.bilko.io/api/v1/health
curl -s https://api.bilko.company/api/v1/health

# Direct ACA FQDN health check
curl -s https://bilko-api-demo.purplebeach-f004d490.swedencentral.azurecontainerapps.io/api/v1/health
```

### 3d. Required Secrets / Environment Variables (API)

Managed as Azure Container Apps secrets + env vars, sourced from Key Vault `kv-bilko-demo2` (variable group `bilko-kv-demo` in ADO). ACA write-only secrets return NULL on read — always cross-check against Key Vault, never assume a value from a stale doc.

| Secret / Var           | Description                                                                     |
| ---------------------- | ------------------------------------------------------------------------------- |
| `db-password`          | `bilko_admin` Azure PostgreSQL password                                         |
| `jwt-secret`           | Access token signing secret                                                     |
| `jwt-refresh-secret`   | Refresh token signing secret                                                    |
| `groq-api-key`         | LLM provider key (API)                                                          |
| `resend-api-key`       | Transactional email provider key                                                |
| `field-encryption-key` | PII field-level encryption key                                                  |
| `field-hmac-key`       | PII field-level HMAC key                                                        |
| `CORS_ORIGINS`         | Allowed origins — see `DEPLOY-MAP.md` OCD-12 for the stage custom-domain gotcha |

---

## 4. Deploying the Web Frontend

**Frontend source:** `apps/web/` (Next.js 15)
**Deploy target:** Azure Container App `bilko-web-demo` (+ `bilko-web-stage`)
**Build pipeline:** Same `azure-pipelines.yml` `Build` stage — builds `apps/web/` alongside the API in one pipeline run
**Production domains:** `app.bilko.io`, `app.bilko.cloud`, `app.bilko.company` (routed through Cloudflare Worker `bilko-edge-proxy`, which rewrites Host/SNI to the ACA FQDN)

### 4a. Automatic Deploy (Stage) / 4b. Promote to Production

Same pipeline and same manual-approval gate as the API (§3a/§3b) — web and API are built and promoted together from one Azure DevOps run.

### 4c. Required Environment Variables (Web)

Managed via ACA env vars on `bilko-web-demo` / `bilko-web-stage`.

| Variable              | Description                                                                                          |
| --------------------- | ---------------------------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_API_URL` | API base URL — currently baked at build time (see DEPLOY-MAP.md OCD-9 for the runtime-URL follow-up) |
| `NEXT_PUBLIC_APP_ENV` | `production` or `staging`                                                                            |

### 4d. Verify Web Deployment

```bash
az containerapp revision list -n bilko-web-demo -g rg-bilko-demo --query "[0].name" -o tsv

curl -sI https://app.bilko.cloud/ | head -1
curl -sI https://app.bilko.io/ | head -1
curl -sI https://app.bilko.company/ | head -1
```

---

## 5. Database Operations

**Migration tool:** Flyway 11 (NOT Prisma — Prisma was tied to the deleted Express backend). Migration files live in `apps/api/src/main/resources/db/migration/V*.sql`.

### 5a. Running Migrations

Migrations run automatically in the `Flyway_Migrate` pipeline stage against `bilko-demo-pg` on every `main` push, before `Deploy_Stage`.

Manual run (rare — CI is the default path):

```bash
docker run --rm \
  -v "$(pwd)/apps/api/src/main/resources/db/migration:/flyway/sql" \
  flyway/flyway:11 \
  -url="jdbc:postgresql://bilko-demo-pg.postgres.database.azure.com:5432/bilko?sslmode=require" \
  -user=bilko_admin \
  -password="$DB_PASSWORD" \
  migrate
```

**RULE:** Never edit an existing `V*.sql` migration. Always create a new `V{N+1}__description.sql`. Reserve the version number carefully — two independent branches taking the same `V` number is a recurring collision (see `technical_bilko_promote_manual_and_flyway_collisions_2026-07-20.md`); check `git ls-tree azdo/main -- apps/api/src/main/resources/db/migration/` before picking a number.

### 5b. Checking Migration Status

```bash
psql "host=bilko-demo-pg.postgres.database.azure.com port=5432 dbname=bilko user=bilko_admin sslmode=require" \
  -c "SELECT version, description, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 20;"
```

All rows should show `success = t`.

### 5c. Rolling Back a Migration

Flyway (community edition, as used here) does not support automatic migration rollback.

**Option 1 (preferred):** Write a new compensating migration that undoes the change (new `V{N+1}` file with inverse DDL/DML).

**Option 2 (last resort):** Restore from an Azure PostgreSQL backup (§7f). Data written after the migration will be lost.

### 5d. Database Backup and Restore

Azure PostgreSQL flexible server provides automatic backups (default retention — verify current setting via `az postgres flexible-server show`). For an on-demand logical backup:

```bash
pg_dump "host=bilko-demo-pg.postgres.database.azure.com port=5432 dbname=bilko user=bilko_admin sslmode=require" \
  --format=custom --no-acl --no-owner \
  --file=bilko-backup-$(date +%Y%m%d-%H%M%S).dump
```

Restore:

```bash
pg_restore --dbname="host=... user=bilko_admin sslmode=require" \
  --no-acl --no-owner \
  bilko-backup-YYYYMMDD-HHMMSS.dump
```

---

## 6. Rollback Procedures

### 6a. API Rollback (Azure Container Apps)

```bash
# List revisions, newest first
az containerapp revision list --name bilko-api-demo --resource-group rg-bilko-demo \
  --query "[].{name:name,created:properties.createdTime,active:properties.active}" -o table

# Shift 100% traffic to the last known-good revision
az containerapp ingress traffic set --name bilko-api-demo --resource-group rg-bilko-demo \
  --revision-weight <previous-revision-name>=100
```

### 6b. Web Rollback (Azure Container Apps)

```bash
az containerapp revision list --name bilko-web-demo --resource-group rg-bilko-demo \
  --query "[].{name:name,created:properties.createdTime,active:properties.active}" -o table

az containerapp ingress traffic set --name bilko-web-demo --resource-group rg-bilko-demo \
  --revision-weight <previous-revision-name>=100
```

### 6c. Edge Rollback (Cloudflare)

```bash
# Worker (bilko-edge-proxy) rollback
cd apps/edge-proxy && wrangler rollback

# CF Pages (marketing landings) — via dashboard:
# Pages > project (bilko-io / bilko-cloud / bilko-company) > Deployments > "Rollback to this deployment"
```

### 6d. Database Rollback

**IMPORTANT:** Database rollbacks are destructive if data was written after the migration ran. Always confirm with the team before rolling back the database.

Steps:

1. Take a current-state backup first (§5d)
2. Write a compensating migration (preferred over restore — see §5c Option 1)
3. Only restore from backup if data loss is acceptable and a pre-migration backup exists

---

## 7. Database Server Operations (Azure PostgreSQL)

**Database:** Azure PostgreSQL flexible server `bilko-demo-pg`, B1ms, PostgreSQL 16, region swedencentral, resource group `rg-bilko-demo`
**Schema source of truth:** Flyway migrations, `apps/api/src/main/resources/db/migration/`
**App user:** `bilko_admin` (NOT `bilko`) — password in ACA secret `db-password` / Key Vault `kv-bilko-demo2`
**Shared instance:** stage and demo currently share this one server (see `DEPLOY-MAP.md` OCD-3 for the isolation tradeoff)

### 7a. Connecting (Local / Admin)

```bash
psql "host=bilko-demo-pg.postgres.database.azure.com port=5432 dbname=bilko user=bilko_admin sslmode=require"
```

Firewall: FORGE's public IP is allow-listed (rule `forge-ci-runner`); Microsoft-hosted CI agents use the `allow-azure-services` rule. Connecting from an unlisted IP will time out — add a temporary firewall rule via `az postgres flexible-server firewall-rule create` rather than widening `allow-azure-services`.

### 7b. Running the RLS Audit

```bash
# Via test suite (preferred)
./gradlew :apps:api:test --tests "*RlsAudit*"
```

Tables that MUST have RLS enabled include (non-exhaustive — see the audit test for the authoritative list): `organizations`, `users`, `invoices`, `invoice_items`, `expenses`, `transactions`, `bank_accounts`, `bank_transactions`, `contacts`, `logged_actions`.

### 7c. Verifying RLS Policies in PostgreSQL

```sql
-- List all RLS policies
SELECT schemaname, tablename, policyname, cmd, qual
FROM pg_policies
ORDER BY tablename, policyname;

-- Check which tables have RLS enabled
SELECT relname AS table_name, relrowsecurity AS rls_enabled
FROM pg_class
WHERE relnamespace = 'public'::regnamespace AND relkind = 'r'
ORDER BY relname;
```

### 7d. Enabling RLS on a Table

```sql
ALTER TABLE public.table_name ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.table_name FORCE ROW LEVEL SECURITY;

-- Example org-scoped policy
CREATE POLICY org_isolation ON public.table_name
  USING (organization_id = current_setting('app.current_org_id')::uuid);
```

### 7e. Backup and Restore (Azure PostgreSQL server-level)

```bash
# Server status and current config
az postgres flexible-server show -g rg-bilko-demo -n bilko-demo-pg \
  --query "{name:name, state:state, version:version, location:location}" -o table

# Point-in-time restore to a new server (does NOT overwrite the source — verify before promoting)
az postgres flexible-server restore \
  --resource-group rg-bilko-demo \
  --name bilko-demo-pg-restored \
  --source-server bilko-demo-pg \
  --restore-time "2026-07-28T00:00:00Z"
```

Also see the logical `pg_dump`/`pg_restore` path in §5d for a single-database, portable backup.

---

## 8. Troubleshooting Guide

### API Down

**Symptoms:** `GET /api/v1/health` returns non-200 or times out.

**Steps:**

1. Check Azure status for the region: `az containerapp show -n bilko-api-demo -g rg-bilko-demo --query "properties.provisioningState"`
2. Check container logs: `az containerapp logs show -n bilko-api-demo -g rg-bilko-demo --tail 100`
3. Check recent revisions for a bad deploy: `az containerapp revision list -n bilko-api-demo -g rg-bilko-demo -o table`
4. If a bad revision is live, roll back (§6a)
5. Check the Cloudflare Worker (`bilko-edge-proxy`) isn't misrouting — `curl` the direct ACA FQDN to isolate edge vs. origin
6. If not resolved in 10 minutes: rollback (§6) and open a P1 per §10

### High Error Rate

**Symptoms:** More than 5% of API requests returning 5xx errors.

**Steps:**

1. `az containerapp logs show -n bilko-api-demo -g rg-bilko-demo --tail 200` — look for error patterns
2. Check Application Insights / Azure Monitor (if wired for this service) for new error groups
3. Health check: `curl https://api.bilko.cloud/api/v1/health`
4. Check if a Flyway migration ran recently — look for schema mismatches in the logs
5. Check for `too many connections` errors (see §8 DB Connection Pool Exhausted below)
6. Restart the revision if connection-pool errors persist: `az containerapp revision restart -n bilko-api-demo -g rg-bilko-demo --revision <name>`

### Database Unreachable

**Symptoms:** Health endpoint shows `"database": "error"`, connection errors in logs.

**Steps:**

1. `az postgres flexible-server show -g rg-bilko-demo -n bilko-demo-pg --query "state"`
2. Verify the `db-password` secret / connection string is current: `az containerapp secret list -n bilko-api-demo -g rg-bilko-demo` (values are NULL by design — cross-check against Key Vault `kv-bilko-demo2`, not against this doc)
3. Test connection directly: `psql "host=bilko-demo-pg.postgres.database.azure.com ... sslmode=require" -c "SELECT 1;"`
4. Check firewall rules haven't been tightened/removed: `az postgres flexible-server firewall-rule list -g rg-bilko-demo -n bilko-demo-pg -o table`
5. Check connection pool exhaustion (below)

### DB Connection Pool Exhausted

**Symptoms:** `too many connections` errors, requests queuing indefinitely.

**Steps:**

1. Check current connections:
   ```sql
   SELECT count(*), state FROM pg_stat_activity GROUP BY state;
   ```
2. Find long-running queries:
   ```sql
   SELECT pid, now() - query_start AS duration, query
   FROM pg_stat_activity
   WHERE state = 'active' AND now() - query_start > interval '30 seconds';
   ```
3. Kill a stuck query if safe:
   ```sql
   SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = target_pid;
   ```
4. Restart the API revision to release the connection pool (`az containerapp revision restart`)
5. Consider PgBouncer / connection-pooling middleware if this recurs under load

### Slow Responses

**Symptoms:** p95 response time above 2 seconds.

**Steps:**

1. Check for slow queries:
   ```sql
   SELECT query, calls, mean_exec_time
   FROM pg_stat_statements
   ORDER BY mean_exec_time DESC LIMIT 20;
   ```
2. Check for missing indexes on frequently filtered columns
3. Look for N+1 query patterns in Exposed usage
4. Use `EXPLAIN ANALYZE` on suspect queries

### SSL Certificate Issues

```bash
echo | openssl s_client -connect api.bilko.cloud:443 2>/dev/null | openssl x509 -noout -dates
```

- **Cloudflare-fronted domains** (`app.*`, `api.*`, `bilko.io/.cloud/.company`): CF-managed, auto-renews — check Cloudflare dashboard if issues.
- **Stage custom domains** (`web-stage.bilko.cloud`, `api-stage.bilko.cloud`): ACA-managed certs (`SniEnabled`), direct DNS-only CNAME (not proxied) — check `az containerapp hostname list -n bilko-api-stage -g rg-bilko-demo` for cert binding state.

### Deployment Fails in CI

Common causes:

| Failure                           | Fix                                                                                   |
| --------------------------------- | ------------------------------------------------------------------------------------- |
| `CI_Gates` lint/TSC fail          | Fix and push — see which of the 8 jobs failed in the ADO run                          |
| `Trivy` HIGH/CRITICAL             | Update the dependency or image base, re-run                                           |
| `Flyway_Migrate` fails            | Bad SQL or a version-number collision (see §5a) — never edit an applied migration     |
| `Deploy_Stage` health check fails | New revision not passing `/api/v1/health` or `/` within the 20s wait — check logs     |
| `E2E_UAT` skips                   | Usually a stale `BILKO_E2E_TOKEN_SECRET` in `bilko-kv-demo` — see DEPLOY-MAP.md OCD-1 |

---

## 9. Monitoring and Alerts

See `monitoring/` directory:

- `monitoring/uptime.json` — uptime check configuration
- `monitoring/alert-rules.json` — alert thresholds and routing
- `monitoring/metrics.md` — key metrics with targets

**Quick health commands:**

```bash
# Production API (brand domains)
curl -s https://api.bilko.cloud/api/v1/health
curl -s https://api.bilko.io/api/v1/health
curl -s https://api.bilko.company/api/v1/health

# Production Web
curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" https://app.bilko.cloud

# Stage
curl -s https://api-stage.bilko.cloud/api/v1/health
curl -sI https://web-stage.bilko.cloud/ | head -1
```

**Log locations:**

| Component        | Log location                                                                                   |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| API (ACA)        | `az containerapp logs show -n bilko-api-demo -g rg-bilko-demo`                                 |
| Web (ACA)        | `az containerapp logs show -n bilko-web-demo -g rg-bilko-demo`                                 |
| DB (Azure PG)    | Azure Portal > `bilko-demo-pg` > Monitoring > Logs, or `pg_stat_activity`/`pg_stat_statements` |
| CI/CD            | Azure DevOps > `Bilko-CI-CD` pipeline > run > stage logs                                       |
| Edge (CF Worker) | `wrangler tail` against `bilko-edge-proxy`, or Cloudflare dashboard                            |

---

## 10. Emergency Contacts and Escalation

| Role               | Contact                     | Method         |
| ------------------ | --------------------------- | -------------- |
| On-call / CEO      | Alem Basic (alem@alai.no)   | Direct / Slack |
| Azure support      | portal.azure.com support    | Web            |
| Azure DevOps       | dev.azure.com/alai-holding  | Web            |
| Cloudflare support | dash.cloudflare.com support | Web            |

**Escalation path:**

- **P1 (Production down):** Fix immediately per §8 Troubleshooting; notify Alem directly if not resolved within 15 min.
- **P2 (Degraded performance):** Investigate per §8; notify Alem if unresolved within 1 hour.
- **P3 (Non-urgent):** Log as an MC task (`mc.js add`), handle in the next work cycle.

---

_Last updated: 2026-07-28 — B4 doc cleanup pass (MC #101088): rewritten for Azure Container Apps (all legacy platform and ORM references removed; content grounded against `DEPLOY-MAP.md` and `apps/api/src/main/resources/db/migration/`)._

# Bilko main — scheduled validation backstop (MC #106809, 2026-08-05)

# Bilko main — scheduled validation backstop (MC #106809)

**Datum:** 2026-08-05 · **Builder:** FlowForge · **Merge:** PR #317 (John, zeleni build #941) · **Nezavisna verifikacija:** 8/8 PASS (peer-verdict u evidence)

## Problem
Push-triggered `batchedCI` buildovi na Bilko `main` otkazivali su jedni druge u lancu (org ima 1 self-hosted parallel slot — kontencija, ne bug u `batch:true`): main danima bez završenog runa (zadnji zeleni 2026-08-03 16:48), tri Gate-0 merža nikad post-merge validirana.

## Rješenje (minimalno, 2 aditivne izmjene u `azure-pipelines.yml`)
1. `schedules:` cron `0 */3 * * *` (samo main, `always: false` — vrti se samo ako ima novih commita).
2. Jedna linija u Build stage uslovu: `eq(variables['Build.Reason'], 'Schedule')` — scheduled run izvršava PUN lanac.

Scheduled trigger klasa nije podložna push-vs-push batch otkazivanju → u mirnijem prozoru run završi do kraja. `batch:true` i `pr.autoCancel` netaknuti (kelsey dissent iz forge 106804 D3 ispoštovan).

## Dokaz
- PR #317: merž SAMO nakon zelenog validation builda #941 (nova blokirajuća disciplina).
- Prvi scheduled run **#944 (20260805.3)**: completed/**succeeded**, timeline potvrđuje da su CI Gates, Build, Flyway Migrate, Deploy→Stage i **E2E UAT** svi stvarno izvršeni (jedino Promote→Demo skipped — ručno odobrenje, očekivano).
- Nezavisna peer-verifikacija: 8/8 tvrdnji živim REST pozivima; merge diff = tačno 1 fajl.
- Evidence: `~/system/evidence/106809/` (+ `peer-verify/independent-peer-verdict-106809.md`).

## Napomene
- Ponašanje kroz više tikova (svaka 3h + dedup) posmatrati narednih dana — dizajnirano po ADO semantici, prvi tik dokazan.
- Push izveden REST API-jem jer `turbo` binary u agent sandboxu biva SIGKILL-an (pre-push hook nemoguć) → MC #106854; sporedna posljedica: commit autorstvo = PAT identitet.
- Povezano: #106804 (QODY gate, ZATVOREN lanac), #106811 (mail digest — čeka par mirnih dana), #106802/#106841 (slot/Entra — pauzirano, CEO odluka).

# Bilko /login silent auth failure — dijagnoza i fix (MC #106849)

# Bilko /login silent auth failure — dijagnoza i fix (MC #106849)

**Datum:** 2026-08-05 · **PR:** #318 (`fix/106849-login-dead-click`, commit `3df83cd1`) · **Status:** no active outage; hardening + observability fix

## Simptom
Klik na "Prijavite se Microsoft računom" na `app.bilko.cloud/login` naizgled "ne radi ništa" — korisnik ostane na /login bez ikakve poruke, bez konzolnih grešaka, bez vidljivih network poziva.

## Root cause (potvrđeno live forenzikom)
1. Frontend je bio ISPRAVAN: React onClick → MSAL `loginRedirect()` → top-level navigacija na `ciamlogin.com/.../authorize` se uredno izvršavala.
2. Microsoft CIAM authorize je za sesiju s postojećim kolačićima vraćao **HTTP 503** (čist curl bez kolačića: 200) — vrlo vjerovatno throttling zbog desetina automatizovanih pokušaja u kratkom periodu.
3. Aplikacija je taj neuspjeh **tiho gutala**: povratak na /login bez poruke (503 prije ikakvog MSAL callbacka ⇒ MSAL nema event koji bi opalio).
4. Dijagnostička zamka: top-level navigacije se ne vide kao XHR u network alatima i ne ostavljaju konzolne greške; uz to, sintetički klikovi browser-ekstenzije nisu ni stizali do stranice (automation artefakt) — vidi memory `technical_extension_clicks_can_be_silent_noops_2026-08-05`.

## Fix (D3 — error surfacing)
- `AUTH_ATTEMPT_PENDING_KEY` sessionStorage marker postavljen neposredno prije `loginRedirect()`.
- Na sljedećem MSAL init-u: marker prisutan + 0 accounta ⇒ `authError` (pokriva fail-prije-callbacka); dodatno obrađen `EventType.LOGIN_FAILURE`.
- `/login` renderuje vidljiv alert (`data-testid="entra-auth-error"`) + retry dugme, svih 5 lokala (en/hr/bs/sr-Latn/sr-Cyrl).
- 7 novih testova u `apps/web/test/login-silent-auth-fail-106849.test.tsx` (oba puta + negativne kontrole); dokazano padaju na pre-fix kodu.

## Regression gate (D4)
`azure-pipelines.yml` → `Promote_Demo` sada, odmah poslije curl-only ZAKON PI2 provjere, izvršava **real-browser MSAL login smoke** (postojeći `login-dashboard.spec.ts`) protiv **demo artefakta** (`DEMO_WEB_URL=https://app.bilko.cloud`). Zatvoren strukturni gap: stage i demo su odvojeni buildovi, a login test je do sada postojao samo za stage.

## Verifikacija
- Nezavisni verifier: 35/35 testova samostalno izvršeno, scope čist, verdikt PASS — transcript: `/Users/makinja/system/evidence/106849/verifier-transcript-2026-08-05.md`
- CEO ljudski klik (telefon, čist browser): Microsoft login stranica se uredno otvara.
- Puna forenzika: `/Users/makinja/system/evidence/bilko-login-dead-2026-08-05/repro-evidence.md` + MC #106849 komentari.

## Lekcije
- "Mrtav klik" iz automatizacije ≠ mrtav klik: trijaža = instrumentacija stranice → programatski `.click()` → ljudski klik.
- curl health check je slijep za auth petlje; login smoke mora gađati artefakt koji korisnici stvarno dobijaju.
- Tihi auth fail je UX bug prvog reda: svaki neuspjeh prijave mora biti vidljiv korisniku.

Povezani taskovi: MC #106849 (fix), #106834 (automation artefakt), #106850 (anvil incident), #106856 (subagent identity), #106858 (msal-provider minor).

# bilko.cloud landing — honest claims sweep (MC #106980, 2026-08-08)

# bilko.cloud landing — honest claims sweep (MC #106980)

**Datum:** 2026-08-08 · **Status:** LIVE na produkciji · **PR:** 331 (azdo, fix/106980-landing-claims → main) · **Commit:** 149bb1a0

## Šta je promijenjeno i zašto

Izvor: `~/system/evidence/bilko-fiken-gap/LANDING-NALAZI.md` (07.08) — landing je reklamirao zakonski obaveznu funkciju kao "beta". CEO nalog 08.08: ukloniti sve netačne/podcjenjujuće tvrdnje.

| Stavka | Prije | Poslije |
|---|---|---|
| Hero eRačun | "FISK 2.0/eRačun priprema dostupna u **beta fazi**" | "eRačun (FISK 2.0) podrška je integrirana prema HR CIUS 2025 standardima — **produkcijsko slanje je u fazi aktivacije**" (CEO odabrao varijantu V1, 2026-08-08) |
| Cjenovne kartice CTA | `/demo?country=HR` ("Pogledaj demo") | `/register?plan=trial` ("Isprobaj besplatno") — 2× |
| eRačun tag na karticama | "(beta — uskoro u produkciji)" | "(u aktivaciji)" |
| PSD2 auto-sync tag | "(beta — uskoro u produkciji)" — funkcija ne postoji | "(u pripremi)" — bez datuma i obećanja |
| CSV bankovni uvoz | istaknut kao feature (reklamiranje slabosti) | "Uvoz bankovnih izvoda", neutralno |
| JSON-LD / FAQ / stat badge | "beta fazi" na 7 mjesta | 0; product-level `softwareVersion: beta` namjerno zadržan (beta program stvarno traje) |
| schema.org | `addressCountry: NO` uz Zagreb | `HR` |
| Badge | "FISK 2.0 u razvoju" | "FISK 2.0 u aktivaciji" |

## Model istine (granice tvrdnji)

- `SVERACUN_HR_LIVE` je OFF → ne smije se tvrditi živo produkcijsko slanje. "U fazi aktivacije" je tačno: produkcijski API ključ primljen 03.08 (MC #106981), aktivacija čeka FiskAplikacija potvrde (AMS + PostLink ovlaštenje) i komercijalne uslove.
- PSD2/Open Banking: provider nije izabran (MC #106365 paused) → samo "u pripremi", bez rokova.

## Deploy i verifikacija

- Deploy: `wrangler pages deploy` → CF Pages projekat `bilko-cloud`, `--branch=main` (production). Napomena: bez `--branch=main` deploy ide na preview alias, apex se ne mijenja.
- Verifikacija: Playwright 11/11 PASS na živom apexu (`browser-verification.json`, 4 screenshota uklj. mobile 390×844) + curl content grep (`apex-verify-20260808.txt`).
- Nezavisni peer: `native-peer-verify-transcript.md` — Verdict: PASS, 9/9 tvrdnji.

## Evidence

`~/system/evidence/106980/` — builder-evidence.md, HERO-VARIJANTE.md, apex-verify-20260808.txt, browser-verification.json, browser-screenshots/, native-peer-verify-transcript.md, pr-created-20260808.json.

## Napomene za budućnost

- `apps/landing-hr/index.html` JE izvor (nema više .tsx source-a u tom appu) — direktne HTML izmjene su trajne.
- PR 331 poravnava repo s live artefaktom; merge kad backend CI ozdravi (#106960) — live sajt ne zavisi od toga.

# Bilko Feature — Document Inbox / Ulaz dokumenata (MC #104515)

# Bilko — Document Inbox / Ulaz dokumenata (MC #104515)

**Status: BUILT AND LIVE on `azdo/main`** (verified 2026-08-08 by reading `apps/api` and `apps/web` at the tip of `azdo/main`, commit `88769328`). This page documents the actual shipped implementation — not a plan. The original MC #104515 gap audit (2026-06-29) flagged Document Inbox as "MISSING"; it shipped afterward across three merges:

| MC | What shipped | Merge |
|---|---|---|
| #104515 (Phase 1) | Capture-first upload → pending review queue → book as expense / reject | PR 30, `fee37dc8` |
| #104519 | Proveo route-layer test coverage for Phase 1 | same PR |
| #105687 | Third terminal status `archived` — permanent document archive (`/dokumenti`) for documents that never become an expense | `e2ee2a4a` |
| #106195 | UX redesign of the inbox/`ulazni-racuni` screens | `88769328` |

## 1. Concept

Two distinct product surfaces share one table (`inbox_items`):

- **`/inbox`** ("Ulaz dokumenata") — the active review queue. A user uploads a receipt/invoice scan *before* any accounting record exists. Each upload becomes a `pending` row. From there it is either **booked** (creates an `Expense` and links back), **rejected** (discarded, reason optional), or **archived** (see below).
- **`/dokumenti`** ("Arhiva dokumenata", MC #105687) — permanent archive for documents that will *never* become an expense: contracts, bank statements, delivery notes (*otpremnica*), insurance policies. Modeled as a third terminal status on the same row rather than a new table (see design rationale in the V122 migration, §2).

This is deliberately distinct from `ExpenseDocuments` (V40), which is attach-to-an-*existing*-expense, and from `ReceivedEInvoices` (V139), which is the Storecove e-invoice webhook capture (has OIB/UBL fields, no OCR).

## 2. Schema

Table `inbox_items`, defined in `V106__document_inbox.sql` (Phase 1) and widened by `V122__inbox_archive_documents.sql` (archive feature). Kotlin Exposed object: `InboxItems` in `apps/api/src/main/kotlin/no/alai/bilko/models/Tables.kt`.

**V106 — base table:**
- Identity: `id` (UUID PK), `org_id` (FK → `organizations`, `ON DELETE CASCADE`)
- Storage: `storage_url`, `storage_key`, `original_filename`, `content_type`, `file_size`, `checksum_sha256`, `storage_backend` (`r2` \| `local` \| `unknown`) — mirrors the `ExpenseDocuments` / `ReceiptService.uploadDocument` storage pattern
- Lifecycle: `status` (`pending` \| `booked` \| `rejected`, widened to add `archived` in V122), `uploaded_by`
- **Phase 2 OCR fields (nullable, reserved, NOT populated by Phase 1 code):** `extracted_amount NUMERIC(19,4)`, `extracted_currency CHAR(3)`, `extracted_date DATE`, `extracted_vendor VARCHAR(500)`, `extracted_vat NUMERIC(19,4)`, `ocr_confidence NUMERIC(5,4)` (checked `0.0000–1.0000`) — the migration header states these are for **Azure Document Intelligence**, deferred.
- Booking linkage: `booked_expense_id`, `booked_invoice_id`, `booked_at`, `booked_by`
- Rejection: `rejection_reason`, `rejected_at`, `rejected_by`
- Audit: `created_at`, `updated_at`
- Constraints: `status` check, `file_size > 0`, `storage_backend` allowlist, `ocr_confidence` range check
- Indexes: `(org_id, status)`, `(org_id, created_at DESC)`, `(uploaded_by)`
- **RLS:** `ENABLE ROW LEVEL SECURITY` + `FORCE ROW LEVEL SECURITY`, policy `org_isolation` scopes every row to `current_setting('app.current_org_id')::uuid` for role `bilko_app` — same pattern as `expense_documents` (V40).

**V122 — archive extension** (adds the `archived` outcome, MC #105687):
- Widens the `status` CHECK to include `archived`
- New columns: `document_type` (nullable, enum-checked only when populated: `contract`\|`statement`\|`delivery_note`\|`insurance_policy`\|`other`), `contact_id` (nullable FK → `contacts`, `ON DELETE SET NULL`), `tags TEXT[]` (default `'{}'`), `archived_at`, `archived_by`
- New indexes: partial index on `archived_at` where `status='archived'`, partial index on `document_type`, index on `contact_id`, GIN index on `tags`
- **Design decision (documented in the migration header):** extend `inbox_items` rather than create a new `archived_documents` table, to avoid duplicating storage/RLS/index plumbing. `booked_expense_id` is reused (not status-changing) for the "naknadno vezanje" (late-link) flow — e.g. an *otpremnica* archived first, linked to an expense once the *račun* arrives later.
- **Retention is explicitly NOT enforced** in this migration — HR *knjigovodstvene isprave* retention (an 11-year candidate) needs validation with the `bilko-racunovodstvo-hr` domain expert before any auto-deletion ships. No expiry logic exists today.
- RLS needs no change — row-scoped policy from V106 automatically covers new columns.

## 3. Routes

Defined in `apps/api/src/main/kotlin/no/alai/bilko/routes/InboxRoutes.kt`, wired in `Routing.kt` via `inboxRoutes()` and `documentsRoutes()`, service layer `InboxService.kt` (DI singleton in `DI.kt`).

**`inboxRoutes()` — mounted under `/inbox`:**
| Method | Path | Purpose |
|---|---|---|
| GET | `/inbox/count` | Pending badge count (dashboard bell + sidebar) — registered *before* `/{id}` to avoid Ktor trie ambiguity |
| GET | `/inbox` | Paginated list; query params `status`, `page`, `perPage` |
| POST | `/inbox` | Multipart upload → creates a `pending` item |
| GET | `/inbox/{id}` | Detail for the review screen |
| POST | `/inbox/{id}/book` | Creates an `Expense` in the same transaction, transitions row to `booked`, best-effort attaches the original scan to the new expense via `ExpenseService.attachDocument` |
| POST | `/inbox/{id}/reject` | Transitions to `rejected`, optional `{ "reason": string }` |
| POST | `/inbox/{id}/archive` | Transitions to `archived` (V122); body: `documentType` (required), `contactId` (optional), `tags` (optional) |

**`documentsRoutes()` — mounted under `/documents` (backs the `/dokumenti` screen, MC #105687):**
| Method | Path | Purpose |
|---|---|---|
| GET | `/documents` | Paginated, filterable list of `archived` items (`documentType`, `contactId`, `tag`, `dateFrom`, `dateTo`) |
| POST | `/documents/{id}/link-expense` | Late-link an already-archived document to an `Expense` created separately (body: `{ "expenseId": string }`) |

**Upload security pipeline** (POST `/inbox`, in order): permission check *before* multipart parse → `UploadSecurityGate.authorizeActor` (tenant-bound actor check) → MIME allowlist (`application/pdf`, `image/jpeg`, `image/jpg`, `image/png`) → 20 MB hard cap → empty-file guard → the client-declared `Content-Type` header check here is a cheap early rejection only (attacker-controlled) — the authoritative control is magic-byte content sniffing + ClamAV malware scan + persisted quarantine/scan-provenance state machine at the shared `ReceiptService.uploadObject` choke point (`UploadSecurityGate`, MC #106852 G1-02), which also gates expense-attach, invoice-receipt, and support-ticket-attachment uploads. The route additionally hard-fails closed (compensates/deletes the stored object) if scan provenance (`scanAttemptId`, `scanState == "RELEASED"`, `scanEngine`, `scanEngineVersion`, `scannedAt`) is incomplete after upload — an inbox row is never created for an object without a verified clean-scan verdict.

## 4. RBAC

Role hierarchy (`RbacHelper.kt`): `viewer` (0) < `accountant` (1) < `admin` (2) < `owner` (3). Permission catalog + role grants seeded in `V67__rbac_permissions_catalog.sql`.

| Route | Permission | Roles that hold it |
|---|---|---|
| `GET /inbox/count`, `GET /inbox`, `GET /inbox/{id}`, `GET /documents` | `expense:read` | viewer, accountant, admin, owner |
| `POST /inbox` (upload), `POST /inbox/{id}/book` | `expense:create` | accountant, admin, owner |
| `POST /inbox/{id}/reject`, `POST /inbox/{id}/archive`, `POST /documents/{id}/link-expense` | `expense:categorize` | accountant, admin, owner |

No new permission keys were introduced for Document Inbox — it reuses the existing `expense:*` catalog, treating booking/rejecting/archiving as expense-adjacent classification actions. `viewer` role can browse the inbox and archive but cannot upload, book, reject, or archive.

## 5. Frontend

- `apps/web/app/(dashboard)/inbox/page.tsx` — list/queue screen: drag-and-drop or file-picker upload (PDF/JPEG/PNG, 20 MB cap), status tabs (`pending`/`booked`/`rejected` — `archived` intentionally excluded, it lives on `/dokumenti`), dashboard badge via `GET /inbox/count`. No client-side raw-byte preview; files are proxied through the expense-documents content endpoint.
- `apps/web/app/(dashboard)/inbox/[id]/page.tsx` — review/booking detail screen.
- `apps/web/app/(dashboard)/dokumenti/page.tsx` — permanent archive screen (MC #105687).
- **Sidebar nav** (`apps/web/components/sidebar.tsx`): two entries under the `expensesGroup` section — `{ key: 'inbox', href: '/inbox', icon: Inbox }` and `{ key: 'dokumenti', href: '/dokumenti', icon: Archive }` — both placed above `expenses` and `purchases` in the group.

## 6. i18n

Sidebar labels are localized via `apps/web/messages/{bs,en,hr,sr-Cyrl,sr-Latn}.json`, `navigation` namespace:
- `"inbox": "Ulaz dokumenata"`
- `"dokumenti": "Arhiva dokumenata"`

**Gap found during this review:** the inbox/dokumenti *page bodies* (`inbox/page.tsx`, `inbox/[id]/page.tsx`, `dokumenti/page.tsx`) do **not** call `useTranslations`/`t(...)` — grep for both found zero matches. All in-page copy (labels, buttons, empty states) is hardcoded Bosnian/Croatian JSX, not routed through next-intl. Only the sidebar navigation label is translated. This is a real gap, not a design choice documented anywhere in the code — flagging it here rather than in the "OCR hooks" section since it's a currently-live inconsistency, not deferred work.

## 7. Phase 2 — OCR hooks (deferred, not built)

Both migration headers and the `Tables.kt` block comment explicitly scope OCR to Phase 2, deferred:

> "Phase 2 (OCR via Azure Document Intelligence): extracted_* columns are nullable and reserved for Phase 2 population. Phase 1 build leaves them NULL."

What exists today as the OCR integration point:
- Six nullable columns on `inbox_items`: `extracted_amount`, `extracted_currency`, `extracted_date`, `extracted_vendor`, `extracted_vat`, `ocr_confidence` (0–1 range, checked).
- No service, route, or background job populates them — confirmed by reading `InboxRoutes.kt` and `InboxService.kt` end to end; no reference to Azure Document Intelligence, OCR, or any of the six `extracted_*`/`ocr_confidence` fields appears outside the schema/comments.
- No Phase 2 MC task exists yet for the OCR build itself (only the Phase 1 capture queue, MC #104515, and the archive extension, MC #105687, have shipped).

**Implication for a future Phase 2 build:** the schema already has the landing spot for OCR output; the work is a new async job (upload → queue → Azure Document Intelligence call → populate `extracted_*`/`ocr_confidence` → surface a "confirm extracted values" step in the `/inbox/{id}` review screen before booking). No API contract for that job exists yet.

## 8. Verification method

All facts on this page were read directly from `azdo/main` (Bilko repo, `~/business/ALAI-Holding-AS/products/Bilko`) at commit `88769328` (2026-08-08), not from prior planning docs or memory:
- `git log --all --grep`, `git diff main...feat/document-inbox-104515 --stat`, `git merge-base --is-ancestor` to confirm the feature branch's content reached `main` (Azure DevOps squash-merges, so individual feature-branch commits are not ancestors of `main` even though the content is — checked via `git ls-tree -r azdo/main` file presence, not commit ancestry alone).
- Full reads of `V106__document_inbox.sql`, `V122__inbox_archive_documents.sql`, `InboxRoutes.kt` (630 lines), relevant sections of `Tables.kt`, `Routing.kt`, `DI.kt`, `sidebar.tsx`, `bs.json`, `V67__rbac_permissions_catalog.sql`, `RbacHelper.kt`, and the first ~60 lines of `inbox/page.tsx`.
- The original MC #104515 gap audit (status: done, 2026-06-29) is the origin of this task but is now stale — it predates all three merges above and should not be treated as current state.

## 9. Cross-references

- MC #104515 — Fiken-gap audit that identified doc inbox as missing + Phase 1 build
- MC #104519 — Proveo route-layer test coverage for Phase 1
- MC #105687 — Permanent document archive / `archived` status (V122)
- MC #106195 — Inbox/`ulazni-racuni` UX redesign
- MC #106852 (G1-02) — `UploadSecurityGate` shared upload security choke point
- Related BookStack page: "Bilko Operational Runbook — Azure Container Apps" (same book)

<!-- ALAI-MC:900178:BEFORE -->
MC #900178 (2026-08-24): PROD BUG — upload u Ulaz dokumenata pada za sve korisnike: apiFetch šalje Content-Type: application/json s FormData body, server multipart ruta odbija. Fix: JSON header default samo za string/prazan body. RCA: evidence/incident-inbox-upload-20260823/RCA.md; forged prompt 900178.md.

# bilko.cloud landing truth and CTA remediation — MC #106174/#107324 — 2026-08-18

# bilko.cloud landing truth and CTA remediation — MC #106174/#107324

**Date:** 2026-08-18 · **Status:** candidate branch validated; **NOT merged, NOT deployed, NOT live**

## Root cause

The earlier honest-claims corrections existed on commits `149bb1a0` and `d0f94baa` but were never merged into canonical Azure DevOps main. MC #107298 later hardened and deployed the runtime-only landing tree from main, which still carried stale beta, PSD2, trial and pricing copy. Browser and byte-hash evidence proved live `bilko.cloud` was identical to green main `390bbb21`.

## Candidate

- Branch: `fix/106174-landing-hr-truth`
- Base: `390bbb2171027478dabb2d6c9f5679cc3e5d9054`
- Candidate HEAD: `06acc46db1376f5f9726263fd0262445ec4912af`
- Runtime source: `apps/landing-hr/public/index.html`
- Changed scope: runtime HTML plus two focused tests only; MC #107298 headers, public-root controls, Functions and deployment files untouched.

## What changed

- Removed eRačun beta/coming-soon, PSD2 auto-sync, 7-day/no-card trial and retired registration claims.
- Primary path is the verified instant HR demo; high-intent CTAs go to access/contact.
- Public copy distinguishes offline HR-CIUS/UBL preparation from inactive, unverified production sending.
- Removed placeholder 30/100 quotas, PD-O/ZP filing claims, unconditional OCR/AI, public API/webhooks/Google SSO, broad accounting/export/currency promises, migration SLA and absolute GDPR/AZOP/security claims.
- Preserved confirmed prices €9, €19 and +€18 while separating unresolved provider/overage charges.
- Owner/accountant wording promises prepared reviews/exports, not unsafe shared accountant access.

## Verification

- Landing/public security check: PASS, 55 files, 0 errors.
- Node tests: 20/20 PASS.
- Builder browser proof: mobile/tablet/desktop PASS.
- Independent Finverge round 2: PASS, P0=0/P1=0.
- Independent cross-vendor Proveo: Playwright CLI 3/3 PASS at 390/768/1440, P0=0/P1=0, real demo CTA click, no request mocks.
- Worktree clean; exactly two landing-scoped commits.

## Evidence

Durable evidence is stored under the local MC 106174 evidence root in the live-baseline, builder, finverge-review-round2 and proveo-round3 directories. The forged prompt is stored under the local forged-prompts registry for MC 106174.

## Release boundary

This page documents a verified candidate. Production remains on stale main copy until branch push, Azure PR validation, CEO preview approval, merge and a separately verified Cloudflare Pages deploy. Do not describe this work as live before those steps.