feat: add persisted page insight platform
This commit is contained in:
141
migrations/000004_ai_page_insights.sql
Normal file
141
migrations/000004_ai_page_insights.sql
Normal file
@@ -0,0 +1,141 @@
|
||||
CREATE TABLE IF NOT EXISTS calendar_catalog_versions (
|
||||
id bigserial PRIMARY KEY,
|
||||
version text NOT NULL UNIQUE,
|
||||
status text NOT NULL CHECK (status IN ('DRAFT','APPROVED','ACTIVE','RETIRED')),
|
||||
coverage_start_jalali_year integer NOT NULL,
|
||||
coverage_end_jalali_year integer NOT NULL,
|
||||
source_uri text NOT NULL,
|
||||
source_version text NOT NULL,
|
||||
source_checksum text NOT NULL,
|
||||
imported_by text NOT NULL,
|
||||
reviewed_by text,
|
||||
imported_at timestamptz NOT NULL DEFAULT now(),
|
||||
reviewed_at timestamptz,
|
||||
activated_at timestamptz,
|
||||
CHECK (coverage_end_jalali_year >= coverage_start_jalali_year)
|
||||
);
|
||||
-- statement-break
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS calendar_catalog_one_active_idx
|
||||
ON calendar_catalog_versions ((status)) WHERE status='ACTIVE';
|
||||
-- statement-break
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id bigserial PRIMARY KEY,
|
||||
catalog_version_id bigint NOT NULL REFERENCES calendar_catalog_versions(id) ON DELETE CASCADE,
|
||||
event_key text NOT NULL,
|
||||
title_fa text NOT NULL,
|
||||
title_en text,
|
||||
start_date date NOT NULL,
|
||||
end_date date NOT NULL,
|
||||
recurrence_rule text,
|
||||
event_type text NOT NULL CHECK (event_type IN ('PUBLIC_HOLIDAY','RELIGIOUS','NATIONAL','CULTURAL','COMMERCIAL','SEASONAL','EDUCATION','ECONOMIC','EMERGENCY_CLOSURE')),
|
||||
tone text NOT NULL CHECK (tone IN ('CELEBRATORY','JOYFUL','NEUTRAL','SOLEMN','MOURNING','MEMORIAL')),
|
||||
commercial_posture text NOT NULL CHECK (commercial_posture IN ('PROMOTE','CAUTION','NO_PROMOTION')),
|
||||
official_holiday boolean NOT NULL DEFAULT false,
|
||||
source_uri text NOT NULL,
|
||||
source_version text NOT NULL,
|
||||
source_checksum text,
|
||||
inclusion_rationale text NOT NULL,
|
||||
geographic_scope text NOT NULL DEFAULT 'IR',
|
||||
merchant_categories jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
behavioral_channels jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
lead_days integer NOT NULL DEFAULT 0 CHECK (lead_days BETWEEN 0 AND 90),
|
||||
lag_days integer NOT NULL DEFAULT 0 CHECK (lag_days BETWEEN 0 AND 90),
|
||||
relevance_bps integer NOT NULL CHECK (relevance_bps BETWEEN 0 AND 10000),
|
||||
confidence_bps integer NOT NULL CHECK (confidence_bps BETWEEN 0 AND 10000),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CHECK (end_date >= start_date),
|
||||
CHECK (tone NOT IN ('MOURNING','MEMORIAL') OR commercial_posture <> 'PROMOTE'),
|
||||
UNIQUE (catalog_version_id,event_key)
|
||||
);
|
||||
-- statement-break
|
||||
CREATE INDEX IF NOT EXISTS calendar_events_active_range_idx
|
||||
ON calendar_events (start_date,end_date,catalog_version_id);
|
||||
-- statement-break
|
||||
CREATE TABLE IF NOT EXISTS calendar_catalog_audit (
|
||||
id bigserial PRIMARY KEY,
|
||||
catalog_version_id bigint NOT NULL REFERENCES calendar_catalog_versions(id) ON DELETE CASCADE,
|
||||
action text NOT NULL CHECK (action IN ('IMPORTED','APPROVED','ACTIVATED','RETIRED')),
|
||||
actor text NOT NULL,
|
||||
occurred_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
-- statement-break
|
||||
CREATE TABLE IF NOT EXISTS ai_insight_snapshots (
|
||||
id text PRIMARY KEY,
|
||||
merchant_id bigint NOT NULL REFERENCES merchants(id),
|
||||
terminal_id bigint REFERENCES terminals(id),
|
||||
terminal_source_key text,
|
||||
module text NOT NULL CHECK (module IN ('overview','payments','fees','peers','transactions','growth')),
|
||||
revision integer NOT NULL CHECK (revision > 0),
|
||||
fingerprint text NOT NULL,
|
||||
status text NOT NULL CHECK (status IN ('QUEUED','GENERATING','READY','PARTIAL','FAILED')),
|
||||
as_of_date date NOT NULL,
|
||||
data_through_date date NOT NULL,
|
||||
freshness text NOT NULL CHECK (freshness IN ('FRESH','LAGGING','STALE')),
|
||||
data_lag_days integer NOT NULL CHECK (data_lag_days >= 0),
|
||||
confidence text NOT NULL CHECK (confidence IN ('HIGH','MEDIUM','LOW','INSUFFICIENT_DATA')),
|
||||
catalog_version text NOT NULL,
|
||||
analytics_version text NOT NULL,
|
||||
prompt_version text NOT NULL,
|
||||
schema_version text NOT NULL,
|
||||
model text NOT NULL,
|
||||
deterministic_evidence jsonb NOT NULL,
|
||||
ai_response jsonb,
|
||||
failure_code text,
|
||||
input_tokens bigint NOT NULL DEFAULT 0 CHECK (input_tokens >= 0),
|
||||
output_tokens bigint NOT NULL DEFAULT 0 CHECK (output_tokens >= 0),
|
||||
provider_requests bigint NOT NULL DEFAULT 0 CHECK (provider_requests >= 0),
|
||||
latency_ms bigint NOT NULL DEFAULT 0 CHECK (latency_ms >= 0),
|
||||
estimated_cost text,
|
||||
manual_refresh boolean NOT NULL DEFAULT false,
|
||||
tehran_refresh_day date,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz
|
||||
);
|
||||
-- statement-break
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ai_insight_current_fingerprint_idx
|
||||
ON ai_insight_snapshots (merchant_id,coalesce(terminal_source_key,''),module,fingerprint)
|
||||
WHERE manual_refresh=false AND status IN ('QUEUED','GENERATING','READY','PARTIAL');
|
||||
-- statement-break
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ai_insight_manual_refresh_idx
|
||||
ON ai_insight_snapshots (merchant_id,coalesce(terminal_source_key,''),module,tehran_refresh_day)
|
||||
WHERE manual_refresh=true;
|
||||
-- statement-break
|
||||
CREATE INDEX IF NOT EXISTS ai_insight_latest_scope_idx
|
||||
ON ai_insight_snapshots (merchant_id,terminal_source_key,module,revision DESC,created_at DESC);
|
||||
-- statement-break
|
||||
CREATE TABLE IF NOT EXISTS ai_insight_jobs (
|
||||
id text PRIMARY KEY,
|
||||
snapshot_id text NOT NULL REFERENCES ai_insight_snapshots(id) ON DELETE CASCADE,
|
||||
merchant_id bigint NOT NULL REFERENCES merchants(id),
|
||||
module text NOT NULL,
|
||||
trigger_reason text NOT NULL CHECK (trigger_reason IN ('BACKFILL','NIGHTLY','IMPORT','CALENDAR','ANOMALY','MANUAL')),
|
||||
status text NOT NULL CHECK (status IN ('QUEUED','RUNNING','COMPLETED','FAILED')),
|
||||
idempotency_key text,
|
||||
request_hash text,
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
scheduled_at timestamptz NOT NULL DEFAULT now(),
|
||||
lease_token text,
|
||||
lease_expires_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz,
|
||||
UNIQUE (merchant_id,idempotency_key)
|
||||
);
|
||||
-- statement-break
|
||||
CREATE INDEX IF NOT EXISTS ai_insight_jobs_pending_idx
|
||||
ON ai_insight_jobs (scheduled_at,created_at) WHERE status IN ('QUEUED','RUNNING');
|
||||
-- statement-break
|
||||
CREATE TABLE IF NOT EXISTS ai_insight_events (
|
||||
id bigserial PRIMARY KEY,
|
||||
job_id text NOT NULL REFERENCES ai_insight_jobs(id) ON DELETE CASCADE,
|
||||
merchant_id bigint NOT NULL REFERENCES merchants(id),
|
||||
event_type text NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
occurred_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
-- statement-break
|
||||
CREATE INDEX IF NOT EXISTS ai_insight_events_replay_idx
|
||||
ON ai_insight_events (job_id,id) INCLUDE (occurred_at);
|
||||
-- statement-break
|
||||
INSERT INTO schema_migrations(version) VALUES (4) ON CONFLICT DO NOTHING;
|
||||
Reference in New Issue
Block a user