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- ) 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 , message from 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 What: Two buttons on expense form ( /expenses/new ) have zero functionality: "Skeniraj racun" (Scan Receipt) — no onClick handler, 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 . Connect to expense API upload endpoint. Store receipt URL in expenses.receipt_url column. Future (Phase 2): Wire "Skeniraj racun" to navigator.mediaDevices.getUserMedia for camera access. Add OCR pipeline (Textract or similar). Interim: Change "Skeniraj racun" button label to "Skeniraj racun (uskoro)" with disabled state. 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_organizations junction table (user can belong to multiple orgs) Add GET /users/me/organizations endpoint Add POST /users/me/switch-organization endpoint (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_rates table) 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_format column 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 main branch (web) Register trigger for main branch (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