From 9abacf9047208ffe7f0b0518b189cc37db45f040 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 20 Jan 2022 17:04:26 +1100 Subject: [PATCH] Upgrade old StripeConnect payment methods to SCA StripeConnect has been replaced by StripeSCA. But we still have some StripeConnect payment methods in our production databases. We need to convert them to StripeSCA methods before we can remove the related code and clean up. Otherwise our code would raise errors when encountering an old StripeConnect method, not knowing what that is. --- ...07_convert_stripe_connect_to_stripe_sca.rb | 57 +++++++++++ db/schema.rb | 2 +- ...nvert_stripe_connect_to_stripe_sca_spec.rb | 95 +++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20220118053107_convert_stripe_connect_to_stripe_sca.rb create mode 100644 spec/migrations/convert_stripe_connect_to_stripe_sca_spec.rb diff --git a/db/migrate/20220118053107_convert_stripe_connect_to_stripe_sca.rb b/db/migrate/20220118053107_convert_stripe_connect_to_stripe_sca.rb new file mode 100644 index 0000000000..5d57da7e9f --- /dev/null +++ b/db/migrate/20220118053107_convert_stripe_connect_to_stripe_sca.rb @@ -0,0 +1,57 @@ +class ConvertStripeConnectToStripeSca < ActiveRecord::Migration[6.1] + class SpreePreference < ActiveRecord::Base + scope :leftover_from_payment_type, ->(class_name) { + joins <<~SQL + JOIN spree_payment_methods + ON spree_preferences.key = + CONCAT('/#{class_name.underscore}/enterprise_id/', spree_payment_methods.id) + AND spree_payment_methods.type != '#{class_name}' + SQL + } + end + + def up + delete_outdated_spree_preferences + upgrade_stripe_payment_methods + update_payment_method_preferences + end + + private + + # When changing the type of a payment method, we leave orphaned records in + # the spree_preferences table. The key of the preference contains the type + # of the payment method and therefore changing the type disconnects the + # preference. + # + # Here we delete orphaned preferences first so that we don't have any + # conflicts later when updating preferences alongside the connected + # payment methods. + def delete_outdated_spree_preferences + outdated_keys = + SpreePreference.leftover_from_payment_type("Spree::Gateway::StripeConnect").pluck(:key) + + SpreePreference.leftover_from_payment_type("Spree::Gateway::StripeSCA").pluck(:key) + + SpreePreference.where(key: outdated_keys).delete_all + + # Spree preferences are cached and we want to avoid reading old values. + # Danger: The cache may alter the given array in place. Make sure to + # not use the `outdated_keys` variable after this call. + Rails.cache.delete_multi(outdated_keys) + end + + def upgrade_stripe_payment_methods + execute <<~SQL + UPDATE spree_payment_methods + SET type = 'Spree::Gateway::StripeSCA' + WHERE type = 'Spree::Gateway::StripeConnect' + SQL + end + + def update_payment_method_preferences + execute <<~SQL + UPDATE spree_preferences + SET key = replace(key, 'stripe_connect', 'stripe_sca') + WHERE key LIKE '/spree/gateway/stripe_connect/%' + SQL + end +end diff --git a/db/schema.rb b/db/schema.rb index d3ec898e90..b1c9499ddb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_01_14_110920) do +ActiveRecord::Schema.define(version: 2022_01_18_053107) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/migrations/convert_stripe_connect_to_stripe_sca_spec.rb b/spec/migrations/convert_stripe_connect_to_stripe_sca_spec.rb new file mode 100644 index 0000000000..6a37b1b4a5 --- /dev/null +++ b/spec/migrations/convert_stripe_connect_to_stripe_sca_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../../db/migrate/20220118053107_convert_stripe_connect_to_stripe_sca' + +describe ConvertStripeConnectToStripeSca do + let(:owner) { create(:distributor_enterprise) } + let(:new_owner) { create(:distributor_enterprise) } + let(:old_stripe_connect) { + Spree::Gateway::StripeConnect.create!( + name: "Stripe", + environment: "test", + preferred_enterprise_id: owner.id, + distributor_ids: [owner.id] + ) + } + let(:result) { Spree::PaymentMethod.find(old_stripe_connect.id) } + + before do + # Activate the cache because it's deactivated in test environment. + allow(Spree::Preferences::Store.instance).to receive(:should_persist?).and_return(true) + + # Create the payment method after cache activation to store the owner. + old_stripe_connect + end + + it "converts payment methods" do + subject.up + + expect(result.class).to eq Spree::Gateway::StripeSCA + end + + it "keeps attributes" do + subject.up + + expect(result.name).to eq "Stripe" + expect(result.environment).to eq "test" + expect(result.distributor_ids).to eq [owner.id] + end + + it "keeps Spree preferences" do + subject.up + + expect(result.preferred_enterprise_id).to eq owner.id + end + + it "doesn't move outdated StripeConnect preferences to StripeSCA methods" do + # When you change the type of a payment method in the admin screen + # it leaves old entries in the spree_preferences table. + # Here is a simulation of such a change: + old_stripe_connect.update_columns(type: "Spree::Gateway::StripeSCA") + changed_method = Spree::PaymentMethod.find(old_stripe_connect.id) + changed_method.preferred_enterprise_id = new_owner.id + + subject.up + + expect(result.preferred_enterprise_id).to eq new_owner.id + end + + it "keeps Spree preferences despite conflicting preference keys" do + # We change the payment method to StripeSCA and then back to StripeConnect + # to generate a conflicting preference. We want to keep the preference + # of the current payment method, not the intermediately changed one. + old_stripe_connect.update_columns(type: "Spree::Gateway::StripeSCA") + changed_method = Spree::PaymentMethod.find(old_stripe_connect.id) + changed_method.preferred_enterprise_id = owner.id + old_stripe_connect.update_columns(type: "Spree::Gateway::StripeConnect") + old_stripe_connect.preferred_enterprise_id = new_owner.id + + subject.up + + expect(result.preferred_enterprise_id).to eq new_owner.id + end + + it "doesn't mess with new Stripe payment methods" do + stripe = Spree::Gateway::StripeSCA.create!( + name: "Modern Stripe", + environment: "test", + preferred_enterprise_id: owner.id, + distributor_ids: [owner.id] + ) + + expect { subject.up }.to_not change { stripe.reload.attributes } + end + + it "doesn't mess with other payment methods" do + cash = Spree::PaymentMethod::Check.create!( + name: "Cash on delivery", + environment: "test", + distributor_ids: [owner.id] + ) + + expect { subject.up }.to_not change { cash.reload.attributes } + end +end