UAT Phase 1 Findings
UAT Phase 1 Findings
MC: #10487
Date: 2026-05-02
Stage web: https://bilko-web-stage-dh4m46blja-lz.a.run.app
Stage API: https://bilko-api-stage-dh4m46blja-lz.a.run.app
Verdict: ACCEPT-WITH-FOLLOWUP
Methodology
Three-agent pure UAT discovery on Bilko stage environment (post-Kotlin migration, pre-Express deletion). No build work — observational only.
Team:
- maria-santos — Real-user SMB persona walkthrough (mobile UX + time-to-first-invoice)
- petter-graff — Architecture gap analysis (docs vs code vs schema)
- angie-jones — Functional smoke per epic (INCOMPLETE — deferred to MC #10500)
Top P0 Findings (Blocking Go-Live)
1. Registration Fails — Dual Bug
Root causes:
- DB ENUM type mismatch: Prisma migrations created PostgreSQL ENUM
"UserRole"but Kotlin Flyway V1 declaresVARCHAR(50). Kotlin INSERT with string"owner"→ PostgreSQL rejects with type error. - API field name mismatch: Web sends
organizationName, Kotlin API expectsorgName.
Impact: Zero users can register. Product completely inaccessible to new users.
Evidence:
curl -X POST https://bilko-api-stage-dh4m46blja-lz.a.run.app/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@test.ba","password":"test1234","fullName":"Test User","orgName":"Test DOO","country":"BA","baseCurrency":"BAM"}'
→ HTTP 500: {"error":"PSQLException: column \"role\" is of type \"UserRole\"
but expression is of type character varying","code":"INTERNAL_ERROR"}
Followup: MC #10494
2. Invoice Email Send — UI Only, No Dispatch
What: Invoice wizard Step 6 shows email compose fields (To, Subject, Message, "Send me a copy"). InvoiceService.sendInvoice() changes status to "sent" but does NOT send email. The emailData state is never passed to API.
Impact: Customer never receives invoice. User believes invoice was sent. Creates chargeback disputes and relationship damage when client claims "nisam dobio nista."
Evidence: apps/api/src/main/kotlin/no/alai/bilko/services/InvoiceService.kt — no email service injection in DI container. sendInvoice() returns success without SMTP call.
Followup: MC #10495 (Sprint 1 P0)
3. Dead UI Buttons — Receipt Scan + Attach
What:
- "Skeniraj racun" button (
/expenses/new) has noonClickhandler. No camera access, no OCR, no file picker. - "Prikaci racun" (Paperclip) also has no
onClickand no backing<input type="file">.
Impact: The mobile expense entry feature — marketed as a selling point — is non-functional. Field users will abandon immediately. RS/BA tax law requires receipt documentation for deductible expenses; without attachment capability, Bilko cannot support compliance.
Evidence:
apps/web/app/(dashboard)/expenses/new/page.tsx— button is styled<button>with Camera icon but zero interactivity- maria-santos UX report: iPhone Safari VoiceOver announces "Skeniraj racun, button" — activating it does nothing
Followup: MC #10495 (Sprint 1 P0)
4. Bank CSV Format Incompatibility
What: CSV import expects exact format: date,description,amount,reference (ISO 8601 date, comma-separated). Real Bosnian/Serbian banks export semicolon-separated with local date formats: Raiffeisen BA (DD.MM.YYYY), UniCredit RS (local thousand separators), Intesa RS (multi-line headers).
Impact: User exports from Raiffeisen online banking, uploads CSV, gets 0 imported rows. Feature reads as broken. Bank reconciliation is effectively non-functional for Balkan users.
Evidence: apps/api/src/main/kotlin/no/alai/bilko/services/BankingService.kt:303-354 — generic parser, no named bank format presets
Followup: MC #10496 (Sprint 2)
Mobile UX Score: 5/10
Why not lower: Clean UI, readable fonts, BS/SR/HR localization present, adequate color contrast.
Why not higher:
Localization Status
Good news: BS, SR-Latn, SR-Cyrl, HR, EN all have complete translation files. Default sr-Latn. Language switcher (globe icon) present.
Issues:
- sr-Latn dialect inconsistency: Mixes Bosnian and Serbian forms ("dospijeva" [BS] alongside "dospeva" [RS/SR], "mjesec" [BS] vs "mesec" [RS]). File appears half-written for RS, half for BA.
- No Cyrillic variant shown to BA users (but some BA users from RS prefer Cyrillic).
- "Skeniraj racun" label is actively misleading — button does nothing.
Friction Summary
| Severity | Count |
|---|---|
| P0 (blocks all use) | 2 |
| P1 (core workflow broken) | 4 |
| P2 (significant friction/trust damage) | 6 |
| P3 (polish/consistency) | 4 |
Full list: 16 friction points documented in /tmp/bilko-uat-ux-10487.md
Missing Features (vs SMB Expectations)
- No email delivery of invoices (Step 6 UI is cosmetic)
- No OCR receipt scanning (button exists, zero capability)
- No Bosnian/Serbian bank CSV format support
- No SEF integration UI for new users (Settings "Integracije" present but no SEF config screen)
- No registration completion flow (blocked by P0 bugs)
- No offline/draft persistence (wizard loses data on refresh or connectivity loss)
- No payment link/QR code on invoice
- No onboarding validation (flow exists, untestable due to P0 block)
- No mobile-optimized dashboard (charts overflow/compress at 390px)
- No forgot-password email confirmation (shows "Provjerite vas email" but no evidence of dispatch)
Architecture Findings (petter-graff)
Backend Disambiguation: KOTLIN/KTOR
Evidence:
- Stage health check:
{"status":"ok","service":"bilko-api","version":"1.0.0"} - Matches
apps/api/src/main/kotlin/.../routes/HealthRoutes.kt:14-19exactly - Express includes
uptime,timestamp,dbfields (absent from stage) - DEPLOY-MAP.md line 34:
Dockerfile.api-kotlin, imagebilko/api:stage-1f48fdc
Conclusion: Stage is confirmed Kotlin. Express docs are stale.
User Stories Implementation Matrix
15 stories tracked (not 17 — task brief was imprecise). IDs: US-001..004, US-010..012, US-020, US-030..031, US-040, US-050, US-060..061, US-070.
Summary:
- 0 fully implemented
- 8 partial
- 7 with critical gaps
P0 gaps (3):
- US-001: Email verification absent; Serbian CoA seeding not called
- US-003: Invite endpoint not mounted in Kotlin routing
- US-011: SEF stub only (
SEF-STUB-<id>), no real efaktura.gov.rs HTTP
P1 gaps (4):
- US-002: Lockout message English (not Serbian)
- US-004: No multi-org support
- US-012: No automated overdue detection scheduler
- US-050: No ePorezi export format, no PDV reminder
Schema Reality (Prisma vs Kotlin Exposed vs Flyway)
Model count drift:
- Prisma: 22 models
- Kotlin Exposed: ~18 table objects
- Flyway: 18 CREATE TABLE (V1)
- DEPLOY-MAP.md: claims 24 tables on stage DB
Specific drifts:
| Item | Prisma | Kotlin Exposed | Flyway | Gap |
|---|---|---|---|---|
pausal_rates |
PausalRate model | No Exposed Table (data class only) | V4 | GAP: Kotlin may use raw SQL |
archive_jobs |
ArchiveJob model | Comment: "Round 2" | V4 | GAP: ArchiveService uses in-memory, DB table unused |
sefDocumentId, sefAcceptedAt |
Present in Prisma Invoice | Deferred to "Round 2" in Kotlin | Not in Flyway | DRIFT: Prisma has fields, Flyway/Kotlin defer |
Organization address |
Absent | Comment: "Round 2" | Absent | Missing from all three |
Production Readiness Gaps (12 observational)
- No CI pipeline for Kotlin API (Cloud Build deploys web only)
- DB public IP, no SSL/IAM (TD-3, MC #10241 blocker)
- Compliance endpoints auth-gated (
/pausal/ratesreturns 401 — should be public) - No Prometheus metrics endpoint (Kotlin has no
/metricsroute) - No automated overdue invoice scheduler
- Serbian CoA seeding absent from registration
- No SLOs defined or measured
- Rollback runbook references Vercel (wrong platform)
- Email verification not implemented
- SEF stub in production path (legal compliance risk)
- X-Powered-By header absent (consistency gap, not security issue)
- Invite acceptance endpoint missing
Top 5 Architectural Risks
- Split-backend contract drift with zero CI enforcement — Stage Kotlin, prod Express (or nothing). Web pipeline only. Contract drift invisible.
- SEF stub in production — Serbian e-invoicing law mandates real SEF submission. Stub invoices = legally non-compliant.
- Public IP database, no IAM auth —
postgres-socket-factoryabsent, no SSL enforcement (TD-2/TD-3) - No automated schema validation — Prisma/Exposed/Flyway independently maintained; confirmed drifts exist
- Compliance public endpoint regression — Pausal calculator returns 401, blocks landing-page GTM feature
AC1 Followup: angie-jones Functional Smoke
Status: INCOMPLETE
Evidence: /tmp/bilko-uat-bugs-10487.json = 2 bytes ({})
Expected: Structured JSON with ≥8 epic entries, each with screenshots + HAR + repro steps
Followup: MC #10500 (re-run with HAR/screenshots for 8 epics AFTER Sprint 0 lands so login works)
References
Source MCs:
- MC #10487 — UAT Phase 1 (this page)
- MC #10493 — Express deletion
- MC #10494 — Sprint 0 P0 registracija
- MC #10495 — Sprint 1 P0 (SEF/email/receipt)
- MC #10500 — angie-jones re-run
Evidence files:
/tmp/bilko-uat-ux-10487.md(22KB, maria-santos)/tmp/bilko-uat-gap-10487.md(21KB, petter-graff)/tmp/sentinel-verify-10487.md(sentinel verdict)/tmp/proveo-10487-postflight.json
Bilko repo: https://github.com/johnatbasicas/bilko