-- +goose Up -- Server-driven advertising banner ("advertising network"): operator-managed campaigns rotated in -- the client's one-line announcement strip. A campaign is one placement order with a show weight -- (percent); simultaneously active campaigns compete for shows in proportion to their weights. The -- single perpetual default campaign fills the unsold remainder up to 100%. Each campaign carries -- one or more bilingual messages (en + ru, both mandatory), shown by the viewer's bot (service) -- language. Display timings are global (one settings row). Eligibility (who sees a banner at all) -- reuses account fields + the no_banner role, so it needs no schema here. See docs/ARCHITECTURE.md -- and internal/ads. SET search_path = backend, pg_catalog; -- One advertising campaign = one placement order. weight is the show percentage (1..100). is_default -- marks the single perpetual house campaign that fills the remainder up to 100% and is never deleted; -- it has no validity window (its stored weight is nominal — the rotation derives its effective weight -- as max(0, 100 - sum of active timed weights)). A time-limited campaign is active while now() lies in -- [starts_at, ends_at] (a null bound is open-ended on that side). enabled toggles a campaign without -- deleting it. CREATE TABLE ad_campaigns ( campaign_id uuid PRIMARY KEY, name text NOT NULL, weight integer NOT NULL, is_default boolean NOT NULL DEFAULT false, enabled boolean NOT NULL DEFAULT true, starts_at timestamptz, ends_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT ad_campaigns_weight_chk CHECK (weight BETWEEN 1 AND 100), -- The default campaign is perpetual (no window). CONSTRAINT ad_campaigns_default_window_chk CHECK (NOT is_default OR (starts_at IS NULL AND ends_at IS NULL)), -- A closed window must not be inverted. CONSTRAINT ad_campaigns_window_chk CHECK (starts_at IS NULL OR ends_at IS NULL OR starts_at <= ends_at) ); -- At most one default campaign. CREATE UNIQUE INDEX ad_campaigns_default_idx ON ad_campaigns (is_default) WHERE is_default; -- One bilingual message of a campaign. Both languages are mandatory: the client shows the variant for -- the viewer's bot (service) language. position orders the messages within a campaign (the round-robin -- order when the campaign wins a display slot). body_* is minimal markdown (text + links). CREATE TABLE ad_messages ( message_id uuid PRIMARY KEY, campaign_id uuid NOT NULL REFERENCES ad_campaigns (campaign_id) ON DELETE CASCADE, position integer NOT NULL DEFAULT 0, body_en text NOT NULL, body_ru text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -- Messages of a campaign are read in display order. CREATE INDEX ad_messages_campaign_idx ON ad_messages (campaign_id, position); -- Global banner display timings, a single row (id is always TRUE). The client rotator reads these: -- hold_ms is how long one message shows; edge_pause_ms and scroll_px_per_sec drive the scroll of a -- message wider than the strip; the transition between messages is fade_out_ms -> gap_ms -> fade_in_ms. -- All are clamped in Go on update. CREATE TABLE ad_settings ( id boolean PRIMARY KEY DEFAULT true, hold_ms integer NOT NULL, edge_pause_ms integer NOT NULL, scroll_px_per_sec integer NOT NULL, fade_out_ms integer NOT NULL, gap_ms integer NOT NULL, fade_in_ms integer NOT NULL, updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT ad_settings_singleton_chk CHECK (id) ); -- Seed the perpetual default campaign with one bilingual house message, and the default timings -- (the previous hardcoded UI defaults plus the new fade sequence). Fixed UUIDs keep the seed -- deterministic across environments. INSERT INTO ad_campaigns (campaign_id, name, weight, is_default, enabled) VALUES ('00000000-0000-0000-0000-0000000000ad', 'Default (house)', 100, true, true); INSERT INTO ad_messages (message_id, campaign_id, position, body_en, body_ru) VALUES ( '00000000-0000-0000-0000-0000000000a1', '00000000-0000-0000-0000-0000000000ad', 0, 'Tip: a play using all 7 tiles earns a +50 bonus.', 'Совет: ход всеми 7 фишками приносит бонус +50 очков.' ); INSERT INTO ad_settings (id, hold_ms, edge_pause_ms, scroll_px_per_sec, fade_out_ms, gap_ms, fade_in_ms) VALUES (true, 60000, 5000, 40, 1000, 250, 1000); -- +goose Down SET search_path = backend, pg_catalog; DROP TABLE IF EXISTS ad_settings; DROP TABLE IF EXISTS ad_messages; DROP TABLE IF EXISTS ad_campaigns;