Campaign records, approval workflow, budget guardrails against the ad
account minimum, and Meta ad preview. Creates nothing in Meta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records what Phase 1 does and deliberately does not do: no Meta writes, no
launch or sync endpoint, previews rendered without creating objects, budget
minimums enforced locally, and the fact that a payment method can only be
attached in Meta Business Manager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PUT /campaigns/{id} validated the raw request payload instead of the
merged campaign state, so omitting ad_account_id skipped the minimum
check and a full model_dump() overwrote every unset field (targeting,
guardrails, objective, etc.) with its default, silently destroying
data. Validate against the effective post-merge budget/guardrails/
account and only apply fields the client actually sent
(exclude_unset=True).
Also: persist company_id on UPDATE (was accepted but dropped), log
a warning when Meta account lookups fail so the budget-minimum
fail-open is observable, reword the non-editable-status 409 message
to not prescribe an impossible reject-to-draft action, and add
coverage for Meta error surfacing, LookupError-to-404 on PUT/submit,
reject's pending_approval->draft transition, and GET-unknown-id 404.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_campaign/update_campaign previously committed then opened a
second connection via get_campaign() just to re-read the row they had
just written, adding an extra round-trip and a race window where a
concurrent delete between commit and re-fetch was misreported as
"disappeared immediately after insert/update". Both now RETURNING the
row from the same statement/cursor that wrote it, using a shared
_RETURNING_CLAUSE derived from _SELECT_COLUMNS so the two column lists
cannot drift apart. update_campaign now raises LookupError for an
unknown id (0 rows affected) instead of misdiagnosing it as a race.
Also: delete_campaign uses the (cur.rowcount or 0) > 0 idiom already
established in postgres_repo.py, the module docstring documents that
this repository's SQL paths rely on integration tests rather than
unit tests, and a new DB-free test asserts the RETURNING and SELECT
column lists stay identical.
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.
Address code review findings on the read-only Meta Graph client: add
monkeypatch-based tests for access_token_from_env/MetaNotConfiguredError,
assert generate_previews JSON-encodes the creative param, pin the
existing fail-fast-on-partial-error contract with a dedicated test, and
declare httpx as an explicit dependency since it was only resolving
transitively.
validate_guardrails used a truthy check on daily_budget, which silently
skipped the auto-pause-above-daily-budget check when daily_budget was 0 -
exactly the case where a positive auto-pause threshold can never trigger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Independent FastAPI backend for the MaskanX agentic growth platform.
Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn,
HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage
for chats and cron jobs, provider and secret management, and the CLI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>