diff --git a/db/migrate/20250827205335_migrate_cvv_message_to_redirect_auth_url.rb b/db/migrate/20250827205335_migrate_cvv_message_to_redirect_auth_url.rb new file mode 100644 index 0000000000..035bc63161 --- /dev/null +++ b/db/migrate/20250827205335_migrate_cvv_message_to_redirect_auth_url.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class MigrateCvvMessageToRedirectAuthUrl < ActiveRecord::Migration[7.1] + def up + records = Spree::Payment.where.not( + cvv_response_message: nil + ).where.not( + state: :completed + ) + + records.update_all( + "redirect_auth_url = cvv_response_message, cvv_response_message = null" + ) + end + + def down + records = Spree::Payment.where.not( + redirect_auth_url: nil + ).where.not( + state: :completed + ) + + records.update_all("cvv_response_message = redirect_auth_url, redirect_auth_url = null") + end +end diff --git a/db/schema.rb b/db/schema.rb index d92bed1b26..2de662697f 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[7.1].define(version: 2025_08_20_224718) do +ActiveRecord::Schema[7.1].define(version: 2025_08_27_205335) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "plpgsql" diff --git a/spec/migrations/migrate_cvv_message_to_redirect_auth_url.rb b/spec/migrations/migrate_cvv_message_to_redirect_auth_url.rb new file mode 100644 index 0000000000..31c7aaa1db --- /dev/null +++ b/spec/migrations/migrate_cvv_message_to_redirect_auth_url.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../../db/migrate/20250827205335_migrate_cvv_message_to_redirect_auth_url.rb' + +RSpec.describe MigrateCvvMessageToRedirectAuthUrl, type: :migration do + let(:migration) { described_class.new } + + describe '#up' do + context 'when payments have cvv_response_message with redirect URLs and are not completed' do + let!(:payment_requires_auth) do + create(:payment, + cvv_response_message: 'https://bank.com/3ds-redirect?token=abc123', + redirect_auth_url: nil, + state: 'requires_authorization') + end + + let!(:payment_processing) do + create(:payment, + cvv_response_message: 'https://payment-gateway.com/auth/redirect', + redirect_auth_url: nil, + state: 'processing') + end + + let!(:payment_pending) do + create(:payment, + cvv_response_message: 'https://secure.payment.com/authenticate', + redirect_auth_url: nil, + state: 'pending') + end + + it 'migrates cvv_response_message to redirect_auth_url' do + migration.up + + payment_requires_auth.reload + payment_processing.reload + payment_pending.reload + + expect(payment_requires_auth.redirect_auth_url).to eq('https://bank.com/3ds-redirect?token=abc123') + expect(payment_processing.redirect_auth_url).to eq('https://payment-gateway.com/auth/redirect') + expect(payment_pending.redirect_auth_url).to eq('https://secure.payment.com/authenticate') + + expect(payment_requires_auth.cvv_response_message).to be_nil + expect(payment_processing.cvv_response_message).to be_nil + expect(payment_pending.cvv_response_message).to be_nil + end + end + + context 'when payments are completed' do + let!(:completed_payment) do + create(:payment, + cvv_response_message: nil, + redirect_auth_url: nil, + state: 'completed') + end + + it 'does not affect completed payments (they already have nil cvv_response_message)' do + migration.up + + completed_payment.reload + + expect(completed_payment.cvv_response_message).to be_nil + expect(completed_payment.redirect_auth_url).to be_nil + end + end + + context 'when payments have nil cvv_response_message' do + let!(:nil_cvv_payment) do + create(:payment, + cvv_response_message: nil, + redirect_auth_url: nil, + state: 'pending') + end + + it 'does not migrate payments with nil cvv_response_message' do + migration.up + + nil_cvv_payment.reload + + expect(nil_cvv_payment.cvv_response_message).to be_nil + expect(nil_cvv_payment.redirect_auth_url).to be_nil + end + end + + context 'mixed payment states' do + let!(:eligible_payments) do + [ + create(:payment, cvv_response_message: 'https://url1.com', state: 'requires_authorization'), + create(:payment, cvv_response_message: 'https://url2.com', state: 'processing'), + create(:payment, cvv_response_message: 'https://url3.com', state: 'pending'), + create(:payment, cvv_response_message: 'https://url4.com', state: 'checkout'), + create(:payment, cvv_response_message: 'https://url5.com', state: 'failed'), + create(:payment, cvv_response_message: 'https://url6.com', state: 'void'), + create(:payment, cvv_response_message: 'https://url7.com', state: 'invalid') + ] + end + + let!(:ineligible_payments) do + [ + create(:payment, cvv_response_message: nil, state: 'completed'), + create(:payment, cvv_response_message: nil, state: 'requires_authorization') + ] + end + + it 'only migrates non-completed payments with cvv_response_message' do + migration.up + + # Check eligible payments were migrated + eligible_payments.each do |payment| + payment.reload + expect(payment.redirect_auth_url).to be_present + expect(payment.cvv_response_message).to be_nil + end + + # Check ineligible payments were not migrated + ineligible_payments.each do |payment| + payment.reload + expect(payment.redirect_auth_url).to be_nil + expect(payment.cvv_response_message).to eq(payment.cvv_response_message) + end + end + end + end + + describe '#down' do + context 'when payments have redirect_auth_url and are not completed' do + let!(:requires_auth_payment) do + create(:payment, + cvv_response_message: nil, + redirect_auth_url: 'https://bank.com/3ds-redirect?token=xyz789', + state: 'requires_authorization') + end + + let!(:processing_payment_with_redirect) do + create(:payment, + cvv_response_message: nil, + redirect_auth_url: 'https://gateway.com/authenticate', + state: 'processing') + end + + it 'migrates redirect_auth_url back to cvv_response_message' do + migration.down + + requires_auth_payment.reload + processing_payment_with_redirect.reload + + expect(requires_auth_payment.cvv_response_message).to eq('https://bank.com/3ds-redirect?token=xyz789') + expect(processing_payment_with_redirect.cvv_response_message).to eq('https://gateway.com/authenticate') + expect(requires_auth_payment.redirect_auth_url).to be_nil + expect(processing_payment_with_redirect.redirect_auth_url).to be_nil + end + end + + context 'when payments are completed' do + let!(:completed_payment_with_redirect) do + create(:payment, + cvv_response_message: nil, + redirect_auth_url: nil, + state: 'completed') + end + + it 'does not affect completed payments (they have nil values)' do + migration.down + + completed_payment_with_redirect.reload + + expect(completed_payment_with_redirect.redirect_auth_url).to be_nil + expect(completed_payment_with_redirect.cvv_response_message).to be_nil + end + end + + context 'when payments have nil redirect_auth_url' do + let!(:nil_redirect_payment) do + create(:payment, + cvv_response_message: nil, + redirect_auth_url: nil, + state: 'pending') + end + + it 'does not affect payments with nil redirect_auth_url' do + migration.down + + nil_redirect_payment.reload + + expect(nil_redirect_payment.redirect_auth_url).to be_nil + expect(nil_redirect_payment.cvv_response_message).to be_nil + end + end + end + + describe 'full migration cycle (up then down)' do + let!(:original_payments) do + [ + create( + :payment, + cvv_response_message: 'https://original1.com/auth', + state: 'requires_authorization' + ), + create( + :payment, + cvv_response_message: 'https://original2.com/redirect', + state: 'processing' + ), + create( + :payment, + cvv_response_message: 'https://original3.com/3ds', + state: 'pending' + ) + ] + end + + it 'preserves data integrity through up and down migrations' do + original_urls = original_payments.map(&:cvv_response_message) + + # Verify initial state + expect(original_payments.all? { |p| p.redirect_auth_url.nil? }).to be true + + # Migrate up + migration.up + original_payments.each(&:reload) + + # Verify up migration + expect(original_payments.map(&:redirect_auth_url)).to eq(original_urls) + expect(original_payments.all? { |p| p.cvv_response_message.nil? }).to be true + + # Migrate down + migration.down + original_payments.each(&:reload) + + # Verify down migration restores original state + expect(original_payments.map(&:cvv_response_message)).to eq(original_urls) + expect(original_payments.all? { |p| p.redirect_auth_url.nil? }).to be true + end + end +end