chore(checkpoint): save club core backend and pending pos fixes

This commit is contained in:
Deploy
2026-08-26 17:58:45 +02:00
parent cf3c906ed2
commit 49dfd00406
44 changed files with 2241 additions and 219 deletions

View File

@@ -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,
},
});
};

View File

@@ -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');

View 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');
};