Files
mercadodevida/docs/pos/POS_ARCHITECTURE.md
2026-08-21 21:55:43 +02:00

31 KiB
Raw Blame History

POS Architecture — Mercado de Vida

Status: Discovery (Phase 1) Ticket: POS-001 Author: architect Last updated: 2026-08-21

This document describes how a Point-of-Sale (TPV) application integrates into the existing Mercado de Vida modular monolith. The POS must reuse products, prices, stock, customers, orders, taxes, authentication, RBAC and the admin panel. No business-logic duplication.


0. Executive summary

Aspect Decision
Frontend app New Next.js app apps/pos inside the existing project/ workspace (sibling to apps/admin). Same Tailwind design tokens, same catch-all proxy pattern, same auth flow as the admin.
Backend New module project/src/modules/pos/ following the existing domain / application / infrastructure / api layout. Reuses all existing modules via injected ports.
Sales = orders A POS sale IS an order with source = 'pos'. New column on orders_orders. The order state machine is extended with a COMPLETED terminal state (no shipping).
Cash sessions, terminals, stores New tables. Schema below.
Inventory per store Extended: inventory_stock becomes keyed by (variant_id, store_id). Migration backfills the default store.
Authentication Reuses the existing backoffice_users table (admin/editor + new pos_cashier, pos_manager roles). Same session cookie mechanism.
Hardware Adapter interfaces (no vendor SDK in core). Browser-first implementations; real ESC/POS adapters land in Phase 7.
Idempotency Reuses idempotency_key on orders. POS generates a UUID per "cobrar" click.

The POS is not a parallel system. It is a new consumer of the same core modules, plus a thin new module for the TPV-specific concepts (terminals, sessions, registers).


1. Current stack (as-found)

1.1 Repository layout

project/
├── apps/
│   ├── admin/         # Next.js 14 admin (Backoffice users)
│   ├── frontend/      # Next.js 14 customer storefront
│   └── storefront/    # legacy customer storefront (being phased out)
├── src/
│   ├── app/           # Fastify composition root (build-app.ts)
│   ├── infrastructure/# server bootstrap, config, logger
│   ├── modules/       # 20+ domain modules (DDD + Clean Architecture)
│   └── shared/        # cross-module helpers (auth, errors, http-input, telemetry, text)
├── migrations/        # node-pg-migrate
├── scripts/           # dev helpers
├── docker-compose.yml # postgres + redis
└── package.json       # ESM, TypeScript strict, vitest

Apps are co-located under project/apps/ and share backend through a single Next.js catch-all proxy (/api/[...path]/route.ts). The monolith.sh script orchestrates all services on ports 3000 (backend), 3003 (frontend), 3004 (admin), 3005 (storefront).

1.2 Backend conventions

  • Module layout (mandatory): domain / application / infrastructure / api. Public API exposed only via index.ts.
  • Module isolation: modules import ONLY public APIs of other modules; the boundary checker (scripts/check-module-boundaries.mjs) enforces this.
  • Auth: Authenticate port from shared/auth.ts; routes call await deps.authenticate(request) and then requireRole(user, 'admin' | 'editor' | 'customer').
  • RBAC: role-based string check in requireRole. Permissions are an enum in apps/admin/src/lib/permissions.ts (admin only, currently).
  • Validation: explicit parseJson(zodSchema, request.body) via shared/http-input.ts. No magic.
  • Errors: AppError(httpStatus, code, message, details?) from shared/errors.ts. Single envelope.
  • DB: PostgreSQL 16, schema-per-module naming <module>_<table>. node-pg-migrate, ES module migrations.
  • Logging: structured pino via app.log.
  • Telemetry: Telemetry / Tracer / Meter ports from observability (no-op default).
  • Tests: vitest unit + itest (integration) inside src/modules/<m>/tests/. E2E in src/app/tests/e2e/.

1.3 Existing modules we will reuse

Module What POS needs from it
identity Authenticate port, backoffice session lookup (cookie + role).
backoffice Login/logout/me endpoints and the cookie contract. Reused as-is.
catalog Product / variant / image / category / brand lookups + search by EAN/SKU/name (GET /products/search, GET /products/suggest).
pricing GET /pricing/variants/:id, POST /pricing/calculate. Server-authoritative price; never trust client totals.
inventory checkAvailability, atomic reserve / release / confirm. The POS sale = confirm of a reserve.
tax Active tax rates via GET /admin/tax-rates.
orders Order creation, state transitions, idempotency.
payments PaymentProvider interface — currently a stub. POS brings concrete cash and external card providers in Phase 3.
checkout The orchestrator pattern (validate → price → reserve → create order → payment intent). POS re-uses the same flow but skips shipping.
users Customer search by email/phone for ticket association.
store-settings store_name, contact_address, footer_text, etc. — printed on tickets.
security AuditLogger for "open register", "close register", "discount applied", etc.
notifications Email on sale confirmation (optional, off by default for POS).
observability Tracer / Meter for spans and counters.

1.4 Existing admin app conventions (POS will mirror them)

  • App Router under apps/admin/src/app/(dashboard)/....
  • Tailwind with shared @theme block (greens #2D6A4F, #1B4332; secondary cream #F5F0E8; accent orange #E76F51; danger #DC2626).
  • AuthProvider context, useAuth() hook, requireRole redirect to /login.
  • lib/api-client.ts with typed namespaces (productsApi, brandsApi, ordersApi, …). Catch-all proxy at app/api/[...path]/route.ts.
  • Tanstack-Query-like manual caching via useEffect + useState (no Redux/Zustand yet).
  • lib/permissions.ts for client-side UI gating.

2. Gaps to address before code

These are real schema/concept gaps the brief asks for that do not exist today:

Gap Brief section Current state Resolution
Multi-store §17 inventory_stock keyed by variant_id only. No store table. Add stores table; key inventory_stock by (variant_id, store_id). Migration backfills default store.
Terminals §17 None. New pos_terminals table.
Cash sessions §18 None. New pos_cash_sessions table.
Order source §16 No source column on orders_orders. Add source enum ('ecommerce' | 'pos' | 'admin') with default 'ecommerce'. Backfill existing rows.
Order COMPLETED state §16, §39 State machine tops out at DELIVERED. POS sales never ship. Add COMPLETED terminal state. POS sales go PENDING → AWAITING_PAYMENT → PAID → COMPLETED (skipping SHIPPED/DELIVERED).
POS roles §27 Roles are customer / admin / editor. Extend backoffice_users.role CHECK to include 'pos_cashier' and 'pos_manager'.
POS-only permissions §27 Permissions enum is admin-only. Extend permissions.ts with POS_* permissions; server-side requireRole is too coarse — add requireAnyRole and requirePermission to shared/auth.ts.
Anonymous sale §14 orders_orders.user_id NOT NULL. Either create a "walk-in customer" placeholder or make user_id nullable when source = 'pos'. Decision: nullable with a CHECK that requires user_id IS NULL OR source = 'pos' if guest, otherwise required.
Hardware adapters §25 None. New interfaces under shared/hardware/: ScannerAdapter, PrinterAdapter, CashDrawerAdapter, PaymentTerminalAdapter, ScaleAdapter. Browser implementation in Phase 2/3; ESC/POS in Phase 7.
Idempotency per sale §32 Already supported via idempotency_key UNIQUE. Reused as-is. POS client generates a UUID per "Cobrar" click.

3. Proposed architecture

3.1 High-level diagram

                       ┌──────────────────────────────────────────────┐
                       │            Mercado de Vida Core              │
                       │                                              │
  ┌──────────┐         │  ┌─────────┐  ┌─────────┐  ┌────────────┐  │
  │ Ecommerce│─────────┼─►│Catalog  │  │Pricing  │  │ Inventory  │  │
  │ Frontend │  API    │  └────┬────┘  └────┬────┘  └─────┬──────┘  │
  └──────────┘         │       │            │              │         │
                       │       └────────────┼──────────────┘         │
  ┌──────────┐         │                    ▼                        │
  │  Admin   │─────────┼─►  ┌──────────────────────────────┐         │
  │  Panel   │  API    │   │  Orders (source=ecommerce/pos/admin)    │
  └──────────┘         │   └──────────────────────────────┘         │
                       │                    │                        │
  ┌──────────┐         │                    ▼                        │
  │   POS    │─────────┼─►  ┌──────────────────────────────┐         │
  │  (new)   │  API    │   │  Payments (cash / card stub) │         │
  │ apps/pos │         │   └──────────────────────────────┘         │
  └────┬─────┘         │                    │                        │
       │ hardware      │                    ▼                        │
       ▼ adapters      │            ┌──────────────┐                 │
  ┌──────────┐         │            │  Audit Log   │                 │
  │ Scanner  │         │            └──────────────┘                 │
  │ Printer  │         │                                              │
  │ Cash drw │         └──────────────────────────────────────────────┘
  │ Card Trm │
  └──────────┘

3.2 Backend modules

src/modules/
├── pos/                  # NEW — POS-specific concepts
│   ├── domain/
│   │   ├── store.ts                  # Store aggregate
│   │   ├── terminal.ts               # POS terminal
│   │   ├── cash-session.ts           # Open/closed register session
│   │   ├── quick-product.ts          # Configurable favorite buttons
│   │   ├── sale.ts                   # Re-exports Order as Sale; defines Source enum
│   │   └── errors.ts
│   ├── application/
│   │   ├── open-cash-session.ts
│   │   ├── close-cash-session.ts
│   │   ├── list-terminals.ts
│   │   ├── get-pos-config.ts         # returns terminal, session, store, tax rates
│   │   ├── search-products-for-pos.ts  # thin wrapper over catalog search + price
│   │   ├── create-pos-sale.ts        # orchestrates pricing + reserve + create + complete
│   │   ├── refund-pos-sale.ts
│   │   ├── park-ticket.ts            # cart parked in DB
│   │   └── ...
│   ├── infrastructure/
│   │   ├── pg-store-repository.ts
│   │   ├── pg-terminal-repository.ts
│   │   ├── pg-cash-session-repository.ts
│   │   ├── pg-quick-product-repository.ts
│   │   └── pg-parked-ticket-repository.ts
│   └── api/
│       └── pos.routes.ts             # all /pos/* endpoints
└── (existing modules, untouched except where noted)

3.3 POS order flow (orchestration)

The POS sale path is a slimmer checkout: no shipping, no promo code, optional customer. We re-use OrderService and InventoryService directly rather than building a separate orchestrator.

[POS UI: Cart with N items]
   │
   │  POST /api/pos/sales  { idempotencyKey, items, customerId?, payments, discount? }
   │
   ▼
pos.routes.ts
   │
   ├─► requireRole(user, ['pos_cashier','pos_manager','admin'])
   ├─► requireOpenSession(terminalId)             // 409 if closed
   ├─► for each item:
   │     pricingApi.getVariantPrice(variantId)    // server-authoritative
   │     inventory.checkAvailability(variantId)   // soft check (real check at confirm)
   ├─► compute totals server-side (no client trust)
   ├─► BEGIN TRANSACTION
   │     ordersService.create({source:'pos', userId?, items, totals, idempotencyKey})
   │     inventory.reserve(variantId, storeId, qty) × items
   │     ordersService.transition(orderId, 'AWAITING_PAYMENT')
   │     for each payment: paymentsService.registerCash/Card({orderId, amountCents, ...})
   │     ordersService.transition(orderId, 'PAID')
   │     for each payment: inventory.confirm(reservation) × items
   │     ordersService.transition(orderId, 'COMPLETED')
   │     auditLogger.log('pos.sale.completed', {orderId, terminalId, totalCents, ...})
   │   COMMIT
   │
   ▼
[POS UI: Receipt rendered, print dispatched, new empty cart]

Key invariants:

  • Idempotency: the same idempotencyKey returns the same order — no double-charge on network retry.
  • Atomicity: everything is in one PG transaction (using a single connection / client). The existing OrderService and InventoryService already operate on a passed pg.Client; we wire them to a single txn.
  • Server-authoritative totals: client sends only item ids + quantities; server computes price, tax, discount.
  • No double reservation: the orchestration is linear and single-threaded per request.

3.4 Hardware abstraction

// project/src/shared/hardware/types.ts

export interface ScannerAdapter {
  /** Subscribe to decoded codes. Most physical scanners are HID-keyboard;
   *  we wrap them in this interface so swapping to serial/BT is a one-line change. */
  onCode(callback: (code: string) => void): () => void;
}

export interface PrinterAdapter {
  /** Renders a structured receipt. Returns success/failure; never throws
   *  for "printer offline" (returns a structured failure instead). */
  print(receipt: ReceiptPayload): Promise<{ ok: true; jobId: string } | { ok: false; reason: string }>;
}

export interface CashDrawerAdapter {
  /** Pulses the cash-drawer kick-out. Best-effort; failures logged. */
  open(): Promise<{ ok: boolean; reason?: string }>;
}

export interface PaymentTerminalAdapter {
  /** Asks the datáfono for an authorisation. Returns approved/declined/error.
   *  No PAN handling: this delegates to a PCI-DSS-compliant provider. */
  requestPayment(input: { amountCents: number; currency: 'EUR'; reference: string }): Promise<
    | { ok: true; providerPaymentId: string; authCode: string }
    | { ok: false; reason: 'declined' | 'timeout' | 'offline' | 'error'; message: string }
  >;
}

export interface ScaleAdapter {
  /** Reads weight in grams. Returns null if the scale is disconnected or settling. */
  readGrams(): Promise<number | null>;
}

Each adapter has two implementations in Phase 7:

  • Browser (Phase 2/3): relies on standard browser APIs. Scanner = keyboard listener on a focused input. Printer = hidden iframe to a print-friendly HTML route. Cash drawer = no-op. Payment terminal = manual entry (operator types last-4 or auth code).
  • Native bridge (Phase 7, deferred): a thin Node addon or a sidecar process that speaks USB/serial/ESC-POS. Exposed over a localhost HTTP socket to the Next.js app.

3.5 Frontend app

apps/pos/
├── package.json
├── next.config.ts
├── tailwind.config.ts (or shared via project root)
├── src/
│   ├── app/
│   │   ├── layout.tsx
│   │   ├── globals.css
│   │   ├── (auth)/
│   │   │   └── login/page.tsx
│   │   ├── (terminal)/
│   │   │   ├── layout.tsx               # minimal chrome — sidebar, connection status
│   │   │   ├── page.tsx                 # main register screen
│   │   │   ├── open-register/page.tsx
│   │   │   ├── close-register/page.tsx
│   │   │   ├── history/page.tsx
│   │   │   └── settings/page.tsx
│   │   └── api/
│   │       └── [...path]/route.ts       # identical proxy to apps/admin
│   ├── components/
│   │   ├── pos/POSLayout.tsx
│   │   ├── pos/POSHeader.tsx
│   │   ├── pos/ProductSearch.tsx
│   │   ├── pos/CategorySelector.tsx
│   │   ├── pos/QuickProducts.tsx
│   │   ├── pos/Cart.tsx
│   │   ├── pos/CartItem.tsx
│   │   ├── pos/CartTotals.tsx
│   │   ├── pos/CustomerSelector.tsx
│   │   ├── pos/DiscountPanel.tsx
│   │   ├── pos/PaymentModal.tsx
│   │   ├── pos/CashPayment.tsx
│   │   ├── pos/CardPayment.tsx
│   │   ├── pos/NumericKeypad.tsx
│   │   ├── pos/TicketHistory.tsx
│   │   ├── pos/RegisterSession.tsx
│   │   ├── pos/POSSettings.tsx
│   │   ├── pos/ConnectionStatus.tsx
│   │   └── ui/                          # shared with apps/admin via component copy (TODO: extract to packages/ui in a future cleanup)
│   ├── features/
│   │   ├── auth/
│   │   ├── cart/                        # POS cart state machine
│   │   ├── register/                    # cash session state
│   │   ├── catalog/                     # cached product list
│   │   └── hardware/                    # adapter wiring
│   ├── lib/
│   │   ├── api-client.ts                # typed client (mirror of apps/admin/src/lib/api-client.ts)
│   │   ├── permissions.ts               # extends admin/permissions with POS_*
│   │   ├── money.ts                     # cents helpers, formatting (es-ES)
│   │   ├── tax.ts                       # client-side tax display only
│   │   ├── idempotency.ts               # UUID v4 generator
│   │   └── hardware/                    # adapter factories (browser impls)
│   ├── hooks/
│   ├── types/
│   └── middleware.ts                    # auth guard, terminal-id cookie
└── tests/

The app is delivered through the existing monolith.sh script (Phase 2: add a pos service on port 3006).

3.6 Multi-store inventory

Two options were considered:

  • (A) Extend inventory_stock with store_id — chosen. Migration adds the column with a default "Main Store" UUID; backfills existing rows. Foreign key to new stores table.
  • (B) New inventory_stock_by_store table — rejected: more moving parts, harder joins, no upside.

Operations:

  • inventory.reserve(variantId, storeId, qty) becomes the signature.
  • The POS endpoint always passes the terminal's storeId.
  • Reporting endpoints (GET /admin/inventory) accept an optional ?storeId= filter.

3.7 Authentication & authorization

  • Authentication: identical to apps/admin. Same cookie, same Authenticate port. The POS app calls the same /api/backoffice/auth/login endpoint.
  • Authorization: extend shared/auth.ts with requireAnyRole(user, [...roles]). New roles: pos_cashier, pos_manager. Permission string POS_* for fine-grained checks (override price, refund, cancel sale, open/close register).
  • Terminal identity: a UUID generated on first boot of the POS app, stored in a Secure cookie (pos_terminal_id, 1 year expiry). The cookie survives reloads; clearing it requires re-binding the terminal to a record via a one-time admin code.
  • Session token in cookie is the same as admin's. One user can be logged into admin and POS simultaneously (different cookies? no — same cookie, different apps).

3.8 Connection status / offline (deferred)

Phase 15: online-only. Connection status shows "● Online" / "⚠ Sin conexión" by pinging GET /health every 10 s.

Phase 8 (out of scope here): offline-first with IndexedDB queue and CRDT-style stock reconciliation. Deferred until the operator explicitly requests it. Architecture leaves a pendingMutations slot in the cart slice so it can be wired later.


4. Data model (full in POS_DATA_MODEL.md)

New tables (concise; full DDL in POS_DATA_MODEL.md):

Table Purpose Key fields
pos_stores Physical stores id, name, address, tax_id, settings_jsonb
pos_terminals One TPV device id, store_id, name, status, interface_mode, settings_jsonb, last_seen_at
pos_cash_sessions Open/close register id, terminal_id, store_id, user_id, opened_at, closed_at, opening_cash_cents, closing_cash_cents, expected_cash_cents, actual_cash_cents, difference_cents, status
pos_quick_products Favorite buttons id, store_id, terminal_id?, user_id?, product_id, variant_id, position
pos_parked_tickets Held tickets id, store_id, terminal_id, user_id, customer_id?, cart_jsonb, created_at, expires_at
pos_payment_methods Configurable payment types id, store_id, code, label, kind ('cash'|'card'|'other'), active, sort_order

Modified tables:

Table Change
inventory_stock Add store_id uuid NOT NULL DEFAULT <main store>, FK to pos_stores. Replace unique index with (variant_id, store_id).
orders_orders Add source text NOT NULL DEFAULT 'ecommerce' CHECK (source IN ('ecommerce','pos','admin')). Add terminal_id uuid NULL, cash_session_id uuid NULL. Add COMPLETED to state CHECK.
backoffice_users.role Extend CHECK to include 'pos_cashier', 'pos_manager'.

No new customer/user table — POS sales link to existing identity_users (or NULL for walk-ins).


5. API surface (full in POS_API.md)

Mounted under existing Fastify via registerPosRoutes(app, deps) in build-app.ts. All under /pos/.... Reuses requireRole and requireAnyRole from shared/auth.ts. Cookie auth same as the rest.

Method Path Purpose Role
GET /pos/stores List stores admin
POST /pos/stores Create store admin
GET /pos/terminals List terminals admin / pos_manager
POST /pos/terminals Register a terminal (returns binding code) admin
GET /pos/terminals/me Current terminal by cookie any pos
PATCH /pos/terminals/:id Update terminal settings admin / pos_manager
GET /pos/sessions/me Current open session for terminal any pos
POST /pos/sessions Open register pos_cashier / pos_manager
POST /pos/sessions/:id/close Close register (counted cash) pos_cashier / pos_manager
GET /pos/config Returns { terminal, store, session, paymentMethods, taxRates, categoriesTree } any pos
GET /pos/products/search?q=... Server-side search (delegates to catalog) any pos
GET /pos/products/by-ean/:ean Direct lookup by EAN (fast path for scanner) any pos
GET /pos/products/by-sku/:sku Direct lookup by SKU any pos
GET /pos/quick-products Quick buttons for the current (terminal, user) any pos
POST /pos/quick-products Add a quick product pos_manager
DELETE /pos/quick-products/:id Remove pos_manager
POST /pos/sales Create sale (idempotent) pos_cashier+
GET /pos/sales List recent sales (scoped to terminal/store) any pos
GET /pos/sales/:id Sale detail any pos
POST /pos/sales/:id/refund Refund (full or partial) pos_manager
POST /pos/sales/:id/print Reprint receipt any pos
POST /pos/parked-tickets Park current cart pos_cashier+
GET /pos/parked-tickets List parked tickets for store pos_cashier+
DELETE /pos/parked-tickets/:id Discard parked ticket pos_cashier+

All POST/PATCH endpoints validate with parseJson(zodSchema, body). All errors are AppError-shaped. All write actions append to security_audit_log via AuditLogger.log.


6. Reusable components

Existing modules reused as-is, no edits:

  • Authenticate port, parseJson, AppError, parseJson (validation), AuditLogger.
  • OrderService (with source extension), InventoryService (with store_id extension).
  • PricingService, TaxRateRepository, CatalogSearch, BrandRepository, CategoryRepository, ProductRepository, VariantRepository.
  • CustomerRepository from users for ticket association.
  • StoreSettingsRepository for ticket header/footer.

Existing frontend utilities reused:

  • lib/api-client.ts pattern (copy + extend with posApi).
  • lib/permissions.ts extended with POS_* permissions.
  • AuthProvider pattern copied and adapted (no shared package yet).
  • Tailwind theme tokens (the @theme block in globals.css is identical).

7. Risks

Risk Severity Mitigation
Multi-store migration is destructive high Migration backfills all existing rows with a single default store; reversible via SQL script kept in scripts/. Operator must approve before Phase 2 starts.
Inventory race between POS and ecommerce high All inventory writes go through InventoryService.reserve/confirm; same transaction model already used by checkout. Documented invariant: reservation is the only way to consume stock.
COMPLETED state breaks existing admin workflows medium State machine is additive; existing DELIVERED flow is unchanged. Admin orders stay on DELIVERED. POS-only orders end on COMPLETED.
Cookie sharing between admin and POS apps low Same domain; both apps read the same cookie. If POS is deployed to a different domain, swap to a JWT or shared session service (Phase 8).
Idempotency conflicts medium idempotency_key is already unique. The POS UI generates a UUID per "Cobrar" click; if the user clicks twice within ~50 ms, only one wins.
Hardware fragmentation medium All hardware behind adapters; no vendor SDK in core. Browser impls ship first (Phase 2/3); native bridge in Phase 7 only when operator commits to a vendor.
Touchscreen keyboard low React NumericKeypad component; no system-level dependencies.
Refunds for split payments medium Partial refund = multiple payment_transactions with negative amount_cents. Existing payments module already supports this.

8. Phased plan

The brief already defines 7 phases. The list below translates them into Orquestra-friendly tickets:

Phase Ticket(s) Goal
0 POS-001 (this) Discovery + architecture docs.
1 POS-002 to POS-010 Core TPV. Schema migrations (stores, terminals, sessions, quick-products, parked-tickets, payment-methods). Multi-store inventory migration. OrderService.source extension. COMPLETED state. New pos module + routes + repository + unit tests. Frontend app skeleton with POSLayout, header, product search, categories, quick products, cart, totals.
2 POS-011 to POS-016 Checkout. Cash payment (with change calculation), card manual entry, idempotent sale creation, stock decrement, receipt rendering, optional customer association, history.
3 POS-017 to POS-022 Cash register. Open/close session, expected vs actual cash, difference capture, audit trail, end-of-shift summary.
4 POS-023 to POS-028 Multi-terminal. Stores + terminals admin UI, terminal binding code flow, per-terminal quick products, per-store payment methods, concurrent terminals (already supported by HTTP).
5 POS-029 to POS-033 Touch UI. Larger hit targets, virtual keypad, scanner auto-focus, keyboard shortcuts (F2/F4/F6/F8/F9/F10/ESC), responsive 1024×768 → 1920×1080, no horizontal scroll.
6 POS-034 to POS-040 Hardware. Adapter interfaces finalized. Browser implementations complete. ESC/POS + USB scanner bridge behind a thin Node sidecar (out-of-process HTTP) — only the spec is written here; the bridge itself is built when the operator picks a vendor.
7 POS-041+ Polish. Refunds UI, returns, partial refunds, gift receipts, scale integration, multi-currency (deferred to operator demand).

Each phase ends with all three gates approved (reviewer, security, qa) and verify.sh exit 0.


9. Open questions for the operator

These are decisions the architect cannot take unilaterally and that block Phase 2:

  1. Default store nameRESOLVED: "Natural - Mercado de Vida" (slug natural-mercado-de-vida).
  2. Cookie domainRESOLVED: separate port :3006 for the POS app (alongside admin :3004). Cookie is set with Domain unset (so the browser scopes it to the POS origin only). When the operator deploys to production with Traefik and subdomains, the cookie strategy will be revisited (Phase 4 cleanup): at that point Domain=.mercadodevida.example allows admin and POS to share the same backoffice session.
  3. Touchscreen target hardwareRESOLVED: 10-inch tablet in landscape. Responsive design from 1024×768 to 1920×1080, with a sensible tablet portrait mode (800×1280) as a stretch goal. The touch CSS uses media query (pointer: coarse) to detect and switch; manual override stored in cookie.
  4. Datáfono providerRESOLVED: stays manual in Phase 3. Phase 7 (POS-038) deferred — only reopens if the operator picks SumUp, Redsys TPVO, or Verifone.
  5. Refund policy defaultsRESOLVED: refunds allowed up to 15 days after sale. Manager approval required for refunds above 50 € (configurable in store_settings).

The remaining questions (printer model, scanner type, cash drawer wiring, scale brand) are documented in POS_HARDWARE.md §10 and resolved there.


10. Definition of done (Phase 1)

  • This document (POS_ARCHITECTURE.md) signed off by leader.
  • POS_DATA_MODEL.md, POS_API.md, POS_HARDWARE.md, POS_OPERATIONS.md exist with concrete specs.
  • POS_TASKS.md lists P0P3 prioritized tickets queued in the backlog (POS-002+).
  • verify.sh exit 0.
  • runtime-status.json reset to idle.

No code is touched in this phase beyond the docs.