95 lines
3.8 KiB
Python
95 lines
3.8 KiB
Python
"""add crm_campaigns
|
|||
|
|
|
||
|
|
Mirrors advertising campaigns pushed from MaskanX so the CRM can report
|
||
|
|
spend and cost per lead alongside the leads those campaigns produced.
|
||
|
|
|
||
|
|
Money columns are integers in minor currency units (paise, cents), matching
|
||
|
|
what MaskanX sends and what Meta's budget fields use. A Numeric here would
|
||
|
|
add a second convention and a rounding step between two systems that
|
||
|
|
currently agree exactly.
|
||
|
|
|
||
|
|
Revision ID: b1c4d7e29a03
|
||
|
|
Revises: eca4681c9f66
|
||
|
|
Create Date: 2026-08-03
|
||
|
|
"""
|
||
|
|
from typing import Sequence, Union
|
||
|
|
|
||
|
|
from alembic import op
|
||
|
|
import sqlalchemy as sa
|
||
|
|
|
||
|
|
|
||
|
|
revision: str = 'b1c4d7e29a03'
|
||
|
|
down_revision: Union[str, None] = 'eca4681c9f66'
|
||
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
||
|
|
depends_on: Union[str, Sequence[str], None] = None
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.create_table(
|
||
|
|
'crm_campaigns',
|
||
|
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||
|
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||
|
|
sa.Column('provider', sa.String(length=80), nullable=False),
|
||
|
|
sa.Column('external_id', sa.String(length=255), nullable=False),
|
||
|
|
sa.Column('name', sa.String(length=240), nullable=False),
|
||
|
|
sa.Column('status', sa.String(length=40), nullable=False),
|
||
|
|
sa.Column('objective', sa.String(length=80), nullable=True),
|
||
|
|
sa.Column('channel', sa.String(length=80), nullable=True),
|
||
|
|
sa.Column('currency', sa.String(length=8), nullable=True),
|
||
|
|
sa.Column('daily_budget', sa.Integer(), nullable=True),
|
||
|
|
sa.Column('spend', sa.Integer(), nullable=False),
|
||
|
|
sa.Column('impressions', sa.Integer(), nullable=False),
|
||
|
|
sa.Column('clicks', sa.Integer(), nullable=False),
|
||
|
|
sa.Column('leads', sa.Integer(), nullable=False),
|
||
|
|
sa.Column('cost_per_lead', sa.Integer(), nullable=True),
|
||
|
|
sa.Column('metrics_from', sa.String(length=20), nullable=True),
|
||
|
|
sa.Column('metrics_to', sa.String(length=20), nullable=True),
|
||
|
|
sa.Column('synced_at', sa.DateTime(timezone=True), nullable=False),
|
||
|
|
sa.Column('metadata_json', sa.JSON(), nullable=False),
|
||
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||
|
|
sa.ForeignKeyConstraint(['tenant_id'], ['crm_tenants.id'], ondelete='CASCADE'),
|
||
|
|
sa.PrimaryKeyConstraint('id'),
|
||
|
|
# MaskanX re-pushes the same campaign as its figures change, so the
|
||
|
|
# provider's own id is what identifies it, not our surrogate key.
|
||
|
|
sa.UniqueConstraint(
|
||
|
|
'tenant_id', 'provider', 'external_id',
|
||
|
|
name='uq_crm_campaigns_provider_external',
|
||
|
|
),
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
'ix_crm_campaigns_tenant_status',
|
||
|
|
'crm_campaigns',
|
||
|
|
['tenant_id', 'status'],
|
||
|
|
)
|
||
|
|
|
||
|
|
# Ad attribution promoted out of crm_leads.attributes into real columns,
|
||
|
|
# so counting leads per campaign is an indexed join rather than a JSON
|
||
|
|
# scan. Nullable: most leads do not come from an ad.
|
||
|
|
op.add_column(
|
||
|
|
'crm_leads',
|
||
|
|
sa.Column('campaign_external_id', sa.String(length=255), nullable=True),
|
||
|
|
)
|
||
|
|
op.add_column(
|
||
|
|
'crm_leads',
|
||
|
|
sa.Column('adset_external_id', sa.String(length=255), nullable=True),
|
||
|
|
)
|
||
|
|
op.add_column(
|
||
|
|
'crm_leads',
|
||
|
|
sa.Column('ad_external_id', sa.String(length=255), nullable=True),
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
'ix_crm_leads_campaign_external_id',
|
||
|
|
'crm_leads',
|
||
|
|
['campaign_external_id'],
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
op.drop_index('ix_crm_leads_campaign_external_id', table_name='crm_leads')
|
||
|
|
op.drop_column('crm_leads', 'ad_external_id')
|
||
|
|
op.drop_column('crm_leads', 'adset_external_id')
|
||
|
|
op.drop_column('crm_leads', 'campaign_external_id')
|
||
|
|
op.drop_index('ix_crm_campaigns_tenant_status', table_name='crm_campaigns')
|
||
|
|
op.drop_table('crm_campaigns')
|