{"record":{"id":"ae9966f6606f55f2","repo":"antiwork/gumroad","slug":"installment-plans-cannot-be-updated","errorCode":null,"errorMessage":"Installment plans cannot be updated.","messagePattern":"Installment plans cannot be updated\\.","errorType":"exception","errorClass":"Subscription::UpdateFailed","httpStatus":null,"severity":"error","filePath":"app/models/subscription.rb","lineNumber":1032,"sourceCode":"    end\n  end\n\n  def end_subscription!\n    with_lock do\n      return if ended_at.present?\n\n      self.ended_at = Time.current\n      self.deactivate!\n\n      CustomerLowPriorityMailer.subscription_ended(id).deliver_later(queue: \"low\")\n      ContactingCreatorMailer.subscription_ended(id).deliver_later(queue: \"critical\") if seller.enable_payment_email?\n    end\n  end\n\n  # creates a new original subscription purchase & archives the existing one.\n  # Any changes to the subscription made here must be reverted in `Subscription::UpdaterService#restore_original_purchase`\n  def update_current_plan!(new_variants:, new_price:, new_quantity: nil, perceived_price_cents: nil, is_applying_plan_change: false, skip_preparing_for_charge: false, offer_code: nil, clear_discount: false, clear_deleted_discount: false, authenticated_offer_code_buyer: AUTHENTICATED_OFFER_CODE_BUYER_NOT_PROVIDED, submitted_pre_discount_price_cents: nil, once_per_cart_discount_allocation: nil)\n    raise Subscription::UpdateFailed, \"Installment plans cannot be updated.\" if is_installment_plan?\n    raise Subscription::UpdateFailed, \"Changing plans for fixed-length subscriptions is not currently supported.\" if has_fixed_length?\n\n    ActiveRecord::Base.transaction do\n      payment_option = last_payment_option\n\n      # build new original subscription purchase\n      new_purchase = build_purchase(override_params: { is_original_subscription_purchase: true,\n                                                       email: original_purchase.email,\n                                                       is_free_trial_purchase: original_purchase.is_free_trial_purchase },\n                                    authenticated_offer_code_buyer:)\n      # avoid failing `Purchase#variants_available` validation if reverting back to the original set of variants & those variants are unavailable\n      new_purchase.original_variant_attributes = original_purchase.variant_attributes\n      # avoid failing `Purchase#price_not_too_low` validation if reverting back to the original subscription price & price has been deleted\n      new_purchase.original_price = price\n      # avoid `Purchase#not_double_charged` and sold out validations\n      new_purchase.is_updated_original_subscription_purchase = true\n      # avoid price validation failures when applying a pre-existing plan change (i.e. a downgrade)\n      new_purchase.is_applying_plan_change = is_applying_plan_change","sourceCodeStart":1014,"sourceCodeEnd":1050,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/subscription.rb#L1014-L1050","documentation":"Raised as Subscription::UpdateFailed (app/models/subscription.rb:10) at the top of Subscription#update_current_plan! when the subscription is an installment plan (is_installment_plan?). update_current_plan! works by creating a new original subscription purchase and archiving the old one — a migration that has no defined meaning for installment plans, so it is refused up front. Callers in the codebase (Subscription::UpdaterService, RecurringChargeWorker, ScheduleMembershipPriceUpdatesJob) rescue Subscription::UpdateFailed explicitly.","triggerScenarios":"Invoking update_current_plan! (the change-plan path used by Subscription::UpdaterService and price-update jobs) on a subscription where is_installment_plan? is true — e.g. a member's plan-change request hitting an installment-plan subscription.","commonSituations":"Product mixes installment-plan and normal memberships and the change-plan endpoint doesn't filter; scheduled membership price-update jobs sweeping all subscriptions including installment plans; UI offering 'change tier' on installment purchases.","solutions":["Filter plan-change flows on !subscription.is_installment_plan? before calling update_current_plan! — installment plans don't support mid-flight plan changes.","For installment-plan members who want a different plan, cancel and repurchase instead.","In jobs/services, rescue Subscription::UpdateFailed as the existing callers do and record/report it rather than letting it crash the worker."],"exampleFix":"# before\nsubscription.update_current_plan!(new_variants:, new_price:) # Subscription::UpdateFailed\n\n# after\nif subscription.is_installment_plan?\n  return { error: \"Installment plans cannot be updated.\" }\nend\nsubscription.update_current_plan!(new_variants:, new_price:)","handlingStrategy":"try-catch","validationCode":"return { unsupported: true } if subscription.is_installment_plan?","typeGuard":null,"tryCatchPattern":"begin\n  subscription.update_current_plan!(new_variants:, new_price:)\nrescue Subscription::UpdateFailed => e\n  # same pattern as Subscription::UpdaterService / RecurringChargeWorker\n  log_and_report(e) # installment plans cannot change plans mid-flight\nend","preventionTips":["Filter plan-change flows on !is_installment_plan? at the entry point.","Offer cancel-and-repurchase for installment-plan members instead.","Price-update jobs must skip installment plans or rescue UpdateFailed."],"tags":["subscriptions","installment-plans","plan-change","rails","updatefailed"],"backgroundTag":"unsupported-plan-change","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}