Skip to main content

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.

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.ktsendInvoice() 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 <input type="file" accept="image/*,application/pdf">. 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