Files
maskanx_crm_backend/alembic/versions/b1c4d7e29a03_add_crm_campaigns.py
T

95 lines
3.8 KiB
Python
Raw Normal View History

"""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')