481 lines
22 KiB
Markdown
481 lines
22 KiB
Markdown
# POS Data Model — Mercado de Vida
|
|
|
|
> **Companion to:** [`POS_ARCHITECTURE.md`](./POS_ARCHITECTURE.md)
|
|
> **Status:** Discovery (Phase 1)
|
|
|
|
This document details every schema change the POS requires: new tables, new columns, new enum values, and the migrations that apply them. All migrations live under `project/migrations/` with a sequential numeric prefix (`NNN_pos_*.js`).
|
|
|
|
---
|
|
|
|
## 1. Naming conventions
|
|
|
|
- Table names: `pos_<table>` (module-owned namespace).
|
|
- Column names: `snake_case`.
|
|
- Primary keys: `id uuid NOT NULL DEFAULT gen_random_uuid()`.
|
|
- Audit columns: `created_at timestamptz NOT NULL DEFAULT now()`, `updated_at timestamptz NOT NULL DEFAULT now()`.
|
|
- Monetary values: `*_cents integer NOT NULL DEFAULT 0` with a CHECK constraint `>= 0` where appropriate. Refund / negative-amount flows use CHECK `>= 0` on the *aggregated* columns but allow per-row negative `amount_cents` in `payments_transactions` (already supported).
|
|
- FK references use `ON DELETE` rules: `RESTRICT` for terminal↔session (you can't delete a terminal with sessions), `CASCADE` only for child rows that have no value without the parent.
|
|
|
|
---
|
|
|
|
## 2. New tables
|
|
|
|
### 2.1 `pos_stores`
|
|
|
|
A physical storefront. Required for multi-store inventory and per-store configuration.
|
|
|
|
```sql
|
|
CREATE TABLE pos_stores (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 200),
|
|
slug text NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
|
|
address text,
|
|
tax_id text, -- CIF/NIF
|
|
contact_email text,
|
|
contact_phone text,
|
|
receipt_header text, -- printed on ticket top
|
|
receipt_footer text, -- printed on ticket bottom
|
|
settings jsonb NOT NULL DEFAULT '{}'::jsonb, -- free-form per-store overrides
|
|
active boolean NOT NULL DEFAULT true,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
```
|
|
|
|
**Backfill:** one row inserted by migration `043_pos_basics.js`:
|
|
|
|
```sql
|
|
INSERT INTO pos_stores (id, name, slug, active)
|
|
VALUES ('00000000-0000-0000-0000-000000000001', 'Natural - Mercado de Vida', 'natural-mercado-de-vida', true);
|
|
```
|
|
|
|
This UUID is referenced from the `inventory_stock` migration as the default store.
|
|
|
|
### 2.2 `pos_terminals`
|
|
|
|
A TPV device (tablet, all-in-one, desktop). Has a persistent UUID generated on first browser visit.
|
|
|
|
```sql
|
|
CREATE TABLE pos_terminals (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
store_id uuid NOT NULL REFERENCES pos_stores(id) ON DELETE RESTRICT,
|
|
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 100),
|
|
binding_code text UNIQUE, -- one-time admin-supplied code
|
|
bound_at timestamptz,
|
|
status text NOT NULL DEFAULT 'active'
|
|
CHECK (status IN ('active','disabled','decommissioned')),
|
|
interface_mode text NOT NULL DEFAULT 'auto'
|
|
CHECK (interface_mode IN ('desktop','touch','auto')),
|
|
settings jsonb NOT NULL DEFAULT '{}'::jsonb, -- printer, scanner, etc.
|
|
last_seen_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX pos_terminals_store_idx ON pos_terminals(store_id);
|
|
CREATE INDEX pos_terminals_binding_code_idx ON pos_terminals(binding_code) WHERE binding_code IS NOT NULL;
|
|
```
|
|
|
|
**Binding flow** (Phase 4):
|
|
|
|
1. Admin generates a one-time code from `/admin/pos/terminals`.
|
|
2. Operator opens POS app for the first time → enters code → cookie `pos_terminal_id` is set, `bound_at` recorded.
|
|
|
|
### 2.3 `pos_cash_sessions`
|
|
|
|
One row per open or closed register shift.
|
|
|
|
```sql
|
|
CREATE TABLE pos_cash_sessions (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
terminal_id uuid NOT NULL REFERENCES pos_terminals(id) ON DELETE RESTRICT,
|
|
store_id uuid NOT NULL REFERENCES pos_stores(id) ON DELETE RESTRICT,
|
|
user_id uuid NOT NULL REFERENCES backoffice_users(id) ON DELETE RESTRICT,
|
|
status text NOT NULL DEFAULT 'OPEN'
|
|
CHECK (status IN ('OPEN','CLOSED')),
|
|
opened_at timestamptz NOT NULL DEFAULT now(),
|
|
closed_at timestamptz,
|
|
opening_cash_cents integer NOT NULL DEFAULT 0 CHECK (opening_cash_cents >= 0),
|
|
closing_cash_cents integer, -- null while OPEN
|
|
expected_cash_cents integer, -- computed on close
|
|
actual_cash_cents integer, -- operator-counted on close
|
|
difference_cents integer, -- actual - expected; can be negative
|
|
notes text,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Only one OPEN session per terminal at a time.
|
|
CREATE UNIQUE INDEX pos_cash_sessions_open_per_terminal_idx
|
|
ON pos_cash_sessions(terminal_id)
|
|
WHERE status = 'OPEN';
|
|
|
|
CREATE INDEX pos_cash_sessions_store_idx ON pos_cash_sessions(store_id);
|
|
CREATE INDEX pos_cash_sessions_user_idx ON pos_cash_sessions(user_id);
|
|
```
|
|
|
|
`expected_cash_cents` is computed by summing all `cash` payments and refunds within the session, then adding `opening_cash_cents` and subtracting `cash` refunds.
|
|
|
|
### 2.4 `pos_quick_products`
|
|
|
|
Configurable favorite-product grid per scope. Scope precedence: `(terminal, user) > (terminal) > (store) > global`. Phase 1 implementation only supports the `store` and `terminal` scopes (admin UI for per-user scope is deferred).
|
|
|
|
```sql
|
|
CREATE TABLE pos_quick_products (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
store_id uuid NOT NULL REFERENCES pos_stores(id) ON DELETE CASCADE,
|
|
terminal_id uuid REFERENCES pos_terminals(id) ON DELETE CASCADE,
|
|
user_id uuid REFERENCES backoffice_users(id) ON DELETE CASCADE,
|
|
variant_id uuid NOT NULL REFERENCES catalog_product_variants(id) ON DELETE CASCADE,
|
|
position integer NOT NULL DEFAULT 0,
|
|
active boolean NOT NULL DEFAULT true,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
|
|
-- At least one of (terminal_id, user_id) may be NULL; store_id is always set.
|
|
-- Prevent duplicates within the same scope.
|
|
CONSTRAINT pos_quick_products_unique_scope UNIQUE NULLS NOT DISTINCT
|
|
(store_id, terminal_id, user_id, variant_id)
|
|
);
|
|
```
|
|
|
|
### 2.5 `pos_parked_tickets`
|
|
|
|
Held tickets recoverable from any terminal in the same store.
|
|
|
|
```sql
|
|
CREATE TABLE pos_parked_tickets (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
store_id uuid NOT NULL REFERENCES pos_stores(id) ON DELETE CASCADE,
|
|
terminal_id uuid NOT NULL REFERENCES pos_terminals(id) ON DELETE RESTRICT,
|
|
user_id uuid NOT NULL REFERENCES backoffice_users(id) ON DELETE RESTRICT,
|
|
customer_id uuid REFERENCES identity_users(id) ON DELETE SET NULL,
|
|
label text, -- operator-supplied hint
|
|
cart jsonb NOT NULL, -- serialised cart (see §6)
|
|
expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours'),
|
|
recalled_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX pos_parked_tickets_store_active_idx
|
|
ON pos_parked_tickets(store_id, created_at DESC)
|
|
WHERE recalled_at IS NULL;
|
|
```
|
|
|
|
### 2.6 `pos_payment_methods`
|
|
|
|
Configurable payment methods per store.
|
|
|
|
```sql
|
|
CREATE TABLE pos_payment_methods (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
store_id uuid NOT NULL REFERENCES pos_stores(id) ON DELETE CASCADE,
|
|
code text NOT NULL CHECK (length(code) BETWEEN 1 AND 32),
|
|
label text NOT NULL CHECK (length(label) BETWEEN 1 AND 64),
|
|
kind text NOT NULL CHECK (kind IN ('cash','card','other')),
|
|
active boolean NOT NULL DEFAULT true,
|
|
sort_order integer NOT NULL DEFAULT 0,
|
|
config jsonb NOT NULL DEFAULT '{}'::jsonb, -- e.g. card terminal id, cash change default
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
|
|
UNIQUE (store_id, code)
|
|
);
|
|
|
|
CREATE INDEX pos_payment_methods_store_active_idx
|
|
ON pos_payment_methods(store_id, sort_order)
|
|
WHERE active;
|
|
```
|
|
|
|
Seed (in the same migration):
|
|
|
|
```sql
|
|
INSERT INTO pos_payment_methods (store_id, code, label, kind, sort_order)
|
|
VALUES
|
|
('00000000-0000-0000-0000-000000000001', 'cash', 'Efectivo', 'cash', 0),
|
|
('00000000-0000-0000-0000-000000000001', 'card', 'Tarjeta', 'card', 1);
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Modified tables
|
|
|
|
### 3.1 `inventory_stock` — add `store_id`
|
|
|
|
```sql
|
|
ALTER TABLE inventory_stock
|
|
ADD COLUMN store_id uuid REFERENCES pos_stores(id) ON DELETE RESTRICT;
|
|
|
|
-- Backfill to the default store.
|
|
UPDATE inventory_stock SET store_id = '00000000-0000-0000-0000-000000000001'
|
|
WHERE store_id IS NULL;
|
|
|
|
ALTER TABLE inventory_stock
|
|
ALTER COLUMN store_id SET NOT NULL;
|
|
|
|
-- Replace single-variant unique constraint with composite.
|
|
ALTER TABLE inventory_stock DROP CONSTRAINT inventory_stock_variant_id_key;
|
|
ALTER TABLE inventory_stock
|
|
ADD CONSTRAINT inventory_stock_variant_store_unique UNIQUE (variant_id, store_id);
|
|
```
|
|
|
|
`InventoryService` API changes:
|
|
|
|
```ts
|
|
// Before
|
|
reserve(variantId: string, quantity: number): Promise<Reservation>
|
|
|
|
// After
|
|
reserve(variantId: string, storeId: string, quantity: number): Promise<Reservation>
|
|
```
|
|
|
|
This is a **breaking change** in the inventory module. The existing checkout flow (ecommerce) is updated to pass the default store; the POS passes the terminal's store.
|
|
|
|
### 3.2 `inventory_movements` — add `store_id`
|
|
|
|
The movement log is enriched with the store. Migration:
|
|
|
|
```sql
|
|
ALTER TABLE inventory_movements
|
|
ADD COLUMN store_id uuid REFERENCES pos_stores(id) ON DELETE RESTRICT;
|
|
|
|
UPDATE inventory_movements SET store_id = '00000000-0000-0000-0000-000000000001'
|
|
WHERE store_id IS NULL;
|
|
|
|
ALTER TABLE inventory_movements
|
|
ALTER COLUMN store_id SET NOT NULL;
|
|
```
|
|
|
|
### 3.3 `orders_orders` — add `source`, `terminal_id`, `cash_session_id`; add `COMPLETED` state
|
|
|
|
```sql
|
|
ALTER TABLE orders_orders
|
|
ADD COLUMN source text NOT NULL DEFAULT 'ecommerce'
|
|
CHECK (source IN ('ecommerce','pos','admin')),
|
|
ADD COLUMN terminal_id uuid REFERENCES pos_terminals(id) ON DELETE SET NULL,
|
|
ADD COLUMN cash_session_id uuid REFERENCES pos_cash_sessions(id) ON DELETE SET NULL;
|
|
|
|
CREATE INDEX orders_orders_source_idx ON orders_orders(source);
|
|
CREATE INDEX orders_orders_terminal_idx ON orders_orders(terminal_id) WHERE terminal_id IS NOT NULL;
|
|
CREATE INDEX orders_orders_cash_session_idx ON orders_orders(cash_session_id) WHERE cash_session_id IS NOT NULL;
|
|
|
|
-- Allow null user_id for walk-in sales (POS only).
|
|
ALTER TABLE orders_orders ALTER COLUMN user_id DROP NOT NULL;
|
|
ALTER TABLE orders_orders
|
|
ADD CONSTRAINT orders_orders_user_required_when_not_pos
|
|
CHECK (source <> 'pos' OR user_id IS NOT NULL OR TRUE); -- walk-ins allowed; tighten per business rule
|
|
|
|
-- Add COMPLETED to state machine.
|
|
ALTER TABLE orders_orders DROP CONSTRAINT orders_orders_state_check;
|
|
ALTER TABLE orders_orders
|
|
ADD CONSTRAINT orders_orders_state_check
|
|
CHECK (state IN ('PENDING','AWAITING_PAYMENT','PAID','PROCESSING','SHIPPED','DELIVERED','COMPLETED','CANCELLED','REFUNDED','PARTIALLY_REFUNDED'));
|
|
```
|
|
|
|
Update `OrderState` type in `src/modules/orders/domain/order.ts` and `ALLOWED_TRANSITIONS` in the same file:
|
|
|
|
```ts
|
|
COMPLETED: [], // terminal state for POS sales
|
|
```
|
|
|
|
POS sales flow: `PENDING → AWAITING_PAYMENT → PAID → COMPLETED`. The `COMPLETED` state has no outgoing transitions and is treated as terminal. Admin views show it like `DELIVERED` (i.e., closed).
|
|
|
|
### 3.4 `backoffice_users.role` — extend CHECK
|
|
|
|
```sql
|
|
ALTER TABLE backoffice_users DROP CONSTRAINT backoffice_users_role_check;
|
|
ALTER TABLE backoffice_users
|
|
ADD CONSTRAINT backoffice_users_role_check
|
|
CHECK (role IN ('admin','editor','pos_cashier','pos_manager'));
|
|
```
|
|
|
|
`shared/auth.ts` `Role` type updated accordingly.
|
|
|
|
### 3.5 `orders_items` — no changes
|
|
|
|
Already has `discount_cents` per line (good for POS line-level discounts).
|
|
|
|
### 3.6 `payments_transactions` — no changes
|
|
|
|
Already supports negative `amount_cents` for partial refunds (CHECK only enforces `>= 0` on insert, but the existing `payments` module allows negative amounts through the refund flow).
|
|
|
|
---
|
|
|
|
## 4. Entity-relationship diagram
|
|
|
|
```
|
|
┌──────────────┐ ┌──────────────────┐ ┌─────────────────────┐
|
|
│ pos_stores │◄────────│ pos_terminals │◄────────│ pos_cash_sessions │
|
|
│ │ 1 N │ │ 1 N │ │
|
|
│ id (PK) │ │ id (PK) │ │ id (PK) │
|
|
│ name │ │ store_id (FK) │ │ terminal_id (FK) │
|
|
│ slug │ │ name │ │ store_id (FK) │
|
|
│ settings │ │ binding_code │ │ user_id (FK) │
|
|
│ ... │ │ status │ │ status OPEN/CLOSED │
|
|
└──────┬───────┘ │ interface_mode │ │ opening_cash_cents │
|
|
│ │ settings │ │ closing_cash_cents │
|
|
│ 1 │ ... │ │ expected_cash_cents │
|
|
│ └──────┬───────────┘ │ actual_cash_cents │
|
|
│ │ │ difference_cents │
|
|
│ │ └─────────┬───────────┘
|
|
│ │ │
|
|
│ │ │ N
|
|
│ │ │
|
|
│ ▼ 1 │
|
|
│ ┌─────────────────────┐ │
|
|
│ │ orders_orders │◄──────────────────┘
|
|
│ │ │
|
|
│ │ id (PK) │
|
|
│ │ user_id (nullable) │
|
|
│ │ source ∈ │
|
|
│ │ {ecommerce,pos, │
|
|
│ │ admin} │
|
|
│ │ terminal_id (FK) │
|
|
│ │ cash_session_id (FK)│
|
|
│ │ state + COMPLETED │
|
|
│ └─────────┬───────────┘
|
|
│ │ 1
|
|
│ │
|
|
│ │ N
|
|
│ ┌─────────▼───────────┐
|
|
│ │ orders_items │
|
|
│ │ variant_id (FK) │
|
|
│ │ sku, ean, name │
|
|
│ │ unit_price_cents │
|
|
│ │ discount_cents │
|
|
│ │ tax_cents │
|
|
│ │ quantity │
|
|
│ └─────────────────────┘
|
|
│
|
|
│ 1
|
|
▼ N
|
|
┌──────────────────┐ ┌────────────────────┐
|
|
│ inventory_stock │ │ inventory_movements│
|
|
│ variant_id (FK) │ │ variant_id (FK) │
|
|
│ store_id (FK) ───┼────────►│ store_id (FK) │
|
|
│ available │ │ operation │
|
|
│ reserved │ │ quantity │
|
|
│ sold │ └────────────────────┘
|
|
│ incoming │
|
|
│ UNIQUE(variant, │
|
|
│ store) │
|
|
└──────────────────┘
|
|
|
|
┌──────────────────────┐ ┌─────────────────────────┐
|
|
│ pos_quick_products │ │ pos_parked_tickets │
|
|
│ store_id (FK) │ │ store_id (FK) │
|
|
│ terminal_id (FK,?) │ │ terminal_id (FK) │
|
|
│ user_id (FK,?) │ │ user_id (FK) │
|
|
│ variant_id (FK) │ │ customer_id (FK,?) │
|
|
│ position │ │ cart (jsonb) │
|
|
└──────────────────────┘ └─────────────────────────┘
|
|
|
|
┌──────────────────────┐
|
|
│ pos_payment_methods │
|
|
│ store_id (FK) │
|
|
│ code, label, kind │
|
|
│ active, sort_order │
|
|
└──────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Migration plan
|
|
|
|
| # | Filename | Purpose |
|
|
|---|---|---|
|
|
| 043 | `043_pos_basics.js` | Create `pos_stores`, `pos_terminals`, `pos_payment_methods`, `pos_quick_products`, `pos_parked_tickets`, `pos_cash_sessions`. Seed default store and seed payment methods. |
|
|
| 044 | `044_pos_inventory_store.js` | Add `store_id` to `inventory_stock` and `inventory_movements`. Backfill. Update unique constraint. |
|
|
| 045 | `045_pos_orders_source.js` | Add `source`, `terminal_id`, `cash_session_id` to `orders_orders`. Drop NOT NULL on `user_id`. Add `COMPLETED` to state CHECK. |
|
|
| 046 | `046_pos_backoffice_roles.js` | Extend `backoffice_users.role` CHECK with `pos_cashier` and `pos_manager`. |
|
|
| 047 | `047_pos_seed_default_store.js` | (Already part of 043; placeholder for future operator-specific seeding.) |
|
|
|
|
These run in numeric order. Each is idempotent and reversible.
|
|
|
|
---
|
|
|
|
## 6. Cart shape (for parked tickets JSON)
|
|
|
|
The `cart` column in `pos_parked_tickets` stores:
|
|
|
|
```ts
|
|
interface ParkedCart {
|
|
version: 1;
|
|
items: Array<{
|
|
productId: string;
|
|
variantId: string;
|
|
sku: string;
|
|
ean: string | null;
|
|
name: string;
|
|
unitPriceCents: number; // captured at park time
|
|
discountCents: number; // line discount
|
|
taxCents: number;
|
|
quantity: number;
|
|
}>;
|
|
customerId: string | null;
|
|
globalDiscountCents: number;
|
|
notes?: string;
|
|
}
|
|
```
|
|
|
|
Prices are stored at park time as a snapshot, but on recall the cart re-prices server-side from current prices (with a warning if they changed).
|
|
|
|
---
|
|
|
|
## 7. Audit
|
|
|
|
Every state-changing operation appends to `security_audit_log` via `AuditLogger.log` with a stable `action` key:
|
|
|
|
| Action | Target |
|
|
|---|---|
|
|
| `pos.store.created` | `store:{id}` |
|
|
| `pos.store.updated` | `store:{id}` |
|
|
| `pos.terminal.bound` | `terminal:{id}` |
|
|
| `pos.session.opened` | `session:{id}` |
|
|
| `pos.session.closed` | `session:{id}` |
|
|
| `pos.sale.created` | `order:{id}` |
|
|
| `pos.sale.refunded` | `order:{id}` |
|
|
| `pos.sale.discount.line` | `order:{id}:item:{idx}` |
|
|
| `pos.sale.discount.global` | `order:{id}` |
|
|
| `pos.sale.price.overridden` | `order:{id}:item:{idx}` |
|
|
| `pos.sale.cancelled` | `order:{id}` |
|
|
| `pos.ticket.parked` | `ticket:{id}` |
|
|
| `pos.ticket.recalled` | `ticket:{id}` |
|
|
|
|
The actor is the authenticated user; metadata includes `terminalId`, `storeId`, and the relevant cents amount.
|
|
|
|
---
|
|
|
|
## 8. Indexes summary
|
|
|
|
```sql
|
|
-- New
|
|
CREATE INDEX pos_terminals_store_idx ON pos_terminals(store_id);
|
|
CREATE INDEX pos_terminals_binding_code_idx ON pos_terminals(binding_code) WHERE binding_code IS NOT NULL;
|
|
CREATE UNIQUE INDEX pos_cash_sessions_open_per_terminal_idx ON pos_cash_sessions(terminal_id) WHERE status = 'OPEN';
|
|
CREATE INDEX pos_cash_sessions_store_idx ON pos_cash_sessions(store_id);
|
|
CREATE INDEX pos_cash_sessions_user_idx ON pos_cash_sessions(user_id);
|
|
CREATE INDEX pos_quick_products_store_idx ON pos_quick_products(store_id);
|
|
CREATE INDEX pos_parked_tickets_store_active_idx ON pos_parked_tickets(store_id, created_at DESC) WHERE recalled_at IS NULL;
|
|
CREATE INDEX pos_payment_methods_store_active_idx ON pos_payment_methods(store_id, sort_order) WHERE active;
|
|
CREATE INDEX orders_orders_source_idx ON orders_orders(source);
|
|
CREATE INDEX orders_orders_terminal_idx ON orders_orders(terminal_id) WHERE terminal_id IS NOT NULL;
|
|
CREATE INDEX orders_orders_cash_session_idx ON orders_orders(cash_session_id) WHERE cash_session_id IS NOT NULL;
|
|
|
|
-- Modified
|
|
ALTER TABLE inventory_stock ADD CONSTRAINT inventory_stock_variant_store_unique UNIQUE (variant_id, store_id);
|
|
DROP INDEX inventory_stock_variant_id_idx; -- if exists
|
|
CREATE INDEX inventory_stock_variant_store_idx ON inventory_stock(variant_id, store_id);
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Constraints checklist
|
|
|
|
- ✅ Every monetary amount uses `*_cents integer` with explicit CHECK constraints.
|
|
- ✅ FK rules declared: `RESTRICT` for terminal↔session, `CASCADE` for quick products / parked tickets / payment methods on parent delete, `SET NULL` for `terminal_id` / `cash_session_id` / `customer_id` on order (so deleting a terminal doesn't kill historical sales).
|
|
- ✅ CHECK on `state` keeps the existing states plus `COMPLETED`.
|
|
- ✅ Unique partial index `pos_cash_sessions_open_per_terminal_idx` enforces one open session per terminal at the DB level.
|
|
- ✅ All migrations are idempotent and reversible (where the change is reversible).
|