Sprint 1-3 + Arch Roadmap
Sprint 1-3 + Arch Roadmap
Sprint program context: Post-CEO Bilko triage 2026-05-02
Phase: Bug-fix sprint following UAT Phase 1 findings (MC #10487)
Sprint 1 (MC #10495) — P0 Legal + Email + Dead UI
Status: OPEN (H priority)
Owner: TBD (awaiting dispatch)
Scope
Three high-severity bugs blocking go-live:
1. SEF Stub → Real e-Invoicing Integration
Current state: SefService.kt issues stub sefId (SEF-STUB-<id>) and marks invoices as "submitted" without ever contacting efaktura.gov.rs.
Legal risk: Serbian e-invoicing law (2023) mandates submission of all B2B invoices to SEF. Stub invoices = non-compliant. If Kotlin backend promoted to prod without completing this, every "sent" invoice will have fake sefId.
Code location: apps/api/src/main/kotlin/no/alai/bilko/services/SefService.kt:180-200
Round 2 comment in code:
// Round 2 replaces stub with real Ktor CIO HTTP call to efaktura.gov.rs
// Retry queue already persisted — just wire up actual SEF submission
Decision pending CEO:
- Option A: Real efaktura.gov.rs integration (requires SEF credentials, test environment access, XML signature setup)
- Option B: Honest "not yet active" banner in UI + block invoice "send" action until SEF configured (safer short-term path)
Blocker: Without CEO decision, cannot proceed. Integration effort = 3-5 days (XML schema, auth flow, retry handling).
2. Invoice Email Delivery
Current state: Invoice wizard Step 6 shows email compose UI (To, Subject, Message, "Send me a copy"). InvoiceService.sendInvoice() changes invoice status to "sent" but does NOT dispatch email.
Impact: Customer never receives invoice. User believes invoice was sent. Creates chargeback disputes when client says "nisam dobio nista."
Code gap: No email service injection in DI container (DI.kt). No SMTP/Resend/SendGrid integration exists in Kotlin backend.
Evidence: apps/api/src/main/kotlin/no/alai/bilko/services/InvoiceService.kt — sendInvoice() function has no email dispatch logic.
Acceptance criteria:
- Wire email service (Resend recommended — already used in other ALAI products)
- Pass
emailTo,subject,messagefrom wizard Step 6 to API - Attach invoice PDF to email
- Return email delivery confirmation (message ID)
- Show interim banner until wired: "Faktura je snimljena — email slanje još nije aktivno. Preuzmite PDF i pošaljite ručno."
3. Dead UI Buttons — Receipt Scan + Attach
- "Skeniraj racun" (Scan Receipt) — no
onClickhandler, no camera access, no OCR - "Prikaci racun" (Attach Receipt, Paperclip icon) — no
onClick, no file input
Impact: Mobile expense entry — a marketed selling point — is non-functional. RS/BA tax law requires receipt documentation for deductible expenses; without attachment, Bilko cannot support compliance.
UX deception: "Skeniraj racun" button is 120px tall, full-width, purple, most dominant UI element on form. Tapping it does nothing. On iPhone Safari with VoiceOver, announces "Skeniraj racun, button" but zero feedback on activation.
Acceptance criteria:
- Minimum viable (Phase 1): Wire Paperclip button to
<input type="file" accept="image/*,application/pdf">. Connect to expense API upload endpoint. Store receipt URL inexpenses.receipt_urlcolumn. - Future (Phase 2): Wire "Skeniraj racun" to
navigator.mediaDevices.getUserMediafor camera access. Add OCR pipeline (Textract or similar). - Interim: Change "Skeniraj racun" button label to "Skeniraj racun (uskoro)" with
disabledstate. Never show interactive UI for non-functional features.
Sprint 2 (MC #10496) — P1 Core Workflow
Status: OPEN (M priority)
Owner: TBD
Scope
Four P1 gaps affecting core accounting workflows:
1. Chart of Accounts Seeding on Org Creation
Gap: CountryService.seedChartOfAccounts() exists (CountryService.kt:220) but is NOT called from AuthService.register().
Impact: New organizations have empty chart of accounts. US-030 AC1 requires "all 10 account classes present on new Serbian org" — currently fails.
Fix: Add countryService.seedChartOfAccounts(organizationId, country) call after org creation in AuthService.register().
2. Multi-Org Switcher
Gap: Each user has single organizationId. No UI switcher, no API endpoint to switch active org.
Impact: Accountants managing multiple companies (common in SMB Balkans) must log out/in with different emails. US-004 unmet.
Acceptance criteria:
- Add
user_organizationsjunction table (user can belong to multiple orgs) - Add
GET /users/me/organizationsendpoint - Add
POST /users/me/switch-organizationendpoint (updates session active org) - Add org switcher dropdown in top-bar (web)
3. EUR Exchange Rate Fetch from Central Bank
Gap: InvoiceService.getExchangeRate() exists (InvoiceService.kt:972-990) but has TODO comment: "fetch from NBS API for RS, CBBiH for BA."
Current behavior: Returns hardcoded 1.0 or throws error if currency not BAM/RSD.
Impact: Foreign currency invoices (EUR, USD) cannot be created. US-070 partially unmet.
Acceptance criteria:
- Wire to RS central bank XML API:
https://www.nbs.rs/kursnaListaModul/zaXML.faces - Wire to BA central bank:
https://www.cbbh.ba/CurrencyExchange/GetJson - Cache rates daily (store in
exchange_ratestable) - Fallback: prompt for manual entry if API unavailable (US-070 AC3)
4. Bank CSV Parser — Balkan Format Presets
Gap: CSV import expects generic date,description,amount,reference format. Real banks: Raiffeisen BA (semicolon, DD.MM.YYYY), UniCredit RS (local thousand separators), Intesa RS (multi-line headers).
Impact: User exports from real bank, uploads CSV, gets 0 imported rows. Feature reads as broken.
Acceptance criteria:
- Add named bank format presets: Raiffeisen BA, UniCredit RS, Intesa RS, ProCredit BA, OTP BA
- Each preset: delimiter, date format, column positions, header skip count
- OR: CSV mapping UI where user defines which column = date/amount/description
- Store selected bank format in
bank_accounts.import_formatcolumn
Sprint 3 (MC #10497) — P2 Polish
Status: OPEN (L priority)
Owner: TBD
Scope
Three P2 UX polish items:
1. sr-Latn Translation Pass (Dialect Consistency)
Gap: apps/web/messages/sr-Latn.json mixes Bosnian and Serbian forms: "dospijeva" (BS) alongside "dospeva" (RS/SR), "mjesec" vs "mesec", "Postavke" vs "Podešavanja."
Decision: sr-Latn targets RS market (pure Serbian ekavica). Bosnian forms belong in bs.json.
Acceptance criteria:
- Audit
sr-Latn.json— replace Bosnian ijekavica with Serbian ekavica - "mjesec" → "mesec", "sedmično" → "nedeljno", "dospijeva" → "dospeva"
- QA review by Serbian native speaker
2. Mobile UX Micro-Fixes
Gaps (from maria-santos report):
- Invoice wizard step labels hidden on mobile (
hidden sm:inline) - Line item inputs ~70px wide on 390px iPhone (grid-cols-2 sm:grid-cols-4)
- Sidebar overlay no slide animation (raw div, no CSS transition)
- Date picker label misalignment on iOS Safari small screens
- Language switcher tap target 36px (WCAG 2.1 AA requires 44px)
- Amount inputs missing
inputmode="decimal"(iOS numeric keyboard has no decimal separator)
Acceptance criteria: Address top 3 (step labels, line item width, sidebar animation).
3. Dashboard Empty States + Error Message Clarity
Gap: Dashboard shows Recharts widgets with no data → renders blank/broken-looking on first login. No empty state illustrations/guidance.
Acceptance criteria:
- Add empty state for "No invoices yet" (illustration + CTA "Create first invoice")
- Add empty state for "No expenses yet"
- Improve API error messages (currently generic "Registration failed. Please try again." with no timing/retry info)
Arch Roadmap (MC #10498) — Infrastructure + CI
Status: OPEN (M priority)
Owner: TBD
Scope
Four architectural/infra improvements:
1. Kotlin CI Pipeline
Gap: cloudbuild.yaml deploys web only. Kotlin API has no CI (manual deploy only).
Risk: Regression in apps/api/ invisible to CI. If developer pushes breaking change to main, web gets deployed, API does not. Two can silently diverge.
Acceptance criteria:
- Add Cloud Build trigger for
apps/api/**path changes - Build steps:
./gradlew test,./gradlew build,docker build -f Dockerfile.api-kotlin - Push image to GCR
- Deploy to stage Cloud Run
bilko-api-stage(auto) - Deploy to prod requires manual approval (Cloud Build approval gate)
2. RUNBOOK.md Fix (Still References Vercel)
Gap: docs/operations/OPERATIONAL-RUNBOOK.md:23,59,66,120 references Vercel (vercel --prod, vercel rollback, vercel env add) as deployment platform.
Reality: Deployment is GCP Cloud Run + Cloud Build. Vercel not used.
Acceptance criteria:
- Replace Vercel commands with Cloud Run equivalents (
gcloud run deploy,gcloud run revisions list,gcloud run services update-traffic) - Add rollback procedure for Kotlin API (currently only web rollback documented)
- Update SLA report template with actual Cloud Run metrics endpoints
3. .gcloudignore Optimization (MC #10504)
Gap: Cloud Build web deploy uploads 492MB (includes apps/api/build/, .gradle/, node_modules/ from all workspaces).
Impact: Upload timeout on slow connections (3+ minutes on Build step 1/24).
Acceptance criteria:
- Add to
.gcloudignore:apps/api/build/ apps/api/.gradle/ **/node_modules/ **/.next/ **/dist/ - Verify upload size reduction (target <100MB)
4. Cloud Build Trigger Registration (Auto-Deploy on Push)
Gap: Cloud Build triggers are manual (gcloud builds submit) per deploy. No auto-trigger on git push origin main.
Acceptance criteria:
- Register Cloud Build GitHub App trigger for
mainbranch (web) - Register trigger for
mainbranch (api, once Kotlin CI pipeline wired) - Add approval gate for prod deploys (manual Cloud Build approval step)
Dependency Diagram
graph TB
UAT[UAT Phase 1 #10487] --> Express[Express Deletion #10493]
Express --> Sprint0[Sprint 0 #10494]
Sprint0 --> WebDockerfix[Web Dockerfile #10505]
WebDockerfix --> Sprint1[Sprint 1 #10495]
Sprint1 --> Sprint2[Sprint 2 #10496]
Sprint1 --> Sprint3[Sprint 3 #10497]
Sprint0 --> Arch[Arch #10498]
Sprint1 --> AngieRerun[Angie Re-run #10500]
Sprint0 --> ProdCutover[PROD CUTOVER #10502]
Arch --> gcloudignore[.gcloudignore #10504]
style UAT fill:#e1f5ff
style Express fill:#c8e6c9
style Sprint0 fill:#c8e6c9
style WebDockerfix fill:#c8e6c9
style Sprint1 fill:#fff9c4
style Sprint2 fill:#fff9c4
style Sprint3 fill:#fff9c4
style Arch fill:#fff9c4
style ProdCutover fill:#ffccbc
style AngieRerun fill:#f3e5f5
style gcloudignore fill:#f3e5f5
Legend:
- Blue: Discovery (UAT)
- Green: DONE
- Yellow: OPEN
- Orange: BLOCKER
- Purple: Followup
References
MCs:
- MC #10487 — UAT Phase 1
- MC #10493 — Express deletion (DONE)
- MC #10494 — Sprint 0 (DONE)
- MC #10495 — Sprint 1 (OPEN, H)
- MC #10496 — Sprint 2 (OPEN, M)
- MC #10497 — Sprint 3 (OPEN, L)
- MC #10498 — Arch roadmap (OPEN, M)
- MC #10500 — angie-jones re-run (OPEN, M)
- MC #10502 — PROD CUTOVER (OPEN, H, BLOCKER)
- MC #10504 — .gcloudignore (OPEN, M, child of #10498)
- MC #10505 — Web Dockerfile regression (DONE)
Evidence:
/tmp/bilko-uat-ux-10487.md— UX findings (maria-santos)/tmp/bilko-uat-gap-10487.md— Architecture gaps (petter-graff)- USER-STORIES.md — Acceptance criteria source