{"record":{"id":"d52c9d76581cf07b","repo":"medusajs/medusa","slug":"promotions-with-ids-diff-join-not-found","errorCode":null,"errorMessage":"Promotions with ids (${diff.join(\",\")}) not found.","messagePattern":"Promotions with ids \\((.+?)\\) not found\\.","errorType":"http","errorClass":"MedusaError","httpStatus":404,"severity":"error","filePath":"packages/modules/promotion/src/services/promotion-module.ts","lineNumber":2093,"sourceCode":"    data: PromotionTypes.AddPromotionsToCampaignDTO,\n    @MedusaContext() sharedContext: Context = {}\n  ) {\n    const { id, promotion_ids: promotionIds = [] } = data\n\n    await this.campaignService_.retrieve(id, {}, sharedContext)\n    const promotionsToRemove = await this.promotionService_.list(\n      { id: promotionIds },\n      {},\n      sharedContext\n    )\n\n    const diff = arrayDifference(\n      promotionsToRemove.map((p) => p.id),\n      promotionIds\n    )\n\n    if (diff.length > 0) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_FOUND,\n        `Promotions with ids (${diff.join(\",\")}) not found.`\n      )\n    }\n\n    await this.promotionService_.update(\n      promotionsToRemove.map((promotion) => ({\n        id: promotion.id,\n        campaign_id: null,\n      })),\n      sharedContext\n    )\n\n    return promotionsToRemove.map((promo) => promo.id)\n  }\n}\n","sourceCodeStart":2075,"sourceCodeEnd":2110,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/promotion/src/services/promotion-module.ts#L2075-L2110","documentation":"Thrown by removePromotionsFromCampaign: some promotion ids supplied in promotionsToRemove do not exist in the resolved promotionIds set (deleted, never attached to this campaign, or invalid). The arrayDifference is non-empty so the removal request is rejected with NOT_FOUND.","triggerScenarios":"Calling removePromotionsFromCampaign with ids that are not currently linked to the campaign, were already removed (double-submit), were hard-deleted, or are typos.","commonSituations":"Double-clicking a remove button firing two requests, UI state desync after another admin removed the promotion, deleted promotions lingering in a client-side list.","solutions":["Re-fetch the campaign's promotions (campaign promotion list) and intersect with the ids you intend to remove","Make removal idempotent client-side: ignore/remove already-absent ids before calling","Guard against double submits in the UI"],"exampleFix":"// before\nawait campaignModule.removePromotionsFromCampaign(campaignId, {\n  promo_ids: [\"promo_already_removed\"],\n})\n\n// after\nconst current = await promotionModule.listPromotions({ campaign_id: campaignId })\nconst toRemove = requestedIds.filter((id) => current.some((p) => p.id === id))\nif (toRemove.length) {\n  await campaignModule.removePromotionsFromCampaign(campaignId, { promo_ids: toRemove })\n}","handlingStrategy":"validation","validationCode":"const attached = await promotionModule.listPromotions({ campaign_id: campaignId })\nconst attachedIds = new Set(attached.map((p) => p.id))\nconst toRemove = requestedIds.filter((id) => attachedIds.has(id))","typeGuard":"const isAttachedToCampaign = (id: string, attachedIds: Set<string>): boolean => attachedIds.has(id)","tryCatchPattern":"try { await removePromotionsFromCampaign(...) } catch (e) { if (e.type === MedusaError.Types.NOT_FOUND) { /* refetch campaign promotions, filter, retry */ } throw e }","preventionTips":["Make removal idempotent client-side","Disable the remove button while a request is in flight","Refresh campaign promotions after mutations"],"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"}