Skip to content

MVP completeness — cross-lane inventory, 2026-09-12

Purpose: answer “what is left before MVP” with evidence rather than impression, so the remaining work can be delegated. Compiled from seven lane inventories in one night; every lane read code or a running system rather than recalling. Scope is frozen by PDR-0001 (VAS + Clientes IN; QR tenders, cobro rápido, POS interaction modes, KPI drill-down OUT) and PDR-0003 (VAS Phase 1 only).

Status: awaiting PO rulings (D1–D9 below). Not a plan until those are answered.

Almost nothing is unbuilt. Most of what remains is not running where it matters.

Four lanes arrived at that independently. The verification reconcile was live on the api and two commits behind on the web the human clicked. The VAS aggregator is M0–M12 merged and fails closed on every realm. The category migration has run on zero realms. A fresh merchant could not open a shift because the realm lagged main’s permissive default. A code-complete inventory is not an answer to “can we ship” — hence the per-realm deploy matrix, commissioned separately.

Three real build gaps survive that framing, and two of them are the things PDR-0001 explicitly put IN.

PDR-0001 names VAS as a core revenue surface the client needs day one. The designs exist (W41–W46); the web rebuild never landed — the legacy TypeORM surface is all there is. US-VAS-4 has no e2e and no Figma. This is the largest hole in the frozen scope, and it has no lane. Mobile VAS shipped; web did not.

G2 — Clientes exists as routes nobody can reach

Section titled “G2 — Clientes exists as routes nobody can reach”

/admin/customers and /admin/customers/[id] are built and have no UI entry point. The board marks US-CLI “not absorbed”; the web CUSTOMERS_WEB flag is dead (not referenced in src/) while the api’s CUSTOMERS_MODULE is true. Code, board and both flag planes disagree. PDR-0001 puts Clientes IN.

Found independently by the auth and onboarding lanes from opposite ends, which is why it is the most certain item here. Accept creates a membership and a PIN-less employee row but no password and no first session; setOwnPin needs a session that needs a password the invite never creates, so pin-login 401s. No endpoint, no ticket. The solo owner is unaffected — they are created as an employee at onboarding. If MVP includes non-owner staff, this is required.

Verification — CORRECTED 2026-09-12 late: the images do not decode

Section titled “Verification — CORRECTED 2026-09-12 late: the images do not decode”

Supersedes the section below. The “document front never reaches the engine” diagnosis was WRONG and was caught by insisting on a seam check before the fix. Reading the one real capture at rest (vs_76441da68c9f5a026c3404185610, the only prod session with stored capture_segments) shows document_front, document_back, selfie_still and selfie_video all present — and the checks RAN: document = CONSIDER with undecodable_image:front, undecodable_image:back, mrz_absent, portrait_crop_failed; face_match = CONSIDER with face_recapture_required.

The engine works and is reporting a real problem: the stored images do not decode. The earlier not_implemented / missing_id_portrait symptoms came only from SYNTHETIC step-only drives that stored no evidence at all, and were carried onto a real capture nobody had inspected.

Two of the three months-risks retire. What remains is one discriminator: open the stored front image. A readable image means the OCR cannot read a real Mexican INE (the long tail). A corrupt, truncated or re-encoded one means it is capture-side on the astro half (days, not months). Both front and back failing identically points at the pipeline, not the photography.

Verification — the superseded diagnosis, kept for the record

Section titled “Verification — the superseded diagnosis, kept for the record”

Every real capture lands needs_review: document check returns not_implemented, face match returns missing_id_portrait. The cause is pinned to the document front image never reaching the engine — an evidence-assembly gap in the verification pipeline, not a vendor gap and not thresholds. The engine checks are code-complete. Owner: the verification solution. Read: weeks, not months. An isolation repro at the engine boundary was requested before the fix is built.

CORRECTED — selling IS blocked. An earlier version of this document said a fresh merchant sells by default. That was wrong: the PO read allowUnverifiedSales from a stale local branch. On origin/main, merchant-provisioning.service.ts:229 stamps allowUnverifiedSales: false on every create path (createMerchant and the self-serve createOwnMerchant), locked by two specs. Since overrideOpen is allowUnverifiedSales !== false, a merchant is born gate-closed and cannot sell, open a shift, register a cash movement, invite staff or add a second store.

Combined with B1 (emails disabled everywhere), that is the real MVP position: a fresh merchant cannot trade and cannot self-verify to become able to — the contact leg needs an email that cannot be sent. The only path today is an out-of-band override.

Corroborating evidence from manual: of four merchants, the only one that has ever traded (13 finalize-sale audits) carries allowUnverifiedSales=true; the born-false one has a store and zero sales. And the audit log holds zero verification.override.opened rows across all 43 entries, so the three true values were written out-of-band — console, seed, or direct REST — not through the audited admin endpoint.

Lane State
merchants / verification Seams proven on manual; #718/#906 close a visible wrong percentage; declined-path proof needs one Alfredo window
conexiones / VAS aggregator M0–M12 MERGED and finished; dormant because VAS_AGGREGATOR_MODE is unset → fail-closed 503 on every realm. A deploy-and-activate problem
liquidaciones sale → settled → “sees balance owed” WORKS (proven with a real card charge). Withdraw does not exist for the merchant: DISPOSE_ENABLED=false per the A5 ruling, pending #53 + #54. UI ~80% built
onboarding Self-serve signup → POS works on current main, including silent terminal self-provision. Blockers are per-realm infra: SMTP, the onboarding flag, and running current code. Phone verify unwired (no SMS vendor)
auth Role model coherent in shape; two inconsistencies — gate↔data signal-less refusal, and membership-without-employee-row. Employee pickers code-closed, flag-gated only
n86 / mobile Proposes handheld MVP = cash+card sale with printed receipt, shift open/close, onboarding gate. Everything else after
verify (recon) 79 routes: 12 built-and-unreachable. ~11 flows specified-not-built. Flag baseline captured
# Decision Consequence
D1 Does MVP include non-owner staff (cashiers, supervisors)? If yes, G3 is required work at the auth↔onboarding↔merchants seam
D2 Self-serve payout (W76/W77) in MVP? UI ~2–4 days; real cost is deps #53 (bank verification) + #54 (platform inbound-credit). Today a merchant watches money accrue and cannot withdraw it
D3 Handheld depth — n86’s reduced MVP, or web-parity? Decides the mobile queue entirely
D4 VAS web (G1) — confirm IN per PDR-0001, or re-scope? It is the largest unbuilt item and currently has no owner
D5 Clientes (G2) — confirm IN? Cheap if yes: routes exist, they need an entry point and a flag decision
D6 Mobile discounts — re-enable or cut? Finished and deliberately hidden; a product call, not work
D7 Ship with escalate-always verification + permissive selling, or hold for the idFront fix? Decides whether launch waits weeks on another team
D8 CURRENT_TERMS_VERSION bump policy — who bumps, does it force re-acceptance? Undefined in code (TODO(human))
D9 Gift-cards admin list — finish or cut? Shipped dark behind a flag, mock-only, no user story. Contradicts feature-flag minimalism

Delegation blockers, independent of the rulings

Section titled “Delegation blockers, independent of the rulings”
  • 26 drafted tickets sit unuploaded in backlog/drafts/ with manifests. The fleet cannot see files. And they need a STALENESS CHECK before any of them is uploaded. Two are already known to predate a shipped decision: the web and mobile SIM-chip drafts propose a POST /vas/chips/sell route and an ICCID point-of-sale UI, but api #512 resolved chip sale through the ordinary cart line — a loop picking either up would build a phantom vertical nobody could reach. A draft describes the world on the day it was written, and this world has moved four times in one cycle.
  • 13 open issues are off the board, so no loop will pick them up.
  • Settlements is built with no user-story rows at all — the completeness board undercounts what exists.

Top-up is ledger-only (gap #50) — accepted for MVP by PDR-0003; real vendor crediting is post-MVP. Offline cash queue is post-MVP by PO ruling. Wallet withdrawal is a deliberate unbuilt seam.

Addendum — merchants sub-lane audit (same night, after the table above)

Section titled “Addendum — merchants sub-lane audit (same night, after the table above)”

G4 — a merchant cannot edit or delete a store. Only CREATE and a merchant-scoped LIST exist; there is no edit-store and no delete-store endpoint. A merchant who mistypes their store name at signup cannot correct it. If that is MVP, it is unbuilt work with no ticket. → D10

Resolved, no work needed: STORAGE_CANONICAL_BUCKETS is a no-op guard — no migration ever ran because there was nothing to migrate; manual and qa-design each hold exactly the five canonical buckets with no legacy ones (the only unknown is the realm mobile points at). WRITE_GATEWAY is default-ON everywhere and its coverage spans every tenant-write resource; OFF is the legacy client-direct path that caused the create("users") regression. White-label theming is built AND wired live. Deposits/wallet is substantially built with no MVP gap.

Platform-admin console is read + impersonate + the allowUnverifiedSales override. No merchant suspend/edit/delete. Demo-adequate, not an admin console — acceptable for MVP unless operations needs more.

# Decision Consequence
D10 Is editing (or deleting) a store in MVP? Unbuilt: no endpoint either way. A merchant cannot fix a mistyped store name today
D11 Does allowUnverifiedSales need a TTL? It has NO expiry, and since nothing auto-approves it is the only way any merchant trades today. Indefinite means unverified trading is permanent by default, not temporary

Bumping CURRENT_TERMS_VERSION lapses every merchant on an older version at once — each must re-accept. Three shapes to choose between: retroactive lapse-all, grandfather-new-only, or a grace deadline. How much it hurts depends on whether re-acceptance hard-blocks selling, which is part of the same ruling.

The web lane’s route/flow/flag recon is a companion document with a ruling checkbox per item. Its triangulations — findings three independent inventories agree on — are the part worth reading first.

G5 — the merchant wallet is built, live-green, and has no door. /admin/wallet works and US-BAL-1 shipped; nothing in the UI links to it. PDR-0003 puts wallet VISIBILITY in MVP. So this joins VAS web and Clientes: the third surface a PDR names as IN that a merchant cannot reach.

That is the pattern worth naming. Three of the gaps in the frozen scope are not unbuilt — they are built and unreachable. A completeness view that counts merged code would score all three as done, which is exactly why the route inventory (79 routes, 12 with no UI entry) found what seven lane inventories did not.

Environment note, now confirmed: qa-design’s api digest is byte-identical to stage-f4d8e98 and to stage’s — the deploy took, the tag merely reads :stage-latest locally. There was no discrepancy, and the flag baseline is trustworthy. Read digests, never tags.

Addendum 3 — the per-realm deploy matrix (the answer to “can we ship”)

Section titled “Addendum 3 — the per-realm deploy matrix (the answer to “can we ship”)”

Read from running containers by digest, with capability presence established by git ancestry of the introducing commit against the running SHA. Full matrix in team/workers/terminales-conexiones.md.

Running api images: manual = stage-12af9ac (5 behind tip) · qa-design = stage-f4d8e98 · stage = same digest · dev and qa-sh = stage-96f47a2, 120 commits behind, predating everything · prod = none.

B1 — emails are DISABLED on all five realms (DISABLE_EMAILS=1)

Section titled “B1 — emails are DISABLED on all five realms (DISABLE_EMAILS=1)”

The single biggest blocker, and it is configuration, not code. Verification’s contact/email gate, employee invites, password reset and magic links cannot complete on ANY realm. This reframes G3: the invitee first-password gap is real, but even a perfect invite could not have been delivered. Fix is SMTP plus DISABLE_EMAILS=0 per realm, or routing to the existing mailpit catcher. Human-gated config.

No single realm is end-to-end MVP-shippable

Section titled “No single realm is end-to-end MVP-shippable”
  • manual is closest — near-tip code, FEATURE_FLAG_MERCHANT_ONBOARDING ON (the only realm), deny-gate and self-provision and verification-reconcile present, and a real merchant already trading cash and card. Between it and full MVP trade: emails disabled; VAS deliberately fail-closed (money safety); 5 commits behind tip; and its Appwrite schema/backfill state UNKNOWN pending a realm read (commissioned).
  • qa-design is the only realm with VAS_AGGREGATOR_MODE=mantarys and sandbox credentials — VAS is being certified there — but onboarding is OFF, so store creation 404s, and it has no merchant.
  • stage has credentials staged and fail-closed; dev/qa-sh are 120 behind and predate everything.

So manual does core POS but not VAS or email; qa-design does VAS-sandbox but not onboarding or email. Neither is a merchant-trading MVP candidate today, and the gap in both cases is configuration and deployment rather than unbuilt code — the same conclusion the lane inventories reached from the other side.

Addendum 4 — the sell gate, the override provenance, and the verification cause (final)

Section titled “Addendum 4 — the sell gate, the override provenance, and the verification cause (final)”

sales.controller.ts:71 gates @Post('finalize') before any write, finalizeOrder awaits assertCanSell internally, and both clients finalize through that endpoint. The cart is client-direct under RLS by design, but a cart is not a sale. So “the POS shell renders” and “the merchant cannot complete a sale” are both true — the UI opens and finalize 403s, which is the open-then-403 UX defect already drafted, not a security gap.

Code is STRICTER than the design doc, and code wins. Shipped overrideOpen is allowUnverifiedSales === true (merchant-capabilities.service.ts:266); ux-design.md:86 specifies !== false. A null merchant is DENIED by the running code and permitted by the design. Correct the design doc to match the behaviour, which is the safer of the two.

GOVERNANCE — the three open overrides on manual are untraceable

Section titled “GOVERNANCE — the three open overrides on manual are untraceable”

manual runs seed.profile: none; all four merchants were created by real onboarding and born false; no repo script writes the override to true; the only api path that does is the audited platform-admin endpoint — and the audit log holds zero verification.override.opened rows across all 43 entries.

So the trading gate on the realm holding our only real merchant, card sale and settlement was opened by a direct REST or console write, bypassing the audited endpoint the bootstrap runbook documents. It cannot be pinned to a commit or a person. That needs a policy answer, not just a note.

Verification — FINAL cause, after four diagnoses

Section titled “Verification — FINAL cause, after four diagnoses”

The documents are captured as video. context.idFront and context.idBack point at 125KB WebM chunks; the engine’s document check calls Image.open() on a video and reports undecodable_image. Frame extraction runs for the SELFIE video (producing a clean 480×640 JPEG) but not for the document videos — the asymmetry is the bug.

No readable document still has ever existed, so the fix is not a repoint: extend frame extraction to the document videos, or capture documents as photos on the astro side. Real work, still days rather than months, and the OCR-accuracy long tail remains untested because the engine has never been handed a document it could open.

Three earlier diagnoses — missing evidence, bad image quality, a dangling storage ref — were each disproven by opening the next layer. None was built. The cost was a night; the alternative was shipping a fix for a cause that did not exist.

The error label is itself a defect: undecodable_image here means “handed a video, tried to open it as a still”. It must distinguish wrong-media-type from a corrupt still, or the next instance costs another night.

Addendum 5 — B1 RETRACTED: emails already work (to a catcher)

Section titled “Addendum 5 — B1 RETRACTED: emails already work (to a catcher)”

The “single biggest blocker” was not one. DISABLE_EMAILS gates nothing: src/env.ts:124 compares === 'true' and the value is '1', so the flag is inert and no send-branch reads it. Appwrite’s SMTP on every realm already points at the self-hosted appwrite-realms-mailpit. Proven, not inferred: mailpit already holds three real “Account Verification for Terminales manual” emails sent 2026-09-10.

Caught before acting: we were one step from redeploying five realms — including manual, which holds the only real merchant, card sale and settlement — to flip a variable that does nothing, and then reporting email as fixed.

The honest, narrower finding that replaces it: mailpit is a catcher, not delivery. Verification, invitations and password reset work end to end for our own addresses; no external customer receives any email until a real SMTP provider replaces mailpit. That is a decision for the PO, not a defect.

What this does and does not change:

  • The binding sequence stands, but not for the reason given: the enforcement backfill must still not run before merchants can actually complete verification.
  • Trading is still blocked, and the cause is now unambiguous: a merchant is born gate-closed and cannot become verified because the KYC documents are captured as video (addendum 4). Email was never the gate.
  • The staff-invite chain is no longer email-blocked at all — and the auth design’s activation code (an owner reads a code to the cashier in front of them) removes the dependency entirely.

VAS manual testing — recommendation: qa-design, not manual

Section titled “VAS manual testing — recommendation: qa-design, not manual”

Pointing manual at any aggregator, even the sandbox, abandons the fail-closed money-safety posture chosen deliberately for it and mixes test VAS data into the only realm with real trading history. qa-design is already bound to the sandbox aggregator with credentials set. Shortest path to driving VAS there: enable FEATURE_FLAG_MERCHANT_ONBOARDING (unset today, so store-create 404s) or seed a merchant, enable and run the catalog sync (the catalog is empty), then drive a sandbox sale. Emails already work there.

Addendum 6 — G1 RETRACTED: VAS web is built and reachable

Section titled “Addendum 6 — G1 RETRACTED: VAS web is built and reachable”

The largest “gap” in this document was not one. The VAS sale surface for the three aggregator verticals — TAE, Pines, Servicios — is built, wired, reachable and live-backed on web: /terminal/home → ProductPicker → VasSaleScreen (1,370 lines), calling vas/catalog, vas/sale, vas/adeudo, vas/pin/reprint and vas/sale/:id. There is zero TypeORM in terminales-web; the legacy surface the earlier claim referred to lives in the api.

It was reported missing because the docs, the board stories and the e2e specs all still point at /admin/airtime and /admin/giftcards — routes that #847 deliberately deleted when it moved VAS into the POS picker. The feature moved; its address book did not.

This is the fourth built-but-mislocated surface found in one cycle, after Clientes, the merchant wallet and the settlements reverse-orphan. The pattern is the finding: our documentation describes where things used to be. A completeness view assembled from documents will keep reporting phantom gaps until someone walks the routes. It is also why the route inventory found what seven lane inventories could not.

  1. The e2e suite is BROKEN, not absent. Three specs (US-VAS-1 ×2, US-VAS-2) navigate to the deleted admin routes and fail at navigation; only US-VAS-3 was migrated to the picker. Rule 13’s shape exactly — #732 wrote the specs, #847 removed the routes, nobody built the combination — and green component tests masked the red e2e. First build item, approved.
  2. SIM chips (US-VAS-4) are genuinely absent — no CHIP kind, no screen, no Figma frame. It is physical serialized inventory via /vas/chips/import|activate, not /vas/sale. Ruled IN (PDR-0001 names it); building from the backend contract plus mobile parity rather than waiting for design.
  3. Design state-frames for aggregator-error, timeout, no-stock and reverso are missing on web; the shipped mobile surface already encodes that taxonomy, so web derives from it.
  4. Everything live is dormant because VAS_AGGREGATOR_MODE is unset → 503. Deploy/config, not a build item.

Net effect on MVP: the largest named gap collapses to a broken test suite, one genuinely missing vertical, and some error states. That is a materially better position than this document reported.

Addendum 7 — G3 RESOLVED: the invitee credential chain is built and merged

Section titled “Addendum 7 — G3 RESOLVED: the invitee credential chain is built and merged”

An invited cashier can now sign in. api #722 + web #908 (both merged 2026-09-12) close the gap that had no endpoint and no ticket: invite → activation code → short-lived session (no password) → PIN → PIN-login. The credential is the invite secret; a session is minted with the same primitive PIN-login already uses; no password is ever created for a cashier.

The design’s best property is that it does not need email. An owner reads an 8-digit code aloud to the person in front of them, who enters it on the terminal. So the emailed invite became convenience for remote staff, not a prerequisite — “better once SMTP is decided”, never “blocked on it”. That matters because every other estimate in this document had been sequenced behind an email decision.

Four security properties were fail-verified (each deliberately broken, the test watched to fail, then restored): single-use, timing-uniform failure, code bound to its employee, and a per-employee lockout. Two defects were caught in review that green suites could not see — a timing oracle, and a storeId / storeTeamId collapse in fixtures (see rule 4’s corollary).

But “can sign in” is NOT “can sell” — and the boundary is earlier than expected

Section titled “But “can sign in” is NOT “can sell” — and the boundary is earlier than expected”

On a fresh merchant the honest end of the chain is: the cashier signs in, reaches the shift screen, and opening a shift 403s. Both assertCanOpenShift (shift.service:356) and assertCanSell (sales.service:199) key on the merchant’s verification state, and a merchant is born allowUnverifiedSales: false. So the block comes before the sale, not at finalize.

That is the merchant verification gate doing its job, not an auth defect — but the chain must not be reported as complete while meaning “can sell”. On an overridden merchant (La Esperanza) it sells today.

  • Employee-row provisioning (Api SEC Stores Membership Without Employee Row) — assigned to the auth lane, next. Does not block the invite path; blocks a manager minted by POST /pos/stores.
  • The supervisor path is designed but never walked. Activation is identical for a supervisor, and their distinct capability is same-day void approval — but no supervisor has been activated via a code and then approved a void. Flagged as a gap, not claimed. Needs one guided walk.