Files
maskanx_cm_backend/tests/test_campaign_analytics.py
T
AFFAANhandClaude Opus 5 e0cec074ff fix(campaigns): persist the Meta linkage on insert
create_campaign never wrote meta_campaign_id, sync_status, sync_error or
approved_by. An adopted campaign arrives already linked to a Meta object,
and the link was dropped on the way into the database.

The consequences compounded. Every lookup keyed on meta_campaign_id
missed the row, so adopting a campaign twice created a second copy
instead of returning the first, and the reconciler — which imports any
Meta campaign it cannot find locally — imported the same campaign again
on every cycle. At the default 120s interval that is a new row every two
minutes, indefinitely.

The unit tests could not have caught this: their fake repository stores
the spec object itself, so no field can be lost between write and read.
It took a real database. The two regression tests added here run against
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 18:14:24 +05:30

122 lines
3.8 KiB
Python

# -*- coding: utf-8 -*-
"""Dashboard figures and campaign ranking.
The two things worth pinning down: costs are recomputed from totals rather
than averaged, and the windows end yesterday so partial figures for today
never make the numbers look like a collapse.
"""
from datetime import date
from adclaw.campaigns.analytics import dashboard, rank_campaigns, summarise
def test_costs_are_derived_from_the_totals():
summary = summarise({"spend": 60000, "clicks": 30, "leads": 4, "impressions": 3000})
assert summary["cost_per_lead"] == 15000
assert summary["cost_per_click"] == 2000
assert summary["ctr"] == 1.0
def test_costs_are_none_rather_than_zero_with_no_outcomes():
"""Zero would read as 'free', which is the opposite of the truth."""
summary = summarise({"spend": 60000, "clicks": 0, "leads": 0})
assert summary["cost_per_lead"] is None
assert summary["cost_per_click"] is None
def test_missing_metrics_are_treated_as_zero():
assert summarise({})["spend"] == 0
def test_ctr_is_safe_at_zero_impressions():
assert summarise({"clicks": 5, "impressions": 0})["ctr"] == 0.0
# --- ranking ---
def _row(campaign_id, spend, leads, name=None):
return {
"campaign_id": campaign_id,
"name": name or campaign_id,
"status": "live",
"spend": spend,
"leads": leads,
"clicks": 100,
"impressions": 10000,
}
def test_best_and_worst_rank_by_cost_per_lead_not_spend():
"""Efficiency is the question, not who spent the most."""
ranked = rank_campaigns([
_row("cheap", spend=10000, leads=10), # 1000 per lead
_row("costly", spend=90000, leads=3), # 30000 per lead
])
assert ranked["best"]["campaign_id"] == "cheap"
assert ranked["worst"]["campaign_id"] == "costly"
def test_campaigns_with_no_leads_are_excluded_from_ranking():
"""A campaign an hour old should not be called the worst performer."""
ranked = rank_campaigns([
_row("established", spend=10000, leads=10),
_row("brand new", spend=500, leads=0),
])
assert ranked["best"]["campaign_id"] == "established"
assert ranked["worst"] is None
def test_a_single_campaign_has_no_worst():
"""There is nothing to compare it against."""
ranked = rank_campaigns([_row("only", spend=10000, leads=10)])
assert ranked["best"]["campaign_id"] == "only"
assert ranked["worst"] is None
def test_no_campaigns_ranks_nothing():
assert rank_campaigns([]) == {"best": None, "worst": None}
# --- dashboard ---
class FakeInsightsRepo:
def __init__(self):
self.total_calls: list[tuple] = []
self.per_campaign_calls: list[tuple] = []
async def totals_between(self, since, until, campaign_id=None):
self.total_calls.append((since, until))
return {"spend": 10000, "clicks": 20, "leads": 2, "impressions": 1000}
async def per_campaign_totals(self, since, until):
self.per_campaign_calls.append((since, until))
return [_row("camp_1", spend=10000, leads=2)]
async def test_both_windows_end_yesterday_not_today():
"""Today is partial; including it makes every morning look like a crash."""
repo = FakeInsightsRepo()
result = await dashboard(repo, today=date(2026, 8, 3))
assert repo.total_calls[0] == (date(2026, 8, 2), date(2026, 8, 2))
assert repo.total_calls[1] == (date(2026, 7, 27), date(2026, 8, 2))
assert result["window"]["yesterday"] == "2026-08-02"
assert result["window"]["last_7_days_to"] == "2026-08-02"
async def test_the_dashboard_carries_totals_and_the_ranking():
result = await dashboard(FakeInsightsRepo(), today=date(2026, 8, 3))
assert result["yesterday"]["cost_per_lead"] == 5000
assert result["last_7_days"]["spend"] == 10000
assert result["best"]["campaign_id"] == "camp_1"
assert len(result["campaigns"]) == 1