feat(campaigns): add PostgreSQL campaign repository

Adds CampaignRepository with list/get/create/update/delete for
campaigns plus add_event/list_events for the audit trail, following
the existing postgres_repo.py connection pattern. Replaces the
brief's two bare `assert created/updated is not None` checks with
explicit RuntimeError raises so the guard survives python -O.
This commit is contained in:
AFFAANh
2026-08-01 13:27:08 +05:30
parent 985527a451
commit 1df49aee37
2 changed files with 289 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""Row mapping for the campaign repository."""
from datetime import datetime, timezone
from adclaw.campaigns.repo import campaign_from_row
def _row(**overrides):
row = {
"id": "camp_1",
"company_id": "maskanx",
"name": "Lead gen",
"status": "draft",
"origin": "maskanx",
"objective": "OUTCOME_LEADS",
"ad_account_id": "act_1",
"budget": {"daily_budget": 10000},
"guardrails": {"max_cost_per_lead": 150},
"targeting": {"age_min": 25},
"advanced": {},
"channels": ["facebook"],
"schedule": {},
"meta_campaign_id": None,
"sync_status": "not_synced",
"sync_error": None,
"approved_by": None,
"created_at": datetime(2026, 8, 1, tzinfo=timezone.utc),
"updated_at": datetime(2026, 8, 1, tzinfo=timezone.utc),
}
row.update(overrides)
return row
def test_campaign_from_row_maps_all_fields():
spec = campaign_from_row(_row())
assert spec.id == "camp_1"
assert spec.budget["daily_budget"] == 10000
assert spec.guardrails["max_cost_per_lead"] == 150
assert spec.channels == ["facebook"]
assert spec.sync_status == "not_synced"
def test_campaign_from_row_defaults_null_json_to_empty():
spec = campaign_from_row(
_row(budget=None, guardrails=None, targeting=None, advanced=None,
channels=None, schedule=None),
)
assert spec.budget == {}
assert spec.guardrails == {}
assert spec.targeting == {}
assert spec.advanced == {}
assert spec.channels == []
assert spec.schedule == {}