{"record":{"id":"b0bbd03f8b8427d6","repo":"medusajs/medusa","slug":"cannot-add-promotions-diff-join-to-campa","errorCode":null,"errorMessage":"Cannot add promotions (${diff.join(\",\")}) to campaign. These promotions are either already part of a campaign or not found.","messagePattern":"Cannot add promotions \\((.+?)\\) to campaign\\. These promotions are either already part of a campaign or not found\\.","errorType":"http","errorClass":"MedusaError","httpStatus":404,"severity":"error","filePath":"packages/modules/promotion/src/services/promotion-module.ts","lineNumber":2029,"sourceCode":"    data: PromotionTypes.AddPromotionsToCampaignDTO,\n    @MedusaContext() sharedContext: Context = {}\n  ) {\n    const { id, promotion_ids: promotionIds = [] } = data\n\n    const campaign = await this.campaignService_.retrieve(id, {}, sharedContext)\n    const promotionsToAdd = await this.promotionService_.list(\n      { id: promotionIds, campaign_id: null },\n      { relations: [\"application_method\"] },\n      sharedContext\n    )\n\n    const diff = arrayDifference(\n      promotionsToAdd.map((p) => p.id),\n      promotionIds\n    )\n\n    if (diff.length > 0) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_FOUND,\n        `Cannot add promotions (${diff.join(\n          \",\"\n        )}) to campaign. These promotions are either already part of a campaign or not found.`\n      )\n    }\n\n    const promotionsWithInvalidCurrency = promotionsToAdd.filter(\n      (promotion) =>\n        campaign.budget?.type === CampaignBudgetType.SPEND &&\n        promotion.application_method?.currency_code !==\n          campaign?.budget?.currency_code\n    )\n\n    if (promotionsWithInvalidCurrency.length > 0) {\n      throw new MedusaError(\n        MedusaError.Types.INVALID_DATA,\n        `Cannot add promotions to campaign where currency_code don't match.`","sourceCodeStart":2011,"sourceCodeEnd":2047,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/promotion/src/services/promotion-module.ts#L2011-L2047","documentation":"Thrown by PromotionModuleService when adding promotions to a campaign: the ids supplied in promotionsToAdd contain at least one id that is not present in the promotionIds resolved from the module (i.e., the promotion does not exist or is not eligible). The diff between requested ids and found ids is non-empty, so the entire operation is rejected with NOT_FOUND.","triggerScenarios":"Calling addPromotionsToCampaign (or the admin POST /admin/campaigns/:id/promotions route) with promotion ids that were deleted, mis-typed, belong to another scope, or are already assigned to a different campaign so they are filtered out of promotionIds.","commonSituations":"Stale promotion ids cached in the client/UI, copy-pasting ids between environments (dev/staging/prod), re-adding promotions that were already attached to another campaign, race where the promotion was deleted after the UI loaded the list.","solutions":["Verify each promotion id exists via promotionModule.listPromotions({ id: [...] }) before calling addPromotionsToCampaign","Detach the promotion from its existing campaign first (removePromotionsFromCampaign on the old campaign) if reassignment is intended","Remove non-existent ids from the payload and retry with only valid ids","Check for environment mismatch — ids generated in one database will not exist in another"],"exampleFix":"// before\nawait campaignModule.addPromotionsToCampaign(campaignId, {\n  promo_ids: [\"promo_123\", \"promo_deleted\"],\n})\n\n// after\nconst existing = await promotionModule.listPromotions({ id: promoIds })\nconst validIds = existing.map((p) => p.id)\nawait campaignModule.addPromotionsToCampaign(campaignId, {\n  promo_ids: validIds,\n})","handlingStrategy":"validation","validationCode":"const existing = await promotionModule.listPromotions({ id: promoIds })\nconst existingIds = new Set(existing.map((p) => p.id))\nconst validIds = promoIds.filter((id) => existingIds.has(id))\nif (validIds.length !== promoIds.length) {\n  // skip/report unknown ids instead of failing the whole call\n}","typeGuard":"const isValidPromotionId = (id: string, existing: Set<string>): boolean => existing.has(id)","tryCatchPattern":"try { await addPromotionsToCampaign(...) } catch (e) { if (e instanceof MedusaError && e.type === MedusaError.Types.NOT_FOUND) { /* refresh promo list and retry with valid ids */ } throw e }","preventionTips":["Fetch fresh promotion ids right before attaching to a campaign","Never persist promotion ids across environments","Detach from the previous campaign before reassigning"],"tags":["promotion","campaign","not-found","medusa"],"backgroundTag":"referenced-entity-not-found","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}