From 4d87b88e856520d6193601f8672074f0d8a92c1a Mon Sep 17 00:00:00 2001 From: NewAlexandria Date: Mon, 2 Mar 2026 15:23:53 -0500 Subject: [PATCH] Handle duplicate base_codes in PromotionMigrator PromotionMigrator#copy_promotion_codes joined legacy and new promotion_code_batches on base_code alone, which is not unique across promotions; unrelated batches sharing a base_code multiplied rows in the INSERT ... SELECT and triggered PG::UniqueViolation on index_solidus_promotions_promotion_codes_on_value. Constrain the join on promotion_id and created_at so each legacy code resolves to the correct new batch for the promotion being migrated. --- .../solidus_promotions/promotion_migrator.rb | 5 +- .../promotion_migrator_spec.rb | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/promotions/lib/solidus_promotions/promotion_migrator.rb b/promotions/lib/solidus_promotions/promotion_migrator.rb index b7b22c691b0..f3e9313aa54 100644 --- a/promotions/lib/solidus_promotions/promotion_migrator.rb +++ b/promotions/lib/solidus_promotions/promotion_migrator.rb @@ -61,7 +61,10 @@ def copy_promotion_codes(new_promotion) SELECT solidus_promotions_promotions.id AS promotion_id, solidus_promotions_promotion_code_batches.id AS promotion_code_batch_id, value, spree_promotion_codes.created_at, spree_promotion_codes.updated_at FROM spree_promotion_codes LEFT OUTER JOIN spree_promotion_code_batches ON spree_promotion_code_batches.id = spree_promotion_codes.promotion_code_batch_id - LEFT OUTER JOIN solidus_promotions_promotion_code_batches ON solidus_promotions_promotion_code_batches.base_code = spree_promotion_code_batches.base_code + LEFT OUTER JOIN solidus_promotions_promotion_code_batches + ON solidus_promotions_promotion_code_batches.base_code = spree_promotion_code_batches.base_code + AND solidus_promotions_promotion_code_batches.promotion_id = #{new_promotion.id} + AND solidus_promotions_promotion_code_batches.created_at = spree_promotion_code_batches.created_at INNER JOIN spree_promotions ON spree_promotion_codes.promotion_id = spree_promotions.id INNER JOIN solidus_promotions_promotions ON spree_promotions.id = solidus_promotions_promotions.original_promotion_id WHERE spree_promotion_codes.promotion_id = #{new_promotion.original_promotion_id}; diff --git a/promotions/spec/lib/solidus_promotions/promotion_migrator_spec.rb b/promotions/spec/lib/solidus_promotions/promotion_migrator_spec.rb index 85b9a8e5cb6..031586030ac 100644 --- a/promotions/spec/lib/solidus_promotions/promotion_migrator_spec.rb +++ b/promotions/spec/lib/solidus_promotions/promotion_migrator_spec.rb @@ -52,6 +52,55 @@ end end + context "when multiple promotions have batches with the same base_code" do + let(:shared_base_code) { "SUVIE" } + let(:shared_time) { Time.current.change(usec: 0) } + let!(:spree_promotion) { create(:promotion) } + let!(:another_spree_promotion) { create(:promotion) } + let!(:first_batch) do + Spree::PromotionCodeBatch.create!( + promotion: spree_promotion, + base_code: shared_base_code, + number_of_codes: 1, + created_at: shared_time, + updated_at: shared_time + ) + end + let!(:second_batch) do + Spree::PromotionCodeBatch.create!( + promotion: another_spree_promotion, + base_code: shared_base_code, + number_of_codes: 1, + created_at: shared_time, + updated_at: shared_time + ) + end + let!(:first_code) do + create( + :promotion_code, + promotion: spree_promotion, + promotion_code_batch: first_batch, + value: "suvie-lgm4gw" + ) + end + let!(:second_code) do + create( + :promotion_code, + promotion: another_spree_promotion, + promotion_code_batch: second_batch, + value: "suvie-abc123" + ) + end + + it "copies each code exactly once without raising a duplicate value error" do + expect { subject }.not_to raise_error + expect(SolidusPromotions::PromotionCode.where(value: [first_code.value, second_code.value]).count).to eq(2) + expect( + SolidusPromotions::PromotionCode.where(value: [first_code.value, second_code.value]).group(:value).count.values + ).to all(eq(1)) + end + end + context "if our rules and actions are missing from the promotion map" do let(:promotion_map) do {