# 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:**
```kotlin
// 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 `<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

```mermaid
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