feat(F-186): completed feature

This commit is contained in:
chattie
2026-08-22 22:08:09 +02:00
parent 63a305bdd4
commit a3f6edd325
30 changed files with 3603 additions and 624 deletions

View File

@@ -0,0 +1,80 @@
# F-186 — Architecture
## Decision
Implement the request as one coherent POS checkout state machine while preserving the existing `POST /pos/sales` entry point. Payment allocation remains client-side draft state; the server receives and atomically validates the final allocation only after explicit user confirmation.
## Persistence
Add migration `054_pos_checkout_receipts.js`:
- `pos_receipt_settings(store_id PK, prefix, next_number, padding, return_policy, updated_at)` provides configurable, transaction-safe numbering and return text.
- `orders_orders.receipt_number` stores the immutable human ticket number with a unique partial index.
- `orders_items.product_id` and `variant_id` become nullable and `orders_items.is_free_item` is added. A CHECK requires both IDs for stocked lines and both NULL for free lines.
- Seed receipt settings for the default store. Existing rows remain stocked lines.
Terminal JSON settings remain appropriate for device-specific switches:
- `touchNavigationEnabled`
- `quickProductVariantIds` (exactly eight slots)
- `lineDiscountsEnabled` (default true)
Existing `pos_payment_methods` remains the source of truth for configurable methods. No provider SDK integration is introduced.
## Sale contract
Stock line request: `{ kind:'stock', variantId, quantity, discountCents }`.
Free line request: `{ kind:'free', name, unitPriceCents, quantity }`.
Legacy stocked line fields remain accepted for compatibility but are ignored for price/name/SKU snapshots.
Payment request: `{ methodCode, amountCents, tenderedCents? }`. Legacy `{ kind }` maps to the seeded code where possible.
Server invariants:
1. Session belongs to terminal and is open.
2. Terminal identity from request header must match body terminal.
3. Stock lines are reloaded from catalog/pricing for the session store and inventory rows are locked.
4. Discounts are blocked if terminal settings disable them; otherwise validated against authoritative gross price.
5. Free items require a trimmed name, positive cent price, positive integer quantity, and never touch inventory.
6. Every method code must be active for the store.
7. Applied payment amounts equal the sale total exactly. Only cash may have `tenderedCents`, which must be >= applied amount. Change is `tendered-applied`.
8. Sale, item snapshots, stock movement, payment transactions, reporting payment lines, cash-session expected amount, receipt sequence and receipt number commit in one transaction.
9. Cash session expected balance increases by the applied cash amount, not tendered cash, so change is not counted as revenue/cash retained.
## Receipt
The sale response includes a structured receipt assembled from immutable order/item/payment rows plus store/terminal/session metadata. It contains:
- company name, address, tax ID, contact email/phone
- receipt number, date/time, order/session/terminal/cashier
- item quantity, unit amount, subtotal, discount, tax and line total
- subtotal, discount, tax and total
- method labels/codes, applied amount, cash tendered and change
- return policy
`GET /pos/sales/:id/receipt` and `/print` return the same shape. `POST /pos/sales/:id/receipt/email` validates an email address and uses the existing SMTP-backed transactional email helper. Browser print remains the hardware-neutral print implementation.
## Frontend state machine
`editing cart -> allocating payments -> ready to confirm -> submitting -> receipt -> delivered/reset`
- Clicking any configured method opens a touch modal defaulting to the remaining total, with “total” and “partial” controls.
- Allocations remain visible and removable in the cashier; paid and remaining totals are displayed.
- Cash modal also captures tendered amount and previews change.
- Confirmation is a separate action and is enabled only when remaining is zero.
- On successful sale the cart is retained while the receipt modal is open.
- Print calls `window.print()` after rendering a print-only receipt. Email calls the backend endpoint. Either successful action resets cashier for the next sale.
- Add a touch-sized `Artículo libre` modal for name and positive euro price.
- Discount UI is rendered only when `lineDiscountsEnabled !== false`.
## Admin
Expand terminal touch configuration to eight quick slots and a line-discount switch. Add store receipt/company fields and receipt numbering controls. Add payment-method creation, activation and ordering for cash/card/other labels (e.g. Bizum, Stripe, Apple Pay).
## Security
- Admin-only configuration routes.
- POS roles only for sale/receipt delivery.
- Header terminal binding checked against body/session.
- No client-supplied catalog price/name trusted.
- Receipt email endpoint accepts only the generated receipt and validated destination; no arbitrary subject/body.
- Payment processor credentials and card PAN are out of scope and never accepted.
## Tests
- Unit tests for monetary allocation/change/free-line normalization and disabled discounts.
- Integration tests where DB is available for migration, receipt sequence, free item, method validation and stock mutation.
- Typecheck/build all three affected packages; run root tests sequentially for real PostgreSQL integration files.

View File

@@ -0,0 +1,6 @@
# F-186 — Documentation
- Added `docs/pos/POS_CHECKOUT.md` with admin setup, cashier state flow, stock/free line contracts, mixed payment/change invariants, ticket print/email, reporting capture and follow-up roadmap.
- Added a precedence notice in `docs/pos/POS_API.md` because its discovery-era single-payment request is superseded by the implemented F-186 contract.
- Documented that custom method labels do not imply real provider integrations.
- Documented that paid sales are `COMPLETED`; pending balances and returns remain F-188/F-189.

View File

@@ -0,0 +1,56 @@
# F-186 — Implementer evidence
## Implemented
### POS cashier
- Eight quick-product slots rendered in a 4×2 touch grid.
- Touch-sized line-discount button; hidden when `terminal.settings.lineDiscountsEnabled=false`.
- Touch payment modal per configured method with “total” / “partial” choice.
- Mixed payment allocations remain visible with paid and outstanding totals and can be removed.
- Cash tender captures overpayment and previews/records change.
- Separate “Confirmar y cerrar ticket” step; disabled until applied amounts equal total.
- Receipt modal supports browser print and SMTP email; cashier is only reset after one delivery action completes.
- Free-item modal adds non-stock service/product lines by name and positive price.
### Admin
- Terminal config expanded from 4 to 8 quick slots.
- Per-terminal discount switch for self-payment terminals.
- Payment-method management supports custom active/inactive cash/card/other methods (Bizum, Stripe, Apple Pay labels, etc.; no external processor integration).
- Company identity, ticket prefix/next number/padding, header/footer and return-policy settings.
### Backend and persistence
- Migration `054_pos_checkout_receipts.js` adds transaction-safe receipt settings/sequence, immutable receipt numbers, and nullable stock IDs for CHECK-constrained free lines.
- POS sale now reloads catalog prices/names/SKU server-side, locks/decrements stock, rejects disabled discounts/methods, validates exact payment allocation and cash tender, and commits as `COMPLETED`.
- Payment transactions preserve configured method, tender and change in immutable raw data.
- Each payment is captured in `reporting_payment_lines`; this addresses new POS sales not reaching reporting.
- Cash session expected balance increases by applied cash, never by tendered cash (change excluded).
- Rich receipt builder contains company, time, ticket number, item details, totals, payment allocations, change and return policy.
- Receipt access is terminal-scoped for cashier/manager and unrestricted only for admin.
- Receipt email uses existing SMTP settings and cannot inject custom content.
- POS cashier creation was corrected to use `backoffice_users` with Argon2 instead of invalid identity rows/SHA-256.
## Tests and verification
- `cd project && npm run typecheck` — PASS.
- `cd project && npm test` — PASS: 268 unit tests; DB tests skipped without env.
- `TEST_DATABASE_URL=... npx vitest run --no-file-parallelism` — PASS: 78 files / 350 tests, including all real-PostgreSQL integration files sequentially.
- New `payment-allocation.test.ts` — PASS: mixed partial allocation, cash change, exact total, disabled method and legacy mapping.
- New `pos-checkout-receipts.itest.ts` — PASS: complete state, stock decrement, free line, mixed payment/change, reporting capture, receipt sequence, cash expected amount and idempotent replay.
- Migration fresh up / no-op second up / full down / re-up — PASS.
- POS Next.js production build — PASS.
- Admin Next.js production build — PASS (pre-existing dynamic upload tracing warnings only).
- `./scripts/verify.sh` — PASS.
- `git diff --check` — PASS.
- Targeted ESLint and Prettier for all changed backend/admin/POS files — PASS.
## Known repository-wide pre-existing checks
- Root `npm run lint` remains red on 9 unrelated pre-existing errors in `generate-thumbnails.mjs`, `log-broadcaster.ts`, old `store-repository.test.ts`, and reporting files. Changed files pass targeted ESLint/Prettier.
- Boundary violations were reduced from 8 to 1 by adding the POS public index and removing old deep imports. The remaining violation is pre-existing `security.routes.ts -> infrastructure/logging/log-broadcaster.ts` and is outside F-186.
## Follow-up tickets captured from user additions
- F-187 cashier deletion/deactivation.
- F-188 pending-payment orders (paid POS orders are already `COMPLETED` here).
- F-189 negative quantity returns and return tickets.
- F-190 full reporting update/refresh audit (new POS payment lines are captured here).
- F-191 terminal and daily close reconciliation.
- F-192 simplified storefront VAT label.
- F-193 storefront expiry/weight and shipping-weight calculation.

View File

@@ -0,0 +1,19 @@
{
"feature_id": "F-186",
"agent": "leader",
"stage": "close",
"verdict": "APPROVED",
"checks": [
{ "item": "reviewer/security/qa gates APPROVED", "ok": true },
{ "item": "350/350 tests with real PostgreSQL sequential", "ok": true },
{ "item": "backend/POS/admin typecheck and production builds", "ok": true },
{ "item": "migration up/no-op/down/up", "ok": true },
{ "item": "verify.sh final exit 0", "ok": true },
{ "item": "documentation and follow-up tickets F-187..F-193", "ok": true }
],
"issues": [],
"notes": [
"Unrelated pre-existing upload files are intentionally excluded from the feature commit.",
"Repository-wide lint debt is documented in implementer evidence; changed-file lint is green."
]
}

View File

@@ -0,0 +1,31 @@
{
"feature_id": "F-186",
"agent": "qa",
"stage": "qa_gate",
"verdict": "APPROVED",
"acceptance": [
{ "id": 1, "criterion": "Admin configures and POS renders eight quick products", "ok": true, "evidence": "Admin and touch API enforce length 8; POS renders 8 slots" },
{ "id": 2, "criterion": "Touch line discount can be disabled per terminal", "ok": true, "evidence": "Touch-sized button plus UI hiding and backend POS_DISCOUNTS_DISABLED guard" },
{ "id": 3, "criterion": "Enabled configurable payment methods only", "ok": true, "evidence": "Admin CRUD/toggle; config filters active; sale validates active store methods" },
{ "id": 4, "criterion": "Full/partial mixed allocations display paid and remaining", "ok": true, "evidence": "PaymentModal and cashier allocation list; payment-allocation unit test" },
{ "id": 5, "criterion": "Cash overpayment records change", "ok": true, "evidence": "Unit and PostgreSQL integration assertions for tendered 1000/applied 500/change 500" },
{ "id": 6, "criterion": "Exact allocation and explicit confirmation before completed sale", "ok": true, "evidence": "Confirm button state plus backend exact-total validation; order integration state COMPLETED" },
{ "id": 7, "criterion": "Complete configurable numbered receipt", "ok": true, "evidence": "Receipt builder/UI/email includes company, time, number, lines, quantities, totals, methods, change, return policy" },
{ "id": 8, "criterion": "Print/email then cashier reset", "ok": true, "evidence": "Receipt modal is blocking and resetCashier is only called by completed print/email action" },
{ "id": 9, "criterion": "Free item without inventory mutation", "ok": true, "evidence": "DB CHECK and integration test verify NULL stock IDs while stocked line alone decrements inventory" },
{ "id": 10, "criterion": "Compatibility and green regression", "ok": true, "evidence": "350/350 tests pass with real PostgreSQL sequentially; backend/POS/admin typecheck and builds pass; verify.sh green" }
],
"regression": {
"unit": "268 passed",
"real_postgresql_sequential": "350 passed across 78 files",
"migration_cycle": "fresh up, second no-op, full down, re-up passed",
"pos_build": "passed",
"admin_build": "passed",
"verify": "passed"
},
"issues": [],
"notes": [
"No physical printer or SMTP server was available for hardware delivery; browser print and SMTP dispatch paths were build/type/security validated.",
"New user requests for pending balances, returns and daily close are separately queued as F-188/F-189/F-191."
]
}

View File

@@ -0,0 +1,21 @@
{
"feature_id": "F-186",
"agent": "reviewer",
"stage": "review_gate",
"verdict": "APPROVED",
"checks": [
{ "item": "Eight-slot quick product and terminal discount contracts agree across admin, API and POS", "ok": true },
{ "item": "Payment allocation requires an enabled method and exact authoritative total", "ok": true },
{ "item": "Cash change and expected cash use tendered/applied values correctly", "ok": true },
{ "item": "Sale, stock, reporting lines, payment rows and receipt sequence are atomic and idempotent", "ok": true },
{ "item": "Free items are DB-constrained and excluded from inventory mutation", "ok": true },
{ "item": "Receipt delivery precedes cashier reset", "ok": true },
{ "item": "Unit, integration, migration, typecheck and production builds pass", "ok": true }
],
"issues": [],
"notes": [
"Browser print APIs cannot distinguish a confirmed print from a cancelled print dialog; reset occurs when the print dialog returns.",
"Pending-payment orders and return tickets are intentionally tracked in F-188/F-189, not mixed into this fully-paid checkout contract.",
"Repository-wide lint debt outside changed files remains; targeted changed-file lint is green."
]
}

View File

@@ -0,0 +1,29 @@
{
"feature_id": "F-186",
"agent": "security",
"stage": "security_gate",
"verdict": "APPROVED",
"checks": [
{ "item": "Admin configuration endpoints require admin role", "ok": true },
{ "item": "Sale and receipt endpoints require authenticated POS roles", "ok": true },
{ "item": "Bound terminal header must match sale terminal/session", "ok": true },
{ "item": "Non-admin receipt reads/email are scoped to the originating terminal", "ok": true },
{ "item": "Catalog price/name/SKU and enabled payment methods are server-authoritative", "ok": true },
{ "item": "All new SQL uses bound parameters and transactional row locks", "ok": true },
{ "item": "Receipt email validates destination and uses fixed generated content", "ok": true },
{ "item": "No PAN, processor credentials or arbitrary provider payload accepted", "ok": true },
{ "item": "POS cashier passwords use Argon2 and backoffice identity storage", "ok": true },
{ "item": "Secret scan of changed diff", "ok": true },
{ "item": "Production dependency audit: backend, POS and admin", "ok": true }
],
"dependency_audit": {
"backend": "0 vulnerabilities",
"pos": "0 vulnerabilities",
"admin": "0 vulnerabilities"
},
"issues": [],
"notes": [
"Review is scoped to F-186 endpoints in the legacy monolithic POS route file; unrelated historical placeholder endpoints require separate hardening.",
"External card/Bizum/Stripe/Apple Pay processing remains out of scope; labels only, so no PCI card data is handled."
]
}

View File

@@ -1,3 +1,32 @@
# F-185Integration suite stability
# F-186POS configurable checkout, mixed payments and receipts
Run real-PostgreSQL integration files sequentially because each resets shared schema. Add forward migration correcting reporting_payment_lines currency default to literal EUR.
Complete the TPV cashier flow for touch terminals and self-payment use cases.
## Scope
- Increase configurable quick products from 6 to 8.
- Replace line discount text/inline interaction with a touch-sized button; allow admins to disable line discounts per terminal.
- Let admins configure and enable payment methods (cash, card, Bizum, Stripe, Apple Pay, or another named method).
- Payment modal must allocate either the full remaining amount or a partial amount. Keep partial allocations visible in the cashier and permit another method until the total is covered.
- For cash, accept tendered amount above the outstanding amount and calculate change.
- Require an explicit final confirmation after payment allocation before closing the sale.
- Generate a receipt with company identity, date/time, configurable ticket numbering, item name, quantity, subtotal, totals, payment methods/amounts, cash change, and return policy.
- Offer print and email delivery. Clear the cashier only after print/email action succeeds or is explicitly completed.
- Add a free-item flow for a non-stock product/service with required name and positive price; free items must not mutate inventory.
- Keep monetary validation and sale completion authoritative on the backend.
## Out of scope
- Real integrations with external payment processors.
- Certified fiscal-printer protocols or country-specific fiscal certification.
- Hardware-specific printer drivers; browser print is sufficient.
## Acceptance
1. Admin can configure up to eight quick products and POS renders all configured slots.
2. Line discount is a touch target and is absent/blocked when the terminal disables discounts.
3. Enabled admin payment methods appear in POS; disabled methods cannot be submitted.
4. Cashier supports full and partial payment allocations, displays paid/remaining totals, and permits mixed methods.
5. Cash tender above the remaining total displays and records change; non-cash overpayment is rejected.
6. A sale can only be confirmed when allocations cover the exact total, and requires explicit confirmation.
7. Receipt contains all requested company, numbering, line, total, payment, change, and return-policy data.
8. Receipt supports browser print and email delivery; cashier resets only after delivery completion.
9. A free item can be added with name and positive price and does not reserve or decrement stock.
10. Existing POS sale/reporting contracts remain compatible and tests plus `verify.sh` are green.

View File

@@ -488,3 +488,10 @@
- PG16 no soporta `ADD CONSTRAINT IF NOT EXISTS` → usar `DO $$ IF NOT EXISTS`
- Batch de rutas múltiples en un archivo requiere cuidado con el balanceo de llaves/paréntesis
- El script de análisis de Python para balanceo de llaves cuenta `}` dentro de literales de template como cierres de bloque (da falsos positivos)
## F-186 cerrada (2026-08-22) — POS configurable checkout, mixed payments and receipts
- Gates: reviewer APPROVED, security APPROVED, qa APPROVED, verify.sh exit 0.
- Entregable: 8 productos rápidos, descuentos táctiles/desactivables, métodos configurables, pagos parciales/mixtos, cambio en efectivo, confirmación explícita, artículos libres y tickets numerados por impresión/email.
- Integridad: venta `COMPLETED`, stock/pagos/reporting/caja/secuencia atómicos e idempotentes; precio de catálogo y métodos validados en backend.
- Evidencia: 350/350 tests con PostgreSQL real en secuencia, builds backend/POS/admin verdes; `work/artifacts/F-186/`.
- Seguimiento solicitado: F-187..F-193.

View File

@@ -6,6 +6,6 @@
"state": "waiting",
"next_agent": "leader",
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
"updated_at": "2026-08-22T17:30:45Z",
"updated_at": "2026-08-22T20:08:30Z",
"timeline": []
}