{"record":{"id":"1cc8769791af1e6b","repo":"antiwork/gumroad","slug":"replacement-card-errors-messages-base-first","errorCode":null,"errorMessage":"replacement_card.errors.messages[:base].first","messagePattern":"replacement_card\\.errors\\.messages\\[:base\\]\\.first","errorType":"exception","errorClass":"Subscription::UpdateFailed","httpStatus":null,"severity":"error","filePath":"app/services/subscription/updater_service.rb","lineNumber":125,"sourceCode":"        end\n\n        # Update card if necessary\n        unless use_existing_card?\n          had_saved_card = subscription.credit_card.present?\n\n          # (a) Get chargeable. Return if error\n          error_message = get_chargeable\n          if error_message.present?\n            logger.info(\"SubscriptionUpdater: Error fetching chargeable for subscription #{subscription.external_id}: #{error_message}\")\n            raise Subscription::UpdateFailed, error_message\n          end\n\n          # (b) Create new credit card. Return if error.\n          replacement_card = CreditCard.create(chargeable, card_data_handling_mode, logged_in_user)\n\n          unless replacement_card.errors.empty?\n            logger.info(\"SubscriptionUpdater: Error creating new credit card for subscription #{subscription.external_id}: #{replacement_card.errors.full_messages}\")\n            raise Subscription::UpdateFailed, replacement_card.errors.messages[:base].first\n          end\n\n          if indian_card_mandate_validation_required?(replacement_card)\n            # A plan update builds its replacement purchase before mandate validation. Keep the\n            # new card available for that build, but persist it only after validation succeeds.\n            subscription.credit_card = replacement_card\n          else\n            associate_replacement_card!(replacement_card, had_saved_card:, **validate_indian_card_mandate!(replacement_card))\n            replacement_card = nil\n          end\n        end\n\n        if !same_plan_and_price? || (is_resubscribing && discount_changed) || seller_price_changed\n          self.new_purchase = subscription.update_current_plan!(\n            new_variants: variants,\n            new_price: price,\n            new_quantity: params[:quantity],\n            perceived_price_cents: purchase_perceived_price_cents(original_discount),","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/subscription/updater_service.rb#L107-L143","documentation":"After a chargeable is built, UpdaterService calls CreditCard.create(chargeable, card_data_handling_mode, logged_in_user). CreditCard.create does not raise on failure — it returns a CreditCard whose errors are populated. When :base errors are present, the first base message is raised as Subscription::UpdateFailed and the whole update transaction rolls back. The base message is buyer-facing (typically instructing a different card).","triggerScenarios":"A new card is supplied on a membership update (use_existing_card? false) and the charge processor rejects saving the payment method: Stripe customer/card record creation fails, the instrument data in the chargeable fails CreditCard :base validations, or the token passed to card creation has already been consumed.","commonSituations":"Double-submit of the card form consuming the token on the first attempt, PayPal billing agreement not yet confirmed when card creation runs, charge processor outage mid-flow, or a chargeable built for a processor the merchant account no longer accepts.","solutions":["Read the logged line \"SubscriptionUpdater: Error creating new credit card\" — replacement_card.errors.full_messages names the exact base error(s).","Retry with a freshly entered card; token reuse after a double-submit is the most common cause.","If the base error looks like a processor/API failure rather than a card problem, verify charge processor health and credentials, then retry."],"exampleFix":"# before: assuming CreditCard.create always succeeds\ncard = CreditCard.create(chargeable, card_data_handling_mode, user)\nsubscription.credit_card = card\n\n# after: mirror the service's guard (updater_service.rb:123)\nraise Subscription::UpdateFailed, card.errors.messages[:base].first unless card.errors.empty?","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  result = Subscription::UpdaterService.new(subscription:, params:, logged_in_user:, gumroad_guid:, remote_ip:).perform\nrescue Subscription::UpdateFailed => e\n  result = { success: false, error_message: e.message } # keep the old card; prompt re-entry\nend\nrender_error(result[:error_message]) unless result[:success]","preventionTips":["Disable the submit button after the first click — double-submits consume the token so the second create fails.","Treat any :base error from CreditCard.create as buyer-actionable and surface it verbatim rather than retrying blindly.","Confirm the PayPal billing agreement is fully set up before offering it as a membership payment method."],"tags":["subscription","credit-card","payment-method","validation"],"backgroundTag":"payment-method-save-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}