-- EnakGame vouchers, what EnakPoint is redeemed for, and what each redemption cost -- which budget (docs/rfc-enakgame.md §5.7, D5). Replaces the unused rewards table for -- EnakGame; rewards itself stays. CREATE TABLE vouchers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, image_url VARCHAR(500), voucher_type VARCHAR(30) NOT NULL, -- Rupiah: what a redemption costs, the basis of realized cost. face_value BIGINT NOT NULL, -- EnakPoint asked for it; may differ from face_value (PRD §22). point_cost BIGINT NOT NULL, -- Reporting only, never counted against a budget. business_cost BIGINT, stock_mode VARCHAR(20) NOT NULL, -- STATIC only. stock BIGINT, -- EXTERNAL only. provider VARCHAR(50), provider_ref VARCHAR(255), max_per_customer INT, valid_from TIMESTAMP WITH TIME ZONE, valid_until TIMESTAMP WITH TIME ZONE, terms JSONB NOT NULL DEFAULT '{}', status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT chk_vouchers_voucher_type CHECK (voucher_type IN ('FIXED_VALUE', 'PERCENTAGE', 'FREE_ITEM', 'MERCHANT_BENEFIT')), CONSTRAINT chk_vouchers_face_value CHECK (face_value > 0), CONSTRAINT chk_vouchers_point_cost CHECK (point_cost > 0), CONSTRAINT chk_vouchers_business_cost CHECK (business_cost IS NULL OR business_cost >= 0), CONSTRAINT chk_vouchers_stock_mode CHECK (stock_mode IN ('STATIC', 'CODE_POOL', 'EXTERNAL')), CONSTRAINT chk_vouchers_stock CHECK (stock IS NULL OR stock >= 0), CONSTRAINT chk_vouchers_static_stock CHECK ((stock_mode = 'STATIC') = (stock IS NOT NULL)), CONSTRAINT chk_vouchers_external_provider CHECK ((stock_mode = 'EXTERNAL') = (provider IS NOT NULL)), CONSTRAINT chk_vouchers_max_per_customer CHECK (max_per_customer IS NULL OR max_per_customer >= 1), CONSTRAINT chk_vouchers_validity CHECK (valid_from IS NULL OR valid_until IS NULL OR valid_until > valid_from), CONSTRAINT chk_vouchers_status CHECK (status IN ('DRAFT', 'ACTIVE', 'INACTIVE', 'ARCHIVED')) ); CREATE INDEX idx_vouchers_org_status ON vouchers(organization_id, status); -- The codes of a CODE_POOL voucher, one per redemption. CREATE TABLE voucher_codes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), voucher_id UUID NOT NULL REFERENCES vouchers(id) ON DELETE RESTRICT, code VARCHAR(255) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE', -- No foreign key: the code is taken before its redemption row is written. redemption_id UUID, expires_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT uq_voucher_codes_code UNIQUE (voucher_id, code), CONSTRAINT chk_voucher_codes_status CHECK (status IN ('AVAILABLE', 'RESERVED', 'REDEEMED', 'EXPIRED', 'CANCELLED')), CONSTRAINT chk_voucher_codes_redemption CHECK ((status IN ('RESERVED', 'REDEEMED')) = (redemption_id IS NOT NULL)) ); CREATE INDEX idx_voucher_codes_available ON voucher_codes(voucher_id, created_at) WHERE status = 'AVAILABLE'; CREATE INDEX idx_voucher_codes_expiring ON voucher_codes(expires_at) WHERE status = 'AVAILABLE' AND expires_at IS NOT NULL; -- One redemption of a voucher by a customer, with the voucher's numbers frozen (P3). CREATE TABLE voucher_redemptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE RESTRICT, voucher_id UUID NOT NULL REFERENCES vouchers(id), idempotency_key VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL, face_value BIGINT NOT NULL, point_cost BIGINT NOT NULL, voucher_code_id UUID REFERENCES voucher_codes(id), external_code VARCHAR(255), external_ref VARCHAR(255), debit_transaction_id UUID NOT NULL REFERENCES wallet_transactions(id), refund_transaction_id UUID REFERENCES wallet_transactions(id), failure_reason VARCHAR(255), attempts INT NOT NULL DEFAULT 0, completed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT uq_voucher_redemptions_key UNIQUE (customer_id, idempotency_key), CONSTRAINT uq_voucher_redemptions_debit UNIQUE (debit_transaction_id), CONSTRAINT chk_voucher_redemptions_status CHECK (status IN ('PENDING', 'COMPLETED', 'FAILED')), CONSTRAINT chk_voucher_redemptions_refund CHECK ((status = 'FAILED') = (refund_transaction_id IS NOT NULL)), CONSTRAINT chk_voucher_redemptions_completed CHECK ((status = 'COMPLETED') = (completed_at IS NOT NULL)) ); CREATE INDEX idx_voucher_redemptions_customer ON voucher_redemptions(customer_id, created_at DESC); CREATE INDEX idx_voucher_redemptions_customer_voucher ON voucher_redemptions(customer_id, voucher_id); CREATE INDEX idx_voucher_redemptions_pending ON voucher_redemptions(updated_at) WHERE status = 'PENDING'; -- What a redemption cost each budget, frozen when it completes (D5). CREATE TABLE voucher_redemption_costs ( redemption_id UUID NOT NULL REFERENCES voucher_redemptions(id), -- NULL: EnakPoint that did not come from EnakGame (EARN, ADJUSTMENT, MIGRATION). budget_id UUID REFERENCES game_budgets(id), -- Ledger type of the root lot the EnakPoint came from. source_type VARCHAR(30) NOT NULL, points BIGINT NOT NULL, -- Rupiah, a share of face_value. cost BIGINT NOT NULL, -- = completed_at, what puts the cost in a budget period. recognized_at TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT uq_voucher_redemption_costs UNIQUE (redemption_id, budget_id, source_type), CONSTRAINT chk_voucher_redemption_costs_points CHECK (points > 0), CONSTRAINT chk_voucher_redemption_costs_cost CHECK (cost >= 0) ); CREATE INDEX idx_voucher_redemption_costs_budget ON voucher_redemption_costs(budget_id, recognized_at);