{"record":{"id":"72259d96c1c5f240","repo":"overleaf/overleaf","slug":"subscription-already-has-add-on","errorCode":null,"errorMessage":"Subscription already has add-on","messagePattern":"Subscription already has add-on","errorType":"exception","errorClass":"DuplicateAddOnError","httpStatus":null,"severity":"error","filePath":"services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs","lineNumber":219,"sourceCode":"    })\n\n    return changeRequest\n  }\n\n  /**\n   * Purchase an add-on on this subscription\n   *\n   * @param {string} code\n   * @param {number} [quantity]\n   * @param {number} [unitPrice]\n   * @return {PaymentProviderSubscriptionChangeRequest} - the change request to send to\n   * Recurly\n   *\n   * @throws {DuplicateAddOnError} if the add-on is already present on the subscription\n   */\n  getRequestForAddOnPurchase(code, quantity = 1, unitPrice) {\n    if (this.hasAddOn(code)) {\n      throw new DuplicateAddOnError('Subscription already has add-on', {\n        subscriptionId: this.id,\n        addOnCode: code,\n      })\n    }\n\n    const baseAddOns = this.#getBaseAddOnsForChangeRequest('now')\n    const addOnUpdates = baseAddOns.map(addOn => addOn.toAddOnUpdate())\n    addOnUpdates.push(\n      new PaymentProviderSubscriptionAddOnUpdate({ code, quantity, unitPrice })\n    )\n    return new PaymentProviderSubscriptionChangeRequest({\n      subscription: this,\n      timeframe: 'now',\n      addOnUpdates,\n    })\n  }\n\n  /**","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs#L201-L237","documentation":"PaymentProviderEntities.getRequestForAddOnPurchase builds a Recurly subscription-change request to purchase an add-on. Before building it, it checks this.hasAddOn(code); if the add-on already exists on the subscription, a DuplicateAddOnError is thrown with the subscriptionId and addOnCode, because Recurly cannot add an add-on that is already present.","triggerScenarios":"Calling getRequestForAddOnPurchase(code) for an add-on code already attached to the subscription (or included in the pending change's add-on set), e.g. re-submitting a purchase request or attempting to add 'additional-license' when the user already has it.","commonSituations":"Double-submission of an upgrade UI form; logic that adds seats via the add-on path when the add-on already exists; syncing state where a prior pending change already added the add-on; code paths that don't check current entitlements before upgrading.","solutions":["Check this.hasAddOn(code) (or the equivalent entitlement) before calling getRequestForAddOnPurchase","If the add-on exists, use getRequestForAddOnUpdate to change its quantity instead of purchasing","Catch DuplicateAddOnError and treat it as a no-op / idempotent success","Re-fetch the subscription entity before building the change request so pending changes are reflected"],"exampleFix":"// before\nconst request = subscription.getRequestForAddOnPurchase('additional-license', 1)\n// after\nconst request = subscription.hasAddOn('additional-license')\n  ? subscription.getRequestForAddOnUpdate('additional-license', newQuantity)\n  : subscription.getRequestForAddOnPurchase('additional-license', 1)","handlingStrategy":"try-catch","validationCode":"if (subscription.hasAddOn('additional-license')) {\n  // use update path instead of purchase\n}","typeGuard":"function canPurchaseAddOn(subscription, code) {\n  return typeof code === 'string' && !subscription.hasAddOn(code)\n}","tryCatchPattern":"try {\n  const request = subscription.getRequestForAddOnPurchase(code, qty)\n} catch (err) {\n  if (err instanceof DuplicateAddOnError) {\n    return { status: 'already-owned', subscriptionId: err.info.subscriptionId }\n  }\n  throw err\n}","preventionTips":["Always check hasAddOn(code) before purchase requests","Make purchase flows idempotent — treat duplicate as success","Refresh the subscription entity before building change requests","Use the update path for quantity changes on existing add-ons"],"tags":["subscription","recurly","add-on","duplicate-request","idempotency"],"backgroundTag":"duplicate-add-on","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}