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