Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/services/builtin_callouts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions spec/services/builtin_callouts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down