-- EnakGame budgets, reward configurations and sessions (docs/rfc-enakgame.md §5.2, -- §5.3, §5.4, §5.6), in foreign key order. -- What an organization may spend on EnakGame rewards (§5.6). The global budget pays for -- base rewards, one per period; an event budget pays for what its event adds. CREATE TABLE game_budgets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, scope VARCHAR(20) NOT NULL, name VARCHAR(255) NOT NULL, period_start DATE NOT NULL, period_end DATE NOT NULL, -- Rupiah. amount BIGINT NOT NULL, -- {"warning": 70, "critical": 90}: percent of utilization or forecast (PRD §8, §32). thresholds JSONB NOT NULL DEFAULT '{}', -- PRD §34, pending a decision (RFC §19.2). exhaustion_policy VARCHAR(30), created_by UUID NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT chk_game_budgets_scope CHECK (scope IN ('GLOBAL', 'EVENT')), CONSTRAINT chk_game_budgets_amount CHECK (amount > 0), CONSTRAINT chk_game_budgets_period CHECK (period_end >= period_start) ); -- Global budgets of an organization do not overlap: one row per period. CREATE UNIQUE INDEX uq_game_budgets_global_period ON game_budgets(organization_id, period_start) WHERE scope = 'GLOBAL'; CREATE INDEX idx_game_budgets_org_scope ON game_budgets(organization_id, scope, period_start DESC); -- How a game computes its base reward (§5.2, §8). Immutable apart from status (D7): a -- change is a new row with the next version, and a session keeps the one it started -- with. CREATE TABLE game_reward_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, game_id UUID NOT NULL REFERENCES games(id) ON DELETE RESTRICT, version INT NOT NULL, reward_type VARCHAR(30) NOT NULL, -- Shape per reward_type in §8. rules JSONB NOT NULL, max_reward BIGINT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', effective_at TIMESTAMP WITH TIME ZONE, created_by UUID NOT NULL, reason VARCHAR(255), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT uq_game_reward_configs_version UNIQUE (game_id, version), CONSTRAINT chk_game_reward_configs_version CHECK (version >= 1), CONSTRAINT chk_game_reward_configs_reward_type CHECK (reward_type IN ('FIXED', 'SCORE_BASED', 'OUTCOME_BASED', 'PROBABILITY')), CONSTRAINT chk_game_reward_configs_max_reward CHECK (max_reward >= 0), CONSTRAINT chk_game_reward_configs_status CHECK (status IN ('DRAFT', 'ACTIVE', 'RETIRED')) ); -- One active configuration per game. CREATE UNIQUE INDEX uq_game_reward_configs_active ON game_reward_configs(game_id) WHERE status = 'ACTIVE'; -- One play of a game by a customer (§5.3). STARTED → COMPLETED | REFUNDED | EXPIRED, -- each move an UPDATE ... WHERE status = 'STARTED' (D4). CREATE TABLE game_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE RESTRICT, game_id UUID NOT NULL REFERENCES games(id) ON DELETE RESTRICT, -- Snapshots taken at start (D7, P3). reward_config_id UUID NOT NULL REFERENCES game_reward_configs(id), entry_cost BIGINT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'STARTED', started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, ended_at TIMESTAMP WITH TIME ZONE, -- The result the client sent: data only, never a reward amount (P1). result JSONB, -- Validation and calculation: base, event modifiers, guard caps, rejection reasons, -- RNG roll. reward_breakdown JSONB, reward_total BIGINT NOT NULL DEFAULT 0, flagged BOOLEAN NOT NULL DEFAULT FALSE, spend_transaction_id UUID NOT NULL REFERENCES wallet_transactions(id), refund_transaction_id UUID REFERENCES wallet_transactions(id), refund_reason VARCHAR(30), -- Set, in a transaction of its own, when completing failed on a system error (§7.3). completion_failed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT uq_game_sessions_spend_transaction UNIQUE (spend_transaction_id), CONSTRAINT chk_game_sessions_entry_cost CHECK (entry_cost >= 1), CONSTRAINT chk_game_sessions_status CHECK (status IN ('STARTED', 'COMPLETED', 'REFUNDED', 'EXPIRED')), CONSTRAINT chk_game_sessions_reward_total CHECK (reward_total >= 0), CONSTRAINT chk_game_sessions_refund_reason CHECK (refund_reason IN ('SYSTEM_ERROR', 'GAME_DEACTIVATED')), CONSTRAINT chk_game_sessions_refund CHECK ( (status = 'REFUNDED') = (refund_transaction_id IS NOT NULL AND refund_reason IS NOT NULL)) ); CREATE INDEX idx_game_sessions_customer ON game_sessions(customer_id, started_at DESC); CREATE INDEX idx_game_sessions_open ON game_sessions(expires_at) WHERE status = 'STARTED'; CREATE INDEX idx_game_sessions_game_open ON game_sessions(game_id) WHERE status = 'STARTED'; -- One row per budget paying for a session's reward (D6), each with its own ledger row. CREATE TABLE game_session_rewards ( session_id UUID NOT NULL REFERENCES game_sessions(id), budget_id UUID NOT NULL REFERENCES game_budgets(id), amount BIGINT NOT NULL, wallet_transaction_id UUID NOT NULL REFERENCES wallet_transactions(id), PRIMARY KEY (session_id, budget_id), CONSTRAINT uq_game_session_rewards_wallet_transaction UNIQUE (wallet_transaction_id), CONSTRAINT chk_game_session_rewards_amount CHECK (amount > 0) ); CREATE INDEX idx_game_session_rewards_budget ON game_session_rewards(budget_id);