Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/issue-evidence/11596-ad-bid-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Issue #11596 — Advertising Bid Strategy Controls

## Implementation

- Added `bidStrategy` (`cpm`/`cpc`/`cpa`) and `optimizationGoal` (`reach`/`clicks`/`conversions`) to campaign create/update schemas and service types.
- Persisted bid controls in `ad_campaigns.metadata` and returned them from campaign list/detail/create API responses.
- Threaded bid controls through app promotion config and the promotion dialog.
- Mapped controls into Meta ad set and Google campaign create payloads.
- Explicitly rejects TikTok campaign-level bid controls because this adapter only has bid settings on the ad-group path today.
- `updateCampaign` rejects bid-control changes fail-closed (before any credit movement or platform call): no adapter applies bid changes to a live campaign, so persisting them locally would be silent local/platform drift.

## Verification

- `bun test packages/cloud/shared/src/lib/services/__tests__/ad-campaign-bid-strategy.test.ts`
- 9 tests passed.
- `bunx biome check packages/cloud/shared/src/lib/services/__tests__/ad-campaign-bid-strategy.test.ts packages/cloud/shared/src/lib/services/app-promotion.ts packages/ui/src/cloud-ui/components/promotion/promote-app-dialog.tsx packages/cloud/shared/src/lib/services/advertising/providers/meta.ts packages/cloud/shared/src/lib/services/advertising/providers/google.ts packages/cloud/shared/src/lib/services/advertising/providers/tiktok.ts packages/cloud/shared/src/lib/services/advertising/index.ts packages/cloud/shared/src/lib/services/advertising/types.ts packages/cloud/shared/src/lib/services/advertising/schemas.ts packages/cloud/shared/src/db/schemas/ad-campaigns.ts packages/cloud/api/v1/advertising/campaigns/route.ts packages/cloud/api/v1/advertising/campaigns/[id]/route.ts`
- Passed.
- `bun run --cwd packages/cloud/shared typecheck`
- Passed.
- `bun run --cwd packages/cloud/api typecheck`
- Passed.
- `bun run --cwd packages/ui typecheck`
- Passed.
- `bun run --cwd packages/app audit:app`
- 348 captures passed.
- Touched `/apps` route verdicts were `good` for mobile portrait, mobile landscape, desktop landscape, and iPad portrait.
- Command exited 1 on unrelated existing minimalism ratchet failures:
- `plugin-inbox-gui @ mobile-landscape`
- `plugin-screenshare-gui @ mobile-portrait`

## Evidence Gaps

- No live ad-platform campaign was created. Provider behavior is covered with deterministic payload mapping tests; live external ad account spend remains the missing end-to-end evidence.
4 changes: 4 additions & 0 deletions packages/cloud/api/v1/advertising/campaigns/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ app.get("/", async (c) => {
budgetType: campaign.budget_type,
budgetAmount: campaign.budget_amount,
budgetCurrency: campaign.budget_currency,
bidStrategy: campaign.metadata.bid_strategy,
optimizationGoal: campaign.metadata.optimization_goal,
creditsAllocated: campaign.credits_allocated,
creditsSpent: campaign.credits_spent,
startDate: campaign.start_date?.toISOString(),
Expand Down Expand Up @@ -77,6 +79,8 @@ app.patch("/", async (c) => {
{
name: parsed.data.name,
budgetAmount: parsed.data.budgetAmount,
bidStrategy: parsed.data.bidStrategy,
optimizationGoal: parsed.data.optimizationGoal,
startDate: parsed.data.startDate
? new Date(parsed.data.startDate)
: undefined,
Expand Down
6 changes: 6 additions & 0 deletions packages/cloud/api/v1/advertising/campaigns/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ app.get("/", async (c) => {
budgetType: c.budget_type,
budgetAmount: c.budget_amount,
budgetCurrency: c.budget_currency,
bidStrategy: c.metadata.bid_strategy,
optimizationGoal: c.metadata.optimization_goal,
creditsAllocated: c.credits_allocated,
creditsSpent: c.credits_spent,
startDate: c.start_date?.toISOString(),
Expand Down Expand Up @@ -85,6 +87,8 @@ app.post("/", async (c) => {
budgetType: parsed.data.budgetType,
budgetAmount: parsed.data.budgetAmount,
budgetCurrency: parsed.data.budgetCurrency,
bidStrategy: parsed.data.bidStrategy,
optimizationGoal: parsed.data.optimizationGoal,
startDate: parsed.data.startDate
? new Date(parsed.data.startDate)
: undefined,
Expand All @@ -108,6 +112,8 @@ app.post("/", async (c) => {
status: campaign.status,
budgetType: campaign.budget_type,
budgetAmount: campaign.budget_amount,
bidStrategy: campaign.metadata.bid_strategy,
optimizationGoal: campaign.metadata.optimization_goal,
creditsAllocated: campaign.credits_allocated,
dayparting: campaign.metadata.dayparting ?? null,
createdAt: campaign.created_at.toISOString(),
Expand Down
14 changes: 12 additions & 2 deletions packages/cloud/shared/src/db/schemas/ad-campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export type CampaignObjective =
*/
export type BudgetType = "daily" | "lifetime";

/**
* Campaign bid strategy selected by the user.
*/
export type CampaignBidStrategy = "cpm" | "cpc" | "cpa";

/**
* Optimization goal selected by the user.
*/
export type CampaignOptimizationGoal = "reach" | "clicks" | "conversions";

/**
* Ad campaigns table schema.
*
Expand Down Expand Up @@ -116,8 +126,8 @@ export const adCampaigns = pgTable(
.$type<{
external_ad_set_ids?: string[];
external_ad_ids?: string[];
optimization_goal?: string;
bid_strategy?: string;
optimization_goal?: CampaignOptimizationGoal;
bid_strategy?: CampaignBidStrategy;
dayparting?: {
timezone: string;
windows: Array<{
Expand Down
Loading
Loading