feat(campaigns): add campaign management schema migration
This commit is contained in:
@@ -126,6 +126,130 @@ DROP TABLE IF EXISTS maskanx_social_posts;
|
||||
DROP TABLE IF EXISTS maskanx_campaigns;
|
||||
DROP TABLE IF EXISTS maskanx_cron_jobs;
|
||||
DROP TABLE IF EXISTS maskanx_chats;
|
||||
""",
|
||||
),
|
||||
Migration(
|
||||
version="202608010001",
|
||||
name="campaign_management_schema",
|
||||
up_sql="""
|
||||
ALTER TABLE maskanx_campaigns
|
||||
ADD COLUMN IF NOT EXISTS origin TEXT NOT NULL DEFAULT 'maskanx',
|
||||
ADD COLUMN IF NOT EXISTS ad_account_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS budget JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS guardrails JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS targeting JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS advanced JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS meta_campaign_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS sync_status TEXT NOT NULL DEFAULT 'not_synced',
|
||||
ADD COLUMN IF NOT EXISTS sync_error TEXT,
|
||||
ADD COLUMN IF NOT EXISTS last_synced_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS approved_by TEXT,
|
||||
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS launched_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS stopped_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS deleted_in_meta_at TIMESTAMPTZ;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_maskanx_campaigns_meta_id
|
||||
ON maskanx_campaigns (ad_account_id, meta_campaign_id)
|
||||
WHERE meta_campaign_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maskanx_campaign_adsets (
|
||||
id TEXT PRIMARY KEY,
|
||||
campaign_id TEXT NOT NULL
|
||||
REFERENCES maskanx_campaigns(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
targeting JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
budget JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
optimization_goal TEXT,
|
||||
billing_event TEXT,
|
||||
schedule JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta_adset_id TEXT,
|
||||
sync_status TEXT NOT NULL DEFAULT 'not_synced',
|
||||
sync_error TEXT,
|
||||
last_synced_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_maskanx_campaign_adsets_campaign
|
||||
ON maskanx_campaign_adsets (campaign_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maskanx_campaign_ads (
|
||||
id TEXT PRIMARY KEY,
|
||||
campaign_id TEXT NOT NULL
|
||||
REFERENCES maskanx_campaigns(id) ON DELETE CASCADE,
|
||||
adset_id TEXT
|
||||
REFERENCES maskanx_campaign_adsets(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
creative JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta_ad_id TEXT,
|
||||
meta_creative_id TEXT,
|
||||
sync_status TEXT NOT NULL DEFAULT 'not_synced',
|
||||
sync_error TEXT,
|
||||
last_synced_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_maskanx_campaign_ads_campaign
|
||||
ON maskanx_campaign_ads (campaign_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maskanx_campaign_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
campaign_id TEXT NOT NULL
|
||||
REFERENCES maskanx_campaigns(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
actor TEXT,
|
||||
reason TEXT,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_maskanx_campaign_events_campaign
|
||||
ON maskanx_campaign_events (campaign_id, occurred_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maskanx_campaign_insights (
|
||||
id TEXT PRIMARY KEY,
|
||||
level TEXT NOT NULL,
|
||||
object_id TEXT NOT NULL,
|
||||
campaign_id TEXT
|
||||
REFERENCES maskanx_campaigns(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
breakdown_key TEXT NOT NULL DEFAULT '',
|
||||
breakdown_value TEXT NOT NULL DEFAULT '',
|
||||
metrics JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_maskanx_campaign_insights
|
||||
ON maskanx_campaign_insights
|
||||
(level, object_id, date, breakdown_key, breakdown_value);
|
||||
""",
|
||||
down_sql="""
|
||||
DROP TABLE IF EXISTS maskanx_campaign_insights;
|
||||
DROP TABLE IF EXISTS maskanx_campaign_events;
|
||||
DROP TABLE IF EXISTS maskanx_campaign_ads;
|
||||
DROP TABLE IF EXISTS maskanx_campaign_adsets;
|
||||
DROP INDEX IF EXISTS uq_maskanx_campaigns_meta_id;
|
||||
|
||||
ALTER TABLE maskanx_campaigns
|
||||
DROP COLUMN IF EXISTS origin,
|
||||
DROP COLUMN IF EXISTS ad_account_id,
|
||||
DROP COLUMN IF EXISTS budget,
|
||||
DROP COLUMN IF EXISTS guardrails,
|
||||
DROP COLUMN IF EXISTS targeting,
|
||||
DROP COLUMN IF EXISTS advanced,
|
||||
DROP COLUMN IF EXISTS meta_campaign_id,
|
||||
DROP COLUMN IF EXISTS sync_status,
|
||||
DROP COLUMN IF EXISTS sync_error,
|
||||
DROP COLUMN IF EXISTS last_synced_at,
|
||||
DROP COLUMN IF EXISTS approved_by,
|
||||
DROP COLUMN IF EXISTS approved_at,
|
||||
DROP COLUMN IF EXISTS launched_at,
|
||||
DROP COLUMN IF EXISTS stopped_at,
|
||||
DROP COLUMN IF EXISTS deleted_in_meta_at;
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The campaign schema migration must be registered and reversible."""
|
||||
from adclaw.db.migrations import MIGRATIONS
|
||||
|
||||
VERSION = "202608010001"
|
||||
|
||||
|
||||
def _migration():
|
||||
return next((m for m in MIGRATIONS if m.version == VERSION), None)
|
||||
|
||||
|
||||
def test_campaign_migration_is_registered():
|
||||
assert _migration() is not None
|
||||
|
||||
|
||||
def test_campaign_migration_creates_expected_tables():
|
||||
up = _migration().up_sql
|
||||
for table in (
|
||||
"maskanx_campaign_adsets",
|
||||
"maskanx_campaign_ads",
|
||||
"maskanx_campaign_events",
|
||||
"maskanx_campaign_insights",
|
||||
):
|
||||
assert f"CREATE TABLE IF NOT EXISTS {table}" in up
|
||||
|
||||
|
||||
def test_campaign_migration_adds_columns_to_campaigns():
|
||||
up = _migration().up_sql
|
||||
for column in (
|
||||
"origin",
|
||||
"ad_account_id",
|
||||
"budget",
|
||||
"guardrails",
|
||||
"targeting",
|
||||
"advanced",
|
||||
"meta_campaign_id",
|
||||
"sync_status",
|
||||
"sync_error",
|
||||
"last_synced_at",
|
||||
"approved_by",
|
||||
"approved_at",
|
||||
"launched_at",
|
||||
"stopped_at",
|
||||
"deleted_in_meta_at",
|
||||
):
|
||||
assert f"ADD COLUMN IF NOT EXISTS {column}" in up
|
||||
|
||||
|
||||
def test_campaign_migration_down_drops_new_tables():
|
||||
down = _migration().down_sql
|
||||
for table in (
|
||||
"maskanx_campaign_insights",
|
||||
"maskanx_campaign_events",
|
||||
"maskanx_campaign_ads",
|
||||
"maskanx_campaign_adsets",
|
||||
):
|
||||
assert f"DROP TABLE IF EXISTS {table}" in down
|
||||
Reference in New Issue
Block a user