# 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 `_`. 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//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 ```ts // 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; } ``` 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 1–5: 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
`, 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 name** — **RESOLVED**: "Natural - Mercado de Vida" (slug `natural-mercado-de-vida`). 2. ✅ **Cookie domain** — **RESOLVED**: 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 hardware** — **RESOLVED**: 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 provider** — **RESOLVED**: stays manual in Phase 3. Phase 7 (`POS-038`) deferred — only reopens if the operator picks SumUp, Redsys TPVO, or Verifone. 5. ✅ **Refund policy defaults** — **RESOLVED**: 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 P0–P3 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.**