Files
AFFAANhandClaude Opus 5 55d5af849b feat(campaigns): report advertising spend and cost per lead
MaskanX runs the campaigns; this stores what they cost and what they
produced so the question "what did this campaign cost us per lead" is
answerable next to the leads themselves.

POST /integrations/campaigns is an upsert keyed on (provider,
external_id), not an idempotent create like /leads. A lead is an event
that happened once; a campaign's figures change every time they are read,
and MaskanX re-pushes the same campaign as its spend grows. An
Idempotency-Key here would pin the CRM to the first numbers it ever saw.

Money is stored as integers in minor currency units, matching what
MaskanX sends and what Meta uses. A Numeric would add a second convention
and a rounding step between systems that currently agree exactly.

Ad attribution is promoted out of crm_leads.attributes into indexed
columns, so counting leads per campaign is a join rather than a JSON scan
— which also keeps it working on both SQLite and PostgreSQL.

Meta's lead count and the CRM's own are both kept. They routinely differ,
since Meta attributes late and leads can be entered by hand, and the gap
is worth seeing rather than hiding behind one number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:55:46 +05:30

193 lines
6.2 KiB
Python

"""Advertising campaigns mirrored from MaskanX.
The CRM does not run campaigns; MaskanX pushes their figures here so that
spend and cost per lead can be read next to the leads they produced.
The behaviour that matters is that pushing the same campaign again updates
it rather than duplicating it — MaskanX re-pushes every few minutes as
spend grows — and that lead attribution actually links the two.
"""
from fastapi.testclient import TestClient
import pytest
@pytest.fixture()
def service_key(client: TestClient, auth_headers: dict[str, str]) -> str:
created = client.post(
"/api/v1/integrations/credentials",
headers=auth_headers,
json={"name": "MaskanX"},
)
assert created.status_code == 201, created.text
return created.json()["key"]
def _campaign(**overrides) -> dict:
payload = {
"provider": "maskanx",
"external_id": "camp_1",
"name": "Q3 lead gen",
"status": "live",
"objective": "OUTCOME_LEADS",
"channel": "facebook",
"currency": "INR",
"daily_budget": 50000,
"spend": 46000,
"impressions": 12000,
"clicks": 300,
"leads": 4,
"cost_per_lead": 11500,
"metrics_from": "2026-07-05",
"metrics_to": "2026-08-03",
}
payload.update(overrides)
return payload
def _push(client, service_key, **overrides):
return client.post(
"/api/v1/integrations/campaigns",
headers={"X-Integration-Key": service_key},
json=_campaign(**overrides),
)
def test_a_campaign_can_be_pushed_and_read_back(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
pushed = _push(client, service_key)
assert pushed.status_code == 200, pushed.text
assert pushed.json()["created"] is True
listed = client.get("/api/v1/campaigns", headers=auth_headers)
assert listed.status_code == 200, listed.text
body = listed.json()
assert len(body) == 1
assert body[0]["name"] == "Q3 lead gen"
assert body[0]["spend"] == 46000
assert body[0]["cost_per_lead"] == 11500
def test_pushing_the_same_campaign_again_updates_it(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
"""MaskanX re-pushes every few minutes as spend grows."""
_push(client, service_key, spend=46000)
again = _push(client, service_key, spend=90000, leads=9, cost_per_lead=10000)
assert again.json()["created"] is False
body = client.get("/api/v1/campaigns", headers=auth_headers).json()
assert len(body) == 1
assert body[0]["spend"] == 90000
assert body[0]["leads"] == 9
def test_two_campaigns_are_kept_apart(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
_push(client, service_key, external_id="camp_1", name="First")
_push(client, service_key, external_id="camp_2", name="Second")
body = client.get("/api/v1/campaigns", headers=auth_headers).json()
assert {row["name"] for row in body} == {"First", "Second"}
def test_campaigns_are_ordered_by_spend(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
_push(client, service_key, external_id="small", name="Small", spend=1000)
_push(client, service_key, external_id="big", name="Big", spend=99000)
body = client.get("/api/v1/campaigns", headers=auth_headers).json()
assert [row["name"] for row in body] == ["Big", "Small"]
def test_pushing_a_campaign_needs_an_integration_key(client: TestClient) -> None:
response = client.post("/api/v1/integrations/campaigns", json=_campaign())
assert response.status_code == 401
def test_reading_campaigns_needs_a_logged_in_user(client: TestClient) -> None:
assert client.get("/api/v1/campaigns").status_code == 401
def test_negative_spend_is_rejected(client: TestClient, service_key: str) -> None:
"""Money that went backwards is a bug upstream, not a figure to store."""
assert _push(client, service_key, spend=-1).status_code == 422
# --- attribution ---
def _lead(client, service_key, external_id, campaign_id):
return client.post(
"/api/v1/integrations/leads",
headers={
"X-Integration-Key": service_key,
"Idempotency-Key": f"key-{external_id}",
},
json={
"provider": "maskanx",
"external_id": external_id,
"first_name": "Asha",
"last_name": "Menon",
"email": f"{external_id}@example.com",
"campaign": {
"campaign_id": campaign_id,
"adset_id": "meta_set_1",
"ad_id": "meta_ad_1",
},
},
)
def test_leads_are_counted_against_the_campaign_that_produced_them(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
_push(client, service_key, external_id="camp_1", leads=9)
assert _lead(client, service_key, "lead_1", "camp_1").status_code == 200
assert _lead(client, service_key, "lead_2", "camp_1").status_code == 200
body = client.get("/api/v1/campaigns", headers=auth_headers).json()
# Meta's count and the CRM's own count are kept separately: they
# routinely differ, and the gap is worth seeing.
assert body[0]["leads"] == 9
assert body[0]["crm_leads"] == 2
def test_a_lead_from_another_campaign_is_not_counted(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
_push(client, service_key, external_id="camp_1")
_lead(client, service_key, "lead_1", "camp_2")
body = client.get("/api/v1/campaigns", headers=auth_headers).json()
assert body[0]["crm_leads"] == 0
def test_a_lead_with_no_campaign_does_not_break_the_count(
client: TestClient, auth_headers: dict, service_key: str,
) -> None:
"""Most leads do not come from an ad."""
_push(client, service_key, external_id="camp_1")
response = client.post(
"/api/v1/integrations/leads",
headers={"X-Integration-Key": service_key, "Idempotency-Key": "manual-1"},
json={
"provider": "maskanx",
"external_id": "walk_in_1",
"first_name": "Walk",
"last_name": "In",
},
)
assert response.status_code == 200
body = client.get("/api/v1/campaigns", headers=auth_headers).json()
assert body[0]["crm_leads"] == 0