Skip to main content

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 declares VARCHAR(50). Kotlin INSERT with string "owner" → PostgreSQL rejects with type error.
  • API field name mismatch: Web sends organizationName, Kotlin API expects orgName.

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":"[email protected]","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 no onClick handler. No camera access, no OCR, no file picker.
  • "Prikaci racun" (Paperclip) also has no onClick and 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:

  • No mobile sidebar Sheet pattern (raw div overlay, no animation)
  • Invoice wizard desktop-optimized (4-column grid → 70px inputs on 390px iPhone)
  • Only 3 responsive breakpoint uses in ~1,200-line wizard
  • Step labels hidden on mobile (hidden sm:inline → numbered dots with no context)
  • Date picker label misalignment on iOS Safari small screens
  • No service worker / localStorage draft persistence (network loss = lost work)

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)

  1. No email delivery of invoices (Step 6 UI is cosmetic)
  2. No OCR receipt scanning (button exists, zero capability)
  3. No Bosnian/Serbian bank CSV format support
  4. No SEF integration UI for new users (Settings "Integracije" present but no SEF config screen)
  5. No registration completion flow (blocked by P0 bugs)
  6. No offline/draft persistence (wizard loses data on refresh or connectivity loss)
  7. No payment link/QR code on invoice
  8. No onboarding validation (flow exists, untestable due to P0 block)
  9. No mobile-optimized dashboard (charts overflow/compress at 390px)
  10. 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-19 exactly
  • Express includes uptime, timestamp, db fields (absent from stage)
  • DEPLOY-MAP.md line 34: Dockerfile.api-kotlin, image bilko/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)

  1. No CI pipeline for Kotlin API (Cloud Build deploys web only)
  2. DB public IP, no SSL/IAM (TD-3, MC #10241 blocker)
  3. Compliance endpoints auth-gated (/pausal/rates returns 401 — should be public)
  4. No Prometheus metrics endpoint (Kotlin has no /metrics route)
  5. No automated overdue invoice scheduler
  6. Serbian CoA seeding absent from registration
  7. No SLOs defined or measured
  8. Rollback runbook references Vercel (wrong platform)
  9. Email verification not implemented
  10. SEF stub in production path (legal compliance risk)
  11. X-Powered-By header absent (consistency gap, not security issue)
  12. Invite acceptance endpoint missing

Top 5 Architectural Risks

  1. Split-backend contract drift with zero CI enforcement — Stage Kotlin, prod Express (or nothing). Web pipeline only. Contract drift invisible.
  2. SEF stub in production — Serbian e-invoicing law mandates real SEF submission. Stub invoices = legally non-compliant.
  3. Public IP database, no IAM authpostgres-socket-factory absent, no SSL enforcement (TD-2/TD-3)
  4. No automated schema validation — Prisma/Exposed/Flyway independently maintained; confirmed drifts exist
  5. 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