81 lines
5.1 KiB
Markdown
81 lines
5.1 KiB
Markdown
# 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.
|