From 26f58b1d4a4ac1954a395ba226ba2829baaa7bf5 Mon Sep 17 00:00:00 2001 From: lalalune Date: Thu, 2 Jul 2026 15:03:31 -0700 Subject: [PATCH] =?UTF-8?q?fix(advertising):=20gate=20updateCampaign=20on?= =?UTF-8?q?=20account=20approval=20=E2=80=94=20close=20the=20suspended-acc?= =?UTF-8?q?ount=20spend=20leg=20(#11364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11516 gated createCampaign and startCampaign on account.status === "active" but left updateCampaign ungated: a suspended (or still-pending) account could PATCH a campaign budget increase, which deducts credits and pushes the change live to the ad platform — the exact spend the approval workflow exists to stop. Add the same fail-closed gate after the account lookup in updateCampaign (mirrors the createCampaign/startCampaign wording), and extend the #11364 suite with updateCampaign-blocked tests across pending/suspended/disconnected. Verification: ad-account-approval.test.ts 15 pass / 0 fail with the gate; reverting the source change alone fails exactly the 3 new tests (proves the tests pin the hole). cloud/shared tsgo --noEmit clean. Co-Authored-By: Claude Fable 5 --- .../lib/services/__tests__/ad-account-approval.test.ts | 9 +++++++++ .../cloud/shared/src/lib/services/advertising/index.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/packages/cloud/shared/src/lib/services/__tests__/ad-account-approval.test.ts b/packages/cloud/shared/src/lib/services/__tests__/ad-account-approval.test.ts index d018dce830b68..f9b5a991aa765 100644 --- a/packages/cloud/shared/src/lib/services/__tests__/ad-account-approval.test.ts +++ b/packages/cloud/shared/src/lib/services/__tests__/ad-account-approval.test.ts @@ -148,5 +148,14 @@ describe("campaign spend requires an approved (active) account (#11364)", () => /not active/, ); }); + + test(`updateCampaign is blocked when account is ${status} (no budget-increase spend)`, async () => { + track(spyOn(adCampaignsRepository, "findById").mockResolvedValue(makeCampaign())); + track(spyOn(adAccountsRepository, "findById").mockResolvedValue(makeAccount(status))); + + await expect( + advertisingService.updateCampaign(CAMPAIGN_ID, ORG_ID, { budgetAmount: 10_000 }), + ).rejects.toThrow(/not active/); + }); } }); diff --git a/packages/cloud/shared/src/lib/services/advertising/index.ts b/packages/cloud/shared/src/lib/services/advertising/index.ts index 7df5c05a1d784..7d6740054d9bb 100644 --- a/packages/cloud/shared/src/lib/services/advertising/index.ts +++ b/packages/cloud/shared/src/lib/services/advertising/index.ts @@ -686,6 +686,12 @@ class AdvertisingService { throw new Error("Ad account not found"); } + if (account.status !== "active") { + throw new Error( + `Ad account is not active (status: ${account.status}); it must be approved before running campaigns`, + ); + } + const credentials = await this.getCredentials(account); const provider = this.getProvider(account.platform);