# V41 QA — end-to-end test output

Run 1: fresh install from database/install_full.sql · Run 2: V40 database with V40-created data → v41_migration.sql. Both: 143 passed, 0 failed.
Browser (Chromium): desktop + mobile tour of every page, online & offline sale with variants, subscription confirmation → super admin approval — no JS errors.

```

== Super admin & onboarding
  ✔ super admin login
  ✔ super admin mutation without valid CSRF → 419
  ✔ onboard tenant A (BUSINESS)
  ✔ onboard tenant B (CLOUD)
  ✔ duplicate owner email rejected

== Login, session, CSRF, throttling
  ✔ owner A login
  ✔ session returns branches + permissions
  ✔ POST without CSRF token → 419
  ✔ brute force: 6th wrong password → 429

== Multi-branch (V39: fatal "Cannot redeclare")
  ✔ branches.php responds (no fatal)
  ✔ create branch 2
  ✔ owner-dashboard.php responds

== Inventory
  ✔ 3 products created
  ✔ new product registered in all branches
  ✔ duplicate barcode rejected
  ✔ stock adjusted to 10
  ✔ negative stock blocked
  ✔ variant create (V39: unknown column name)

== Device activation & POS checkout
  ✔ unactivated device blocked
  ✔ owner activates device without key
  ✔ catalog for offline use
  ✔ barcode search
  ✔ checkout ok, total from server prices
  ✔ same operation_uuid is idempotent
  ✔ oversell via duplicate lines blocked (V39 bug)
  ✔ discount > subtotal rejected
  ✔ cash underpayment rejected
  ✔ unknown payment method rejected
  ✔ stock ledger rows written once

== Tenant isolation (V39: IDOR via business_id/branch_id)
  ✔ B cannot search A products
  ✔ B cannot sell from A branch
  ✔ B cannot sell A product id in own branch
  ✔ B dashboard excludes A revenue (V39 showed all tenants)
  ✔ B cannot edit A variant
  ✔ CLOUD plan: multi-branch blocked
  ✔ A dashboard sees own omzet

== Roles & permissions
  ✔ owner creates ADMIN
  ✔ owner creates CASHIER
  ✔ ADMIN cannot promote self to OWNER (V39 bug)
  ✔ ADMIN cannot promote cashier to ADMIN
  ✔ ADMIN cannot deactivate OWNER
  ✔ cashier cannot adjust stock
  ✔ cashier inventory view hides cost price
  ✔ cashier blocked from admin center
  ✔ owner override grants cashier permission
  ✔ cashier can create opname

== Cash session (V39: close crashed)
  ✔ open cash session
  ✔ close: expected = 100000 + 10000 cash sales − 3000 = 107000, diff −500

== Hold / resume
  ✔ hold cart
  ✔ resume returns cart
  ✔ cannot resume twice

== Stock opname (V39: double posting)
  ✔ re-count overwrites (1 row)
  ✔ cashier cannot post opname
  ✔ owner posts opname
  ✔ stock = counted (7) regardless of sale in between
  ✔ second post rejected

== Stock transfer
  ✔ create transfer
  ✔ ship uses stored from-branch (ignores body)
  ✔ receive adds to branch 2
  ✔ transfer written to ledger (out+in)
  ✔ ADMIN without access to branch 2 blocked

== Offline sync (CLOUD feature)
  ✔ valid offline sale APPLIED
  ✔ price drift noted
  ✔ unknown product → REJECTED (no infinite retry)
  ✔ insufficient stock → FAILED (retryable)
  ✔ re-push: APPLIED is duplicate
  ✔ retry capped at 5 attempts → REJECTED
  ✔ other tenant cannot touch A operations
  ✔ pull lists device operations

== Billing & webhooks (V39: wrong table / missing columns)
  ✔ create invoice (V39: unknown column license_id)
  ✔ bad signature → 401
  ✔ underpaid → 422 + FAILED
  ✔ provider retry of failed event is re-processed
  ✔ license extended by plan duration (365d)
  ✔ processed event → duplicate
  ✔ adapter webhook settles saas_licenses using plan days, ignoring payload days

== License lifecycle
  ✔ super admin suspends B
  ✔ suspended tenant gets 402
  ✔ renew (V39: failed on saas_renewals columns)
  ✔ renewed tenant works again
  ✔ cron over HTTP without token → 403
  ✔ cron with token

== V41 · Settings, tax & service
  ✔ owner saves settings
  ✔ admin cannot change settings
  ✔ tax+service computed: 9000 + 450 + 1040 = 10490

== V41 · Categories, non-stock menu items, variants
  ✔ create category
  ✔ duplicate category rejected
  ✔ non-stock item sells with 0 stock
  ✔ variants created
  ✔ product with variants requires variant
  ✔ variant without stock blocked
  ✔ search finds product by variant barcode

== V41 · Suppliers & purchases (stock in)
  ✔ create supplier
  ✔ purchase posted, total correct
  ✔ variant stock increased (M=5, XL=3)
  ✔ product cost updated from purchase
  ✔ cashier cannot create purchase
  ✔ sell variants with own prices (2×55000 + 1×50000)

== V41 · Returns / refunds
  ✔ cashier cannot refund by default
  ✔ partial refund 1×XL = 55000, stock back
  ✔ cannot return more than sold
  ✔ full refund of remainder = 105000, status REFUNDED
  ✔ refunded transaction cannot be refunded again
  ✔ other tenant cannot refund
  ✔ cashier history shows only own sales

== V41 · Cash session with refunds
  ✔ expected = 50000 + 20000 − 5000 refund = 65000, diff 0

== V41 · Opname & transfer with variants
  ✔ count variant
  ✔ count without variant rejected
  ✔ variant stock set by opname (M=7)
  ✔ variant transferred (A1 M=5, A2 M=2)

== V41 · Import products
  ✔ import: 2 created, 1 updated, 1 failed
  ✔ import set stock + auto category
  ✔ import updated price by SKU

== V41 · Reports & dashboard
  ✔ summary: net = gross − returns
  ✔ report daily
  ✔ report products
  ✔ report cashiers
  ✔ report transactions
  ✔ CSV export (Excel-ready, ; separated)
  ✔ cashier cannot view reports
  ✔ owner dashboard works on CLOUD plan (V40 required MULTI_BRANCH)
  ✔ dashboard trend has 14 days + low stock

== V41 · Manual billing
  ✔ owner sees subscription
  ✔ owner requests renewal invoice
  ✔ second request returns existing unpaid invoice
  ✔ admin cannot submit payment
  ✔ owner submits confirmation
  ✔ only one pending confirmation
  ✔ approve underpaid confirmation fails
  ✔ super admin rejects
  ✔ owner sees rejection note
  ✔ super admin approves
  ✔ license extended 365 days on approval
  ✔ super admin lists approved confirmations
  ✔ plan price/limits updated
  ✔ billing instructions saved

== V41 · Grace period
  ✔ 1 day after expiry: still usable, flagged in_grace
  ✔ after grace (5 days): blocked 402
  ✔ billing page still reachable when expired
  ✔ pay while expired → reactivated (from today)

== Error hygiene
  ✔ unknown action → clean error code
  ✔ logout
  ✔ after logout → 401

ALL PASSED: 143 passed, 0 failed
```
