feat(POS-002): completed feature
This commit is contained in:
188
project/migrations/043_pos_basics.js
Normal file
188
project/migrations/043_pos_basics.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* F-136 / POS-002 — POS basics: 6 new tables + seed.
|
||||
*
|
||||
* Creates the POS-specific tables. Idempotent (`IF NOT EXISTS` everywhere).
|
||||
*
|
||||
* pos_stores — physical storefronts (multi-store support)
|
||||
* pos_terminals — TPV devices with persistent binding
|
||||
* pos_payment_methods — configurable payment types per store
|
||||
* pos_quick_products — favorite product grid per scope
|
||||
* pos_parked_tickets — held tickets recoverable from any terminal in the store
|
||||
* pos_cash_sessions — register shift lifecycle (OPEN/CLOSED)
|
||||
*
|
||||
* Seed at the end (after all tables exist):
|
||||
* - One pos_stores row with the well-known UUID used by the default-store
|
||||
* backfill in migration 044 and by the checkout flow's DEFAULT_STORE_ID.
|
||||
* - Two pos_payment_methods rows (cash, card) for the default store.
|
||||
*
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
|
||||
/** Well-known UUID for the default store. Kept here as the source of truth. */
|
||||
const DEFAULT_STORE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
export const up = (pgm) => {
|
||||
// ── pos_stores ────────────────────────────────────────────────────────────
|
||||
pgm.createTable('pos_stores', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
name: { type: 'text', notNull: true, check: "length(name) BETWEEN 1 AND 200" },
|
||||
slug: { type: 'text', notNull: true, unique: true, check: "slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'" },
|
||||
address: { type: 'text' },
|
||||
tax_id: { type: 'text' },
|
||||
contact_email: { type: 'text' },
|
||||
contact_phone: { type: 'text' },
|
||||
receipt_header: { type: 'text' },
|
||||
receipt_footer: { type: 'text' },
|
||||
settings: { type: 'jsonb', notNull: true, default: pgm.func("'{}'::jsonb") },
|
||||
active: { type: 'boolean', notNull: true, default: true },
|
||||
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
});
|
||||
|
||||
// ── pos_terminals ─────────────────────────────────────────────────────────
|
||||
pgm.createTable('pos_terminals', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
store_id: { type: 'uuid', notNull: true, references: 'pos_stores(id)', onDelete: 'RESTRICT' },
|
||||
name: { type: 'text', notNull: true, check: "length(name) BETWEEN 1 AND 100" },
|
||||
binding_code: { type: 'text', unique: true },
|
||||
bound_at: { type: 'timestamptz' },
|
||||
status: {
|
||||
type: 'text',
|
||||
notNull: true,
|
||||
default: 'active',
|
||||
check: "status IN ('active','disabled','decommissioned')",
|
||||
},
|
||||
interface_mode: {
|
||||
type: 'text',
|
||||
notNull: true,
|
||||
default: 'auto',
|
||||
check: "interface_mode IN ('desktop','touch','auto')",
|
||||
},
|
||||
settings: { type: 'jsonb', notNull: true, default: pgm.func("'{}'::jsonb") },
|
||||
last_seen_at: { type: 'timestamptz' },
|
||||
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
});
|
||||
pgm.createIndex('pos_terminals', 'store_id', { name: 'pos_terminals_store_idx' });
|
||||
pgm.createIndex('pos_terminals', 'binding_code', {
|
||||
name: 'pos_terminals_binding_code_idx',
|
||||
where: 'binding_code IS NOT NULL',
|
||||
});
|
||||
|
||||
// ── pos_payment_methods ───────────────────────────────────────────────────
|
||||
pgm.createTable('pos_payment_methods', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
store_id: { type: 'uuid', notNull: true, references: 'pos_stores(id)', onDelete: 'CASCADE' },
|
||||
code: { type: 'text', notNull: true, check: "length(code) BETWEEN 1 AND 32" },
|
||||
label: { type: 'text', notNull: true, check: "length(label) BETWEEN 1 AND 64" },
|
||||
kind: { type: 'text', notNull: true, check: "kind IN ('cash','card','other')" },
|
||||
active: { type: 'boolean', notNull: true, default: true },
|
||||
sort_order: { type: 'integer', notNull: true, default: 0 },
|
||||
config: { type: 'jsonb', notNull: true, default: pgm.func("'{}'::jsonb") },
|
||||
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
}, { constraints: { unique: ['store_id', 'code'] } });
|
||||
pgm.createIndex('pos_payment_methods', ['store_id', 'sort_order'], {
|
||||
name: 'pos_payment_methods_store_active_idx',
|
||||
where: 'active',
|
||||
});
|
||||
|
||||
// ── pos_quick_products ────────────────────────────────────────────────────
|
||||
pgm.createTable('pos_quick_products', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
store_id: { type: 'uuid', notNull: true, references: 'pos_stores(id)', onDelete: 'CASCADE' },
|
||||
terminal_id: { type: 'uuid', references: 'pos_terminals(id)', onDelete: 'CASCADE' },
|
||||
user_id: { type: 'uuid', references: 'backoffice_users(id)', onDelete: 'CASCADE' },
|
||||
variant_id: { type: 'uuid', notNull: true, references: 'catalog_product_variants(id)', onDelete: 'CASCADE' },
|
||||
position: { type: 'integer', notNull: true, default: 0 },
|
||||
active: { type: 'boolean', notNull: true, default: true },
|
||||
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
});
|
||||
|
||||
// ── pos_parked_tickets ───────────────────────────────────────────────────
|
||||
pgm.createTable('pos_parked_tickets', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
store_id: { type: 'uuid', notNull: true, references: 'pos_stores(id)', onDelete: 'CASCADE' },
|
||||
terminal_id: { type: 'uuid', notNull: true, references: 'pos_terminals(id)', onDelete: 'RESTRICT' },
|
||||
user_id: { type: 'uuid', notNull: true, references: 'backoffice_users(id)', onDelete: 'RESTRICT' },
|
||||
customer_id: { type: 'uuid', references: 'identity_users(id)', onDelete: 'SET NULL' },
|
||||
label: { type: 'text' },
|
||||
cart: { type: 'jsonb', notNull: true },
|
||||
expires_at: {
|
||||
type: 'timestamptz',
|
||||
notNull: true,
|
||||
default: pgm.func("(now() + interval '24 hours')"),
|
||||
},
|
||||
recalled_at: { type: 'timestamptz' },
|
||||
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
});
|
||||
pgm.createIndex('pos_parked_tickets', ['store_id', { name: 'created_at', sort: 'DESC' }], {
|
||||
name: 'pos_parked_tickets_store_active_idx',
|
||||
where: 'recalled_at IS NULL',
|
||||
});
|
||||
|
||||
// ── pos_cash_sessions ────────────────────────────────────────────────────
|
||||
pgm.createTable('pos_cash_sessions', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
terminal_id: { type: 'uuid', notNull: true, references: 'pos_terminals(id)', onDelete: 'RESTRICT' },
|
||||
store_id: { type: 'uuid', notNull: true, references: 'pos_stores(id)', onDelete: 'RESTRICT' },
|
||||
user_id: { type: 'uuid', notNull: true, references: 'backoffice_users(id)', onDelete: 'RESTRICT' },
|
||||
status: { type: 'text', notNull: true, default: 'OPEN', check: "status IN ('OPEN','CLOSED')" },
|
||||
opened_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
closed_at: { type: 'timestamptz' },
|
||||
opening_cash_cents: { type: 'integer', notNull: true, default: 0, check: 'opening_cash_cents >= 0' },
|
||||
closing_cash_cents: { type: 'integer' },
|
||||
expected_cash_cents: { type: 'integer' },
|
||||
actual_cash_cents: { type: 'integer' },
|
||||
difference_cents: { type: 'integer' },
|
||||
notes: { type: 'text' },
|
||||
created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
|
||||
});
|
||||
// Only one OPEN session per terminal at a time.
|
||||
pgm.createIndex('pos_cash_sessions', 'terminal_id', {
|
||||
name: 'pos_cash_sessions_open_per_terminal_idx',
|
||||
unique: true,
|
||||
where: "status = 'OPEN'",
|
||||
});
|
||||
pgm.createIndex('pos_cash_sessions', 'store_id', { name: 'pos_cash_sessions_store_idx' });
|
||||
pgm.createIndex('pos_cash_sessions', 'user_id', { name: 'pos_cash_sessions_user_idx' });
|
||||
|
||||
// ── Seeds ────────────────────────────────────────────────────────────────
|
||||
// The default store; this UUID is also referenced as DEFAULT_STORE_ID from
|
||||
// the inventory module (see 044_pos_inventory_store.js and
|
||||
// project/src/modules/inventory/domain/ports.ts).
|
||||
pgm.sql(`
|
||||
INSERT INTO pos_stores (id, name, slug, active, receipt_header, receipt_footer)
|
||||
VALUES (
|
||||
'${DEFAULT_STORE_ID}',
|
||||
'Natural - Mercado de Vida',
|
||||
'natural-mercado-de-vida',
|
||||
true,
|
||||
'Natural - Mercado de Vida',
|
||||
'Gracias por su compra'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`);
|
||||
|
||||
pgm.sql(`
|
||||
INSERT INTO pos_payment_methods (store_id, code, label, kind, sort_order)
|
||||
VALUES
|
||||
('${DEFAULT_STORE_ID}', 'cash', 'Efectivo', 'cash', 0),
|
||||
('${DEFAULT_STORE_ID}', 'card', 'Tarjeta', 'card', 1)
|
||||
ON CONFLICT (store_id, code) DO NOTHING
|
||||
`);
|
||||
};
|
||||
|
||||
export const down = (pgm) => {
|
||||
// Drop in reverse dependency order. The CASCADE on FKs cleans up dependent
|
||||
// rows automatically.
|
||||
pgm.dropTable('pos_cash_sessions');
|
||||
pgm.dropTable('pos_parked_tickets');
|
||||
pgm.dropTable('pos_quick_products');
|
||||
pgm.dropTable('pos_payment_methods');
|
||||
pgm.dropTable('pos_terminals');
|
||||
pgm.dropTable('pos_stores');
|
||||
};
|
||||
112
project/migrations/044_pos_inventory_store.js
Normal file
112
project/migrations/044_pos_inventory_store.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* POS-002 — Add `store_id` to `inventory_stock` and `inventory_movements`.
|
||||
*
|
||||
* Steps in `up()`:
|
||||
* 1. Add `store_id` as a NULLABLE column (so the backfill doesn't fail).
|
||||
* 2. Backfill every existing row with the well-known default store UUID.
|
||||
* 3. Make `store_id` NOT NULL and add the FK to `pos_stores`.
|
||||
* 4. Drop the old single-column unique constraint on `inventory_stock.variant_id`.
|
||||
* 5. Add the new composite unique constraint `(variant_id, store_id)`.
|
||||
* 6. Mirror the same 5-step pattern for `inventory_movements` (without the
|
||||
* unique constraint, since movements don't have a UNIQUE invariant).
|
||||
*
|
||||
* Idempotent: every ALTER uses `IF NOT EXISTS` / `IF EXISTS` / `DO` blocks
|
||||
* so re-running is a no-op.
|
||||
*
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
|
||||
const DEFAULT_STORE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
export const up = async (pgm) => {
|
||||
// ── inventory_stock ───────────────────────────────────────────────────────
|
||||
pgm.sql(`
|
||||
ALTER TABLE inventory_stock
|
||||
ADD COLUMN IF NOT EXISTS store_id uuid
|
||||
`);
|
||||
pgm.sql(`
|
||||
UPDATE inventory_stock
|
||||
SET store_id = '${DEFAULT_STORE_ID}'
|
||||
WHERE store_id IS NULL
|
||||
`);
|
||||
pgm.sql(`
|
||||
ALTER TABLE inventory_stock
|
||||
ALTER COLUMN store_id SET NOT NULL
|
||||
`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'inventory_stock_store_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE inventory_stock
|
||||
ADD CONSTRAINT inventory_stock_store_id_fkey
|
||||
FOREIGN KEY (store_id) REFERENCES pos_stores(id) ON DELETE RESTRICT;
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
pgm.sql(`ALTER TABLE inventory_stock DROP CONSTRAINT IF EXISTS inventory_stock_variant_id_key`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'inventory_stock_variant_store_unique'
|
||||
) THEN
|
||||
ALTER TABLE inventory_stock
|
||||
ADD CONSTRAINT inventory_stock_variant_store_unique
|
||||
UNIQUE (variant_id, store_id);
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
|
||||
// ── inventory_movements ──────────────────────────────────────────────────
|
||||
pgm.sql(`
|
||||
ALTER TABLE inventory_movements
|
||||
ADD COLUMN IF NOT EXISTS store_id uuid
|
||||
`);
|
||||
pgm.sql(`
|
||||
UPDATE inventory_movements
|
||||
SET store_id = '${DEFAULT_STORE_ID}'
|
||||
WHERE store_id IS NULL
|
||||
`);
|
||||
pgm.sql(`
|
||||
ALTER TABLE inventory_movements
|
||||
ALTER COLUMN store_id SET NOT NULL
|
||||
`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'inventory_movements_store_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE inventory_movements
|
||||
ADD CONSTRAINT inventory_movements_store_id_fkey
|
||||
FOREIGN KEY (store_id) REFERENCES pos_stores(id) ON DELETE RESTRICT;
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
};
|
||||
|
||||
export const down = (pgm) => {
|
||||
pgm.sql(`ALTER TABLE inventory_movements DROP CONSTRAINT IF EXISTS inventory_movements_store_id_fkey`);
|
||||
pgm.sql(`ALTER TABLE inventory_movements DROP COLUMN IF EXISTS store_id`);
|
||||
|
||||
pgm.sql(`ALTER TABLE inventory_stock DROP CONSTRAINT IF EXISTS inventory_stock_variant_store_unique`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'inventory_stock_variant_id_key'
|
||||
) THEN
|
||||
ALTER TABLE inventory_stock
|
||||
ADD CONSTRAINT inventory_stock_variant_id_key UNIQUE (variant_id);
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
pgm.sql(`ALTER TABLE inventory_stock DROP CONSTRAINT IF EXISTS inventory_stock_store_id_fkey`);
|
||||
pgm.sql(`ALTER TABLE inventory_stock DROP COLUMN IF EXISTS store_id`);
|
||||
};
|
||||
149
project/migrations/045_pos_orders_source.js
Normal file
149
project/migrations/045_pos_orders_source.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* POS-002 — Add `source`, `terminal_id`, `cash_session_id` to `orders_orders`;
|
||||
* drop NOT NULL on `user_id`; add `COMPLETED` to the state CHECK.
|
||||
*
|
||||
* The `source` column distinguishes ecommerce / pos / admin sales. POS sales
|
||||
* may have `user_id IS NULL` (walk-in customer); ecommerce and admin sales
|
||||
* must have a non-null `user_id` (enforced by the application layer; this
|
||||
* migration only relaxes the column).
|
||||
*
|
||||
* The `terminal_id` and `cash_session_id` FKs use `ON DELETE SET NULL` so
|
||||
* decommissioning a terminal or closing its session does not delete
|
||||
* historical sales.
|
||||
*
|
||||
* The state CHECK is rewritten with the new `COMPLETED` value. POS sales
|
||||
* transition PENDING → AWAITING_PAYMENT → PAID → COMPLETED, skipping
|
||||
* SHIPPED / DELIVERED.
|
||||
*
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
|
||||
export const up = (pgm) => {
|
||||
// ── Add columns ──────────────────────────────────────────────────────────
|
||||
pgm.sql(`
|
||||
ALTER TABLE orders_orders
|
||||
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'ecommerce'
|
||||
`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'orders_orders_source_check'
|
||||
) THEN
|
||||
ALTER TABLE orders_orders
|
||||
ADD CONSTRAINT orders_orders_source_check
|
||||
CHECK (source IN ('ecommerce','pos','admin'));
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
|
||||
pgm.sql(`ALTER TABLE orders_orders ADD COLUMN IF NOT EXISTS terminal_id uuid`);
|
||||
pgm.sql(`ALTER TABLE orders_orders ADD COLUMN IF NOT EXISTS cash_session_id uuid`);
|
||||
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'orders_orders_terminal_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE orders_orders
|
||||
ADD CONSTRAINT orders_orders_terminal_id_fkey
|
||||
FOREIGN KEY (terminal_id) REFERENCES pos_terminals(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'orders_orders_cash_session_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE orders_orders
|
||||
ADD CONSTRAINT orders_orders_cash_session_id_fkey
|
||||
FOREIGN KEY (cash_session_id) REFERENCES pos_cash_sessions(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
|
||||
// ── Indexes ──────────────────────────────────────────────────────────────
|
||||
pgm.createIndex('orders_orders', 'source', { name: 'orders_orders_source_idx' });
|
||||
pgm.createIndex('orders_orders', 'terminal_id', {
|
||||
name: 'orders_orders_terminal_idx',
|
||||
where: 'terminal_id IS NOT NULL',
|
||||
});
|
||||
pgm.createIndex('orders_orders', 'cash_session_id', {
|
||||
name: 'orders_orders_cash_session_idx',
|
||||
where: 'cash_session_id IS NOT NULL',
|
||||
});
|
||||
|
||||
// ── Walk-in customers (POS only) ─────────────────────────────────────────
|
||||
pgm.sql(`ALTER TABLE orders_orders ALTER COLUMN user_id DROP NOT NULL`);
|
||||
|
||||
// CHECK: walk-in customers (user_id NULL) are allowed only for POS sales.
|
||||
// Ecommerce and admin sales must have a non-null user_id. DB-level invariant
|
||||
// enforced in addition to the application-level check.
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'orders_orders_user_required_when_not_pos'
|
||||
) THEN
|
||||
ALTER TABLE orders_orders
|
||||
ADD CONSTRAINT orders_orders_user_required_when_not_pos
|
||||
CHECK (source = 'pos' OR user_id IS NOT NULL);
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
|
||||
// ── Add COMPLETED to state CHECK ─────────────────────────────────────────
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_state_check`);
|
||||
pgm.sql(`
|
||||
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'
|
||||
))
|
||||
`);
|
||||
};
|
||||
|
||||
export const down = (pgm) => {
|
||||
// Re-tighten state CHECK to the pre-COMPLETED set; this will fail if any
|
||||
// COMPLETED rows exist.
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_state_check`);
|
||||
pgm.sql(`
|
||||
ALTER TABLE orders_orders
|
||||
ADD CONSTRAINT orders_orders_state_check
|
||||
CHECK (state IN (
|
||||
'PENDING','AWAITING_PAYMENT','PAID','PROCESSING',
|
||||
'SHIPPED','DELIVERED','CANCELLED','REFUNDED','PARTIALLY_REFUNDED'
|
||||
))
|
||||
`);
|
||||
|
||||
// user_id becomes NOT NULL again; fails if any NULL rows exist.
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_user_required_when_not_pos`);
|
||||
pgm.sql(`ALTER TABLE orders_orders ALTER COLUMN user_id SET NOT NULL`);
|
||||
|
||||
pgm.sql(`DROP INDEX IF EXISTS orders_orders_cash_session_idx`);
|
||||
pgm.sql(`DROP INDEX IF EXISTS orders_orders_terminal_idx`);
|
||||
pgm.sql(`DROP INDEX IF EXISTS orders_orders_source_idx`);
|
||||
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_cash_session_id_fkey`);
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_terminal_id_fkey`);
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS cash_session_id`);
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS terminal_id`);
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_source_check`);
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS source`);
|
||||
};
|
||||
45
project/migrations/046_pos_backoffice_roles.js
Normal file
45
project/migrations/046_pos_backoffice_roles.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* POS-002 — Extend `backoffice_users.role` CHECK to include POS roles.
|
||||
*
|
||||
* New roles: `pos_cashier`, `pos_manager`. The application-side enum
|
||||
* (`Role` in `project/src/shared/auth.ts`) is updated in the same ticket.
|
||||
*
|
||||
* Idempotent: `IF EXISTS` on the old constraint + `IF NOT EXISTS` guard on
|
||||
* the new one.
|
||||
*
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`ALTER TABLE backoffice_users DROP CONSTRAINT IF EXISTS backoffice_users_role_check`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'backoffice_users_role_check'
|
||||
) THEN
|
||||
ALTER TABLE backoffice_users
|
||||
ADD CONSTRAINT backoffice_users_role_check
|
||||
CHECK (role IN ('admin','editor','pos_cashier','pos_manager'));
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
};
|
||||
|
||||
export const down = (pgm) => {
|
||||
pgm.sql(`ALTER TABLE backoffice_users DROP CONSTRAINT IF EXISTS backoffice_users_role_check`);
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'backoffice_users_role_check'
|
||||
) THEN
|
||||
ALTER TABLE backoffice_users
|
||||
ADD CONSTRAINT backoffice_users_role_check
|
||||
CHECK (role IN ('admin','editor'));
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
};
|
||||
34
project/migrations/047_orders_user_required_when_not_pos.js
Normal file
34
project/migrations/047_orders_user_required_when_not_pos.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* POS-002 catch-up — Add the walk-in CHECK constraint to existing DBs.
|
||||
*
|
||||
* Migration 045 drops NOT NULL on `orders_orders.user_id` (so POS walk-in
|
||||
* sales work) but originally forgot the matching CHECK: ecommerce and admin
|
||||
* sales must still have a non-null user_id. This migration adds the CHECK
|
||||
* for DBs that already ran 045 before the fix landed.
|
||||
*
|
||||
* Idempotent: the constraint is added via `DO $$ IF NOT EXISTS ...`,
|
||||
* so re-running on a fresh DB that already has the CHECK (via 045) is a
|
||||
* no-op.
|
||||
*
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
*/
|
||||
|
||||
export const up = (pgm) => {
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'orders_orders_user_required_when_not_pos'
|
||||
) THEN
|
||||
ALTER TABLE orders_orders
|
||||
ADD CONSTRAINT orders_orders_user_required_when_not_pos
|
||||
CHECK (source = 'pos' OR user_id IS NOT NULL);
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
};
|
||||
|
||||
export const down = (pgm) => {
|
||||
pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_user_required_when_not_pos`);
|
||||
};
|
||||
Reference in New Issue
Block a user