-- +goose Up -- Manual account blocking ("suspension"), the operator's hard counterpart to the soft, -- reversible accounts.flagged_high_rate_at marker. Named "suspension" throughout the schema -- and Go to stay distinct from the peer-to-peer `blocks` table (one player muting another); -- the wire/UI vocabulary the player sees is "blocked". A suspension forces the player to -- forfeit every active game and replaces their whole UI with a terminal "blocked" screen until -- it is lifted or (for a temporary one) expires. See docs/ARCHITECTURE.md ยง12. SET search_path = backend, pg_catalog; -- The operator-editable reason picklist, one row per reason with its English and Russian text. -- A suspension snapshots the chosen text (account_suspensions.reason_en/ru), so editing or -- deleting a reason here never changes or breaks a reason already shown to a blocked player. CREATE TABLE suspension_reasons ( reason_id uuid PRIMARY KEY, text_en text NOT NULL, text_ru text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -- The block history: one row per block. blocked_until is NULL for a permanent block and the -- expiry instant for a temporary one; lifted_at is stamped when an operator unblocks early. -- reason_en/reason_ru are the text snapshot taken at block time (NULL when no reason was -- cited); reason_id keeps a loose link to the picklist entry for later analytics and is nulled -- if that entry is deleted (the snapshot remains the source of truth shown to the player). An -- account is currently blocked when its newest row has lifted_at IS NULL AND (blocked_until IS -- NULL OR blocked_until > now()). CREATE TABLE account_suspensions ( suspension_id uuid PRIMARY KEY, account_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE, blocked_at timestamptz NOT NULL DEFAULT now(), blocked_until timestamptz, reason_en text, reason_ru text, reason_id uuid REFERENCES suspension_reasons (reason_id) ON DELETE SET NULL, lifted_at timestamptz ); -- The enforcement gate looks up the newest suspension for an account on every authenticated -- request; this index serves that "latest by account" probe. CREATE INDEX account_suspensions_account_idx ON account_suspensions (account_id, blocked_at DESC); -- +goose Down SET search_path = backend, pg_catalog; DROP TABLE IF EXISTS account_suspensions; DROP TABLE IF EXISTS suspension_reasons;