diff --git a/app/services/builtin_callouts.rb b/app/services/builtin_callouts.rb index f34b6e845..0575e1e89 100644 --- a/app/services/builtin_callouts.rb +++ b/app/services/builtin_callouts.rb @@ -131,7 +131,7 @@ def initialize(event) def seed existing_keys = @event.registration_ticket_callouts.builtin.pluck(:builtin_key).to_set definitions.reject { |definition| existing_keys.include?(definition[:builtin_key]) } - .map { |definition| create(definition) } + .filter_map { |definition| create(definition) } end # In-memory counterpart to #seed. Skips keys already present on the loaded @@ -294,6 +294,12 @@ def create(definition) callout = @event.registration_ticket_callouts.create!(attributes_for(definition)) build_resource_links(callout, definition) callout + rescue ActiveRecord::RecordNotUnique + # A concurrent request seeded this built-in in the window between our existence + # check and this insert, so the unique index on [event_id, builtin_key] rejects + # the duplicate. Seeding is idempotent, so drop the failed in-memory row and skip. + @event.registration_ticket_callouts.reset + nil end # In-memory sibling of #create: builds the row (and its resource links) without diff --git a/spec/services/builtin_callouts_spec.rb b/spec/services/builtin_callouts_spec.rb index 737161257..59a1fa72f 100644 --- a/spec/services/builtin_callouts_spec.rb +++ b/spec/services/builtin_callouts_spec.rb @@ -230,6 +230,29 @@ expect(faq.hidden).to be(true) end + it "skips a built-in when the DB unique index rejects a concurrent duplicate" do + event = create(:event, cost_cents: 0) + service = described_class.new(event) + + # Simulate the concurrency window: the in-memory existence + uniqueness + # checks pass, but the unique index on [event_id, builtin_key] rejects the + # insert because a concurrent request committed the same row first. Fire it + # for Handouts, mirroring the Honeybadger report. + original_create = event.registration_ticket_callouts.method(:create!) + allow(event.registration_ticket_callouts).to receive(:create!) do |attrs| + raise ActiveRecord::RecordNotUnique, "Duplicate entry" if attrs[:builtin_key] == "handouts" + original_create.call(attrs) + end + + expect { service.seed }.not_to raise_error + + keys = event.registration_ticket_callouts.builtin.pluck(:builtin_key) + expect(keys).to contain_exactly( + "payment", "certificate", "scholarship", "ce_hours", "art_supplies", + "videoconference", "faq" + ) + end + it "appends the built-in callouts after existing custom ones" do event = create(:event) custom = create(:registration_ticket_callout, event:, title: "Parking")