148 lines
5.9 KiB
JavaScript
148 lines
5.9 KiB
JavaScript
/**
|
|
* CLUB-001 — Club de Clientes core backend.
|
|
*
|
|
* Phase 1 schema:
|
|
* - anonymous members + device tokens
|
|
* - ledger transactions as source of truth
|
|
* - future-ready recovery/campaign/reward tables
|
|
* - club config seeds in store_settings
|
|
*/
|
|
|
|
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
|
export const up = (pgm) => {
|
|
pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS club_members (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id uuid REFERENCES identity_users(id) ON DELETE SET NULL,
|
|
member_code text NOT NULL UNIQUE,
|
|
status text NOT NULL DEFAULT 'active',
|
|
tier_code text NOT NULL DEFAULT 'base',
|
|
current_balance_cents integer NOT NULL DEFAULT 0,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT club_members_status_check CHECK (status IN ('active', 'blocked', 'merged')),
|
|
CONSTRAINT club_members_balance_non_negative CHECK (current_balance_cents >= 0),
|
|
CONSTRAINT club_members_member_code_format CHECK (member_code ~ '^MDV-[A-Z0-9]{8}$')
|
|
)
|
|
`);
|
|
|
|
pgm.sql(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS club_members_user_id_unique_idx
|
|
ON club_members (user_id)
|
|
WHERE user_id IS NOT NULL
|
|
`);
|
|
|
|
pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS club_devices (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
member_id uuid NOT NULL REFERENCES club_members(id) ON DELETE CASCADE,
|
|
device_token_hash text NOT NULL UNIQUE,
|
|
last_used_at timestamptz NOT NULL DEFAULT now(),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
revoked_at timestamptz
|
|
)
|
|
`);
|
|
pgm.sql(`CREATE INDEX IF NOT EXISTS club_devices_member_id_idx ON club_devices (member_id)`);
|
|
|
|
pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS club_campaigns (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name text NOT NULL,
|
|
kind text NOT NULL DEFAULT 'generic',
|
|
status text NOT NULL DEFAULT 'draft',
|
|
config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
starts_at timestamptz,
|
|
ends_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT club_campaigns_status_check CHECK (status IN ('draft', 'active', 'archived'))
|
|
)
|
|
`);
|
|
|
|
pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS club_transactions (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
member_id uuid NOT NULL REFERENCES club_members(id) ON DELETE CASCADE,
|
|
sale_id uuid REFERENCES orders_orders(id) ON DELETE SET NULL,
|
|
store_id uuid REFERENCES pos_stores(id) ON DELETE SET NULL,
|
|
type text NOT NULL,
|
|
amount_cents integer NOT NULL DEFAULT 0,
|
|
balance_delta_cents integer NOT NULL,
|
|
idempotency_key text UNIQUE,
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT club_transactions_type_check CHECK (type IN ('earn', 'redeem', 'refund', 'bonus', 'adjustment')),
|
|
CONSTRAINT club_transactions_amount_non_negative CHECK (amount_cents >= 0),
|
|
CONSTRAINT club_transactions_balance_delta_non_zero CHECK (balance_delta_cents <> 0)
|
|
)
|
|
`);
|
|
pgm.sql(`
|
|
CREATE INDEX IF NOT EXISTS club_transactions_member_id_created_at_idx
|
|
ON club_transactions (member_id, created_at DESC)
|
|
`);
|
|
pgm.sql(`CREATE INDEX IF NOT EXISTS club_transactions_sale_id_idx ON club_transactions (sale_id)`);
|
|
|
|
pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS club_recovery_codes (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
member_id uuid NOT NULL REFERENCES club_members(id) ON DELETE CASCADE,
|
|
code_hash text NOT NULL,
|
|
code_fingerprint text NOT NULL UNIQUE,
|
|
used_at timestamptz,
|
|
expires_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
)
|
|
`);
|
|
pgm.sql(`CREATE INDEX IF NOT EXISTS club_recovery_codes_member_id_idx ON club_recovery_codes (member_id)`);
|
|
|
|
pgm.sql(`
|
|
CREATE TABLE IF NOT EXISTS club_rewards (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
member_id uuid NOT NULL REFERENCES club_members(id) ON DELETE CASCADE,
|
|
campaign_id uuid REFERENCES club_campaigns(id) ON DELETE SET NULL,
|
|
status text NOT NULL DEFAULT 'available',
|
|
label text NOT NULL,
|
|
amount_cents integer,
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
expires_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT club_rewards_status_check CHECK (status IN ('available', 'redeemed', 'expired', 'cancelled'))
|
|
)
|
|
`);
|
|
pgm.sql(`CREATE INDEX IF NOT EXISTS club_rewards_member_id_idx ON club_rewards (member_id)`);
|
|
|
|
pgm.sql(`
|
|
INSERT INTO store_settings (key, value) VALUES
|
|
('club_enabled', 'true'),
|
|
('club_cashback_bps', '200'),
|
|
('club_allow_anonymous_members', 'true'),
|
|
('club_allow_recovery_codes', 'true'),
|
|
('club_minimum_redeem_cents', '500')
|
|
ON CONFLICT (key) DO NOTHING
|
|
`);
|
|
};
|
|
|
|
/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
|
|
export const down = (pgm) => {
|
|
pgm.sql(`DELETE FROM store_settings WHERE key IN (
|
|
'club_enabled',
|
|
'club_cashback_bps',
|
|
'club_allow_anonymous_members',
|
|
'club_allow_recovery_codes',
|
|
'club_minimum_redeem_cents'
|
|
)`);
|
|
pgm.sql('DROP INDEX IF EXISTS club_rewards_member_id_idx');
|
|
pgm.sql('DROP TABLE IF EXISTS club_rewards');
|
|
pgm.sql('DROP INDEX IF EXISTS club_recovery_codes_member_id_idx');
|
|
pgm.sql('DROP TABLE IF EXISTS club_recovery_codes');
|
|
pgm.sql('DROP INDEX IF EXISTS club_transactions_sale_id_idx');
|
|
pgm.sql('DROP INDEX IF EXISTS club_transactions_member_id_created_at_idx');
|
|
pgm.sql('DROP TABLE IF EXISTS club_transactions');
|
|
pgm.sql('DROP TABLE IF EXISTS club_campaigns');
|
|
pgm.sql('DROP INDEX IF EXISTS club_devices_member_id_idx');
|
|
pgm.sql('DROP TABLE IF EXISTS club_devices');
|
|
pgm.sql('DROP INDEX IF EXISTS club_members_user_id_unique_idx');
|
|
pgm.sql('DROP TABLE IF EXISTS club_members');
|
|
};
|