-- +goose Up -- accounts holds the editable source-of-truth user-account state. -- email and user_name remain UNIQUE for both live and soft-deleted records: -- emails are never reassigned to a fresh user_id after DeleteUser, and -- user_name is immutable for the lifetime of the account. CREATE TABLE accounts ( user_id text PRIMARY KEY, email text NOT NULL, user_name text NOT NULL, display_name text NOT NULL DEFAULT '', preferred_language text NOT NULL, time_zone text NOT NULL, declared_country text, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, deleted_at timestamptz, CONSTRAINT accounts_email_unique UNIQUE (email), CONSTRAINT accounts_user_name_unique UNIQUE (user_name) ); -- Newest-first listing index used by the trusted admin user-list surface. CREATE INDEX accounts_listing_idx ON accounts (created_at DESC, user_id DESC); -- Reverse-lookup index for the optional declared-country filter; the partial -- predicate keeps the index small while declared_country is mostly NULL. CREATE INDEX accounts_declared_country_idx ON accounts (declared_country) WHERE declared_country IS NOT NULL; -- blocked_emails persists pre-user blocked-email subjects that may exist -- before any user account exists, plus the blocked subjects produced by -- BlockByUserID/BlockByEmail. resolved_user_id is populated when the block -- corresponds to an existing or formerly existing account. CREATE TABLE blocked_emails ( email text PRIMARY KEY, reason_code text NOT NULL, blocked_at timestamptz NOT NULL, actor_type text, actor_id text, resolved_user_id text ); -- entitlement_records stores the immutable history of entitlement periods. -- Each row represents one segment that was current at some point; closed -- segments carry closed_* metadata. CREATE TABLE entitlement_records ( record_id text PRIMARY KEY, user_id text NOT NULL REFERENCES accounts(user_id), plan_code text NOT NULL, source text NOT NULL, actor_type text NOT NULL, actor_id text, reason_code text NOT NULL, starts_at timestamptz NOT NULL, ends_at timestamptz, created_at timestamptz NOT NULL, closed_at timestamptz, closed_by_type text, closed_by_id text, closed_reason_code text ); CREATE INDEX entitlement_records_user_idx ON entitlement_records (user_id, created_at DESC); -- entitlement_snapshots stores the read-optimized current entitlement state. -- Exactly one row per user_id; updated atomically together with history rows -- by EntitlementLifecycleStore operations. CREATE TABLE entitlement_snapshots ( user_id text PRIMARY KEY REFERENCES accounts(user_id), plan_code text NOT NULL, is_paid boolean NOT NULL, starts_at timestamptz NOT NULL, ends_at timestamptz, source text NOT NULL, actor_type text NOT NULL, actor_id text, reason_code text NOT NULL, updated_at timestamptz NOT NULL ); -- Coarse free-versus-paid filter used by the admin listing surface. CREATE INDEX entitlement_snapshots_paid_state_idx ON entitlement_snapshots (is_paid, plan_code); -- Finite paid-expiry filter; partial predicate keeps the index limited to -- finite paid plans (paid_monthly, paid_yearly). CREATE INDEX entitlement_snapshots_paid_expiry_idx ON entitlement_snapshots (ends_at) WHERE is_paid AND ends_at IS NOT NULL; -- sanction_records stores the immutable history of sanction mutations. -- A row may carry removed_at + removed_* fields once the sanction is lifted. CREATE TABLE sanction_records ( record_id text PRIMARY KEY, user_id text NOT NULL REFERENCES accounts(user_id), sanction_code text NOT NULL, scope text NOT NULL, reason_code text NOT NULL, actor_type text NOT NULL, actor_id text, applied_at timestamptz NOT NULL, expires_at timestamptz, removed_at timestamptz, removed_by_type text, removed_by_id text, removed_reason_code text ); CREATE INDEX sanction_records_user_idx ON sanction_records (user_id, applied_at DESC); -- sanction_active stores the at-most-one active record per (user_id, -- sanction_code). It is maintained by PolicyLifecycleStore in the same -- transaction as the corresponding sanction_records mutation. CREATE TABLE sanction_active ( user_id text NOT NULL REFERENCES accounts(user_id), sanction_code text NOT NULL, record_id text NOT NULL REFERENCES sanction_records(record_id), PRIMARY KEY (user_id, sanction_code) ); CREATE INDEX sanction_active_code_idx ON sanction_active (sanction_code); -- limit_records mirrors sanction_records for user-specific limit overrides. CREATE TABLE limit_records ( record_id text PRIMARY KEY, user_id text NOT NULL REFERENCES accounts(user_id), limit_code text NOT NULL, value integer NOT NULL, reason_code text NOT NULL, actor_type text NOT NULL, actor_id text, applied_at timestamptz NOT NULL, expires_at timestamptz, removed_at timestamptz, removed_by_type text, removed_by_id text, removed_reason_code text ); CREATE INDEX limit_records_user_idx ON limit_records (user_id, applied_at DESC); -- limit_active mirrors sanction_active for user-specific limits. value is -- denormalised so the admin listing predicate can read it without joining -- the full history. CREATE TABLE limit_active ( user_id text NOT NULL REFERENCES accounts(user_id), limit_code text NOT NULL, record_id text NOT NULL REFERENCES limit_records(record_id), value integer NOT NULL, PRIMARY KEY (user_id, limit_code) ); CREATE INDEX limit_active_code_idx ON limit_active (limit_code); -- +goose Down DROP TABLE IF EXISTS limit_active; DROP TABLE IF EXISTS limit_records; DROP TABLE IF EXISTS sanction_active; DROP TABLE IF EXISTS sanction_records; DROP TABLE IF EXISTS entitlement_snapshots; DROP TABLE IF EXISTS entitlement_records; DROP TABLE IF EXISTS blocked_emails; DROP TABLE IF EXISTS accounts;