chore(checkpoint): save club core backend and pending pos fixes
This commit is contained in:
@@ -5,10 +5,12 @@
|
||||
*/
|
||||
|
||||
export const up = (pgm) => {
|
||||
pgm.addColumn('catalog_product_variants', 'weight_grams', {
|
||||
type: 'integer',
|
||||
notNull: false,
|
||||
default: null,
|
||||
pgm.addColumn('catalog_product_variants', {
|
||||
weight_grams: {
|
||||
type: 'integer',
|
||||
notNull: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,20 +6,22 @@
|
||||
*/
|
||||
|
||||
export const up = (pgm) => {
|
||||
pgm.addColumn('identity_users', 'confirmation_token', {
|
||||
type: 'string',
|
||||
notNull: false,
|
||||
default: null,
|
||||
});
|
||||
pgm.addColumn('identity_users', 'confirmed_at', {
|
||||
type: 'timestamp',
|
||||
notNull: false,
|
||||
default: null,
|
||||
});
|
||||
pgm.addColumn('identity_users', 'email_confirmed', {
|
||||
type: 'boolean',
|
||||
notNull: true,
|
||||
default: false,
|
||||
pgm.addColumn('identity_users', {
|
||||
confirmation_token: {
|
||||
type: 'text',
|
||||
notNull: false,
|
||||
default: null,
|
||||
},
|
||||
confirmed_at: {
|
||||
type: 'timestamptz',
|
||||
notNull: false,
|
||||
default: null,
|
||||
},
|
||||
email_confirmed: {
|
||||
type: 'boolean',
|
||||
notNull: true,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
// FEAT-199: migrate existing users to confirmed (they already verified their email during signup)
|
||||
pgm.sql('UPDATE identity_users SET email_confirmed = true');
|
||||
|
||||
147
project/migrations/066_club_core.js
Normal file
147
project/migrations/066_club_core.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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');
|
||||
};
|
||||
Reference in New Issue
Block a user