From 94868855da40585b3b3dc05ba65bde53ff8e1705 Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 14 Jul 2026 07:58:52 -0400 Subject: [PATCH 1/6] Sample ticket: render clickable materialized callouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sample-ticket preview showed code-defined fallback cards and rendered every card as a non-navigating div, so an admin couldn't click through to see what a callout page looks like — and the preview could show cards a real registrant wouldn't (built-ins are unpublished by default off facilitator trainings). Seed the built-ins on preview (idempotent, like #edit) so the ticket renders purely from the event's materialized rows: published rows by default, every row (incl. unpublished / not-yet-dripped) under "Show all options". Content/custom callouts link to their registration-independent detail page so admins can preview the real page; behavioral built-ins stay non-navigating (their pages are per-registration). Add a "Preview sample ticket" link in the event editor's callouts section. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/controllers/events_controller.rb | 6 +++ .../event_registrations/_ticket.html.erb | 40 ++++++++++++------ app/views/events/_form.html.erb | 15 +++++++ app/views/events/sample_ticket.html.erb | 5 ++- .../show.html.erb | 8 +++- .../registration_ticket_callouts_spec.rb | 9 ++++ spec/requests/events_spec.rb | 42 ++++++++++++++++--- 7 files changed, 105 insertions(+), 20 deletions(-) diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 1424cf6568..33c12ef0b0 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -67,6 +67,12 @@ def dashboard def sample_ticket authorize! @event, to: :dashboard? + # Materialize any missing built-in callouts (idempotent, like #edit) so the + # preview renders purely from the event's materialized rows — the built-ins + # are real editable rows once seeded, so there's no code-defined fallback to + # fall back to here. + BuiltinCallouts.seed(@event) + @show_all_options = params[:options] == "all" registrant = Person.new(first_name: "Sample", last_name: "Registrant") @event_registration = @event.event_registrations.new( diff --git a/app/views/event_registrations/_ticket.html.erb b/app/views/event_registrations/_ticket.html.erb index a4cbd948e0..eaba809bca 100644 --- a/app/views/event_registrations/_ticket.html.erb +++ b/app/views/event_registrations/_ticket.html.erb @@ -1,4 +1,5 @@ <% preview = local_assigns.fetch(:preview, false) %> +<% show_all = local_assigns.fetch(:show_all, false) %>
@@ -103,31 +104,44 @@ <% builtin_cards = BuiltinCalloutCards.new(event_registration, preview: preview) %> - <% builtin_cards.cards.each do |card| %> - <%= render "event_registrations/callout_card", callout: card, linked: !preview %> + the event hasn't materialized (the fallback for old, unseeded events). + Each knows its own visibility rule. The sample-ticket preview seeds the + event first, so it skips this and renders purely from the rows below. --> + <% unless preview %> + <% builtin_cards.cards.each do |card| %> + <%= render "event_registrations/callout_card", callout: card, linked: true %> + <% end %> <% end %> + editable rows, in the one admin-ordered list. A behavioral built-in row + (payment, certificate, …) renders the app's live card; content/custom rows + render their own copy and link to their detail page. On a real ticket only + published, payment-eligible callouts show; a drip date only withholds the + page content, not the card. The preview shows the same published set by + default; "Show all options" additionally reveals unpublished rows so admins + can preview every card. --> <% payment_access = event_registration.payment_access_granted? || @checkout_payment_status == "paid" %> - <% event_registration.event.registration_ticket_callouts.visible.each do |callout| %> - <% next if callout.payment_access_gated && !payment_access %> + <% callouts = preview && show_all ? event_registration.event.registration_ticket_callouts : event_registration.event.registration_ticket_callouts.visible %> + <% callouts.each do |callout| %> + <% next if callout.payment_access_gated && !payment_access && !(preview && show_all) %> <% if callout.behavioral_builtin? %> <% card = builtin_cards.card_for(callout) %> <% if card %> <%= render "event_registrations/callout_card", callout: card, linked: !preview %> + <% elsif preview && show_all %> + + <%= render "event_registrations/callout_card", callout: callout, href: nil, linked: false %> <% end %> <% else %> <%= render "event_registrations/callout_card", callout: callout, - href: event_registration_ticket_callout_path(event_registration.event, callout, reg: event_registration.slug), - linked: !preview %> + href: preview ? + event_registration_ticket_callout_path(event_registration.event, callout, return_to: "sample_ticket") : + event_registration_ticket_callout_path(event_registration.event, callout, reg: event_registration.slug), + linked: preview ? callout.page_content? : true %> <% end %> <% end %> diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index 5c6975b3f3..203b8e53dd 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -569,6 +569,21 @@ own call-outs for actions (forms to download, balances to pay) or reference reading (parking, policies, what to bring). New custom callouts save in the order shown; drag the handle to reorder saved callouts.

+ <% if @event.persisted? %> + <%# Opens the sample ticket in a new tab so the admin keeps their place in + the editor. It reflects saved callouts, not unsaved edits in this form. %> +
+ <%= link_to sample_ticket_event_path(@event, return_to: "edit_callouts"), + target: "_blank", rel: "noopener", + class: "inline-flex items-center gap-2 rounded-md border border-purple-200 bg-purple-50 px-3 py-1.5 text-sm font-medium text-purple-700 hover:bg-purple-100" do %> + + Preview sample ticket + + <% end %> +

Opens in a new tab, showing published callouts (use “Show all options” there to preview unpublished ones). Reflects saved callouts — save your edits first.

+
+ <% end %> + <%# Built-in cards, shown above custom callouts in the order they appear on the ticket — greyed-out previews of cards the app controls, so the full diff --git a/app/views/events/sample_ticket.html.erb b/app/views/events/sample_ticket.html.erb index 308c4475ff..740d91f4cb 100644 --- a/app/views/events/sample_ticket.html.erb +++ b/app/views/events/sample_ticket.html.erb @@ -6,6 +6,9 @@ when "registrants" %> <% back_label = "← Registrants" %> <% back_path = registrants_event_path(@event) %> +<% when "edit_callouts" %> + <% back_label = "← Edit callouts" %> + <% back_path = edit_event_path(@event, expand: "callouts", anchor: "registration_ticket_callouts") %> <% else %> <% back_label = "← Dashboard" %> <% back_path = dashboard_event_path(@event) %> @@ -53,4 +56,4 @@
-<%= render "event_registrations/ticket", event_registration: @event_registration, preview: true %> +<%= render "event_registrations/ticket", event_registration: @event_registration, preview: true, show_all: @show_all_options %> diff --git a/app/views/registration_ticket_callouts/show.html.erb b/app/views/registration_ticket_callouts/show.html.erb index ea78d733ca..d6a5b7e93d 100644 --- a/app/views/registration_ticket_callouts/show.html.erb +++ b/app/views/registration_ticket_callouts/show.html.erb @@ -1,7 +1,13 @@ <% content_for(:page_bg_class, "public") %> <% content_for(:page_title, "#{@callout.title} — #{@event.title}") %> <% reg_slug = params[:reg].presence %> -<% back_path = reg_slug ? registration_ticket_path(reg_slug) : nil %> +<%# Coming from the admin's sample-ticket preview, return there; a real registrant + returns to their ticket; otherwise the layout falls back to the event page. %> +<% back_path = if params[:return_to] == "sample_ticket" + sample_ticket_event_path(@event) + elsif reg_slug + registration_ticket_path(reg_slug) + end %> <%= render layout: "events/callouts/callout_page", locals: { title: @callout.title, back_path: back_path } do %> <% if @callout.subtitle.present? %>

<%= @callout.subtitle %>

diff --git a/spec/requests/events/registration_ticket_callouts_spec.rb b/spec/requests/events/registration_ticket_callouts_spec.rb index ecc92c63e0..a7e8ea48c4 100644 --- a/spec/requests/events/registration_ticket_callouts_spec.rb +++ b/spec/requests/events/registration_ticket_callouts_spec.rb @@ -16,6 +16,15 @@ expect(response.body).to include("Use the north lot.") end + it "points its back link at the sample ticket when opened from the preview" do + callout = create(:registration_ticket_callout, event:, title: "Parking", + description: "

Use the north lot.

") + + get event_registration_ticket_callout_path(event, callout, return_to: "sample_ticket") + + expect(response.body).to include(sample_ticket_event_path(event)) + end + it "redirects to the event when the callout has no description or resource" do callout = create(:registration_ticket_callout, event:, description: "") diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index 954d7ffa9f..5422778b18 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -151,26 +151,52 @@ def offer_ce!(target_event) before { sign_in admin } it "renders a preview ticket for an unsaved sample registration" do - create(:registration_ticket_callout, event: event) get sample_ticket_event_path(event) expect(response).to have_http_status(:ok) expect(response.body).to include("Sample ticket preview") expect(response.body).to include("Sample Registrant") - # Built-in callout cards render (the sample event is paid, so the payment - # card shows) without raising on the unsaved sample's sentinel slug. + end + + it "materializes the built-in callouts so the preview reads from real rows" do + expect { get sample_ticket_event_path(event) } + .to change { event.registration_ticket_callouts.builtin.count }.from(0).to(8) + end + + it "renders a published custom callout as a link to its detail page" do + callout = create(:registration_ticket_callout, event: event, + title: "Parking & directions", description: "

Lot B

") + get sample_ticket_event_path(event) + expect(response.body).to include("Parking & directions") + expect(response.body).to include( + event_registration_ticket_callout_path(event, callout, return_to: "sample_ticket") + ) + end + + it "omits an unpublished callout by default" do + create(:registration_ticket_callout, :hidden, event: event, title: "Draft note") + get sample_ticket_event_path(event) + expect(response.body).not_to include("Draft note") + end + + it "reveals unpublished built-in and custom callouts with ?options=all" do + create(:registration_ticket_callout, :hidden, event: event, title: "Draft note") + get sample_ticket_event_path(event, options: "all") + expect(response.body).to include("Draft note") + # The payment built-in seeds hidden, so it only appears here; it also + # exercises the card rendering against the unsaved sample's sentinel slug. expect(response.body).to include("Make your payment") end it "models a typical registrant by default, hiding scholarship and CE" do get sample_ticket_event_path(event) - expect(response.body).not_to include("Your scholarship request status") + expect(response.body).not_to include("Your scholarship request and award") expect(response.body).not_to include("CE hours") expect(response.body).to include("Show all options") end it "turns on every option with ?options=all" do get sample_ticket_event_path(event, options: "all") - expect(response.body).to include("Your scholarship request status") + expect(response.body).to include("Your scholarship request and award") expect(response.body).to include("CE hours") expect(response.body).to include("Show typical ticket") end @@ -266,6 +292,12 @@ def offer_ce!(target_event) expect(response.body).to include('name="event[ce_payment_due_deadline]"') expect(response.body).to include("Request CE credit by") end + + it "links to the sample ticket preview from the callouts section" do + get edit_event_path(event) + expect(response.body).to include("Preview sample ticket") + expect(response.body).to include(sample_ticket_event_path(event, return_to: "edit_callouts")) + end end describe "POST /create" do From 2511961841c21f12869fa2d970cf1695fce3506f Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 15 Jul 2026 01:09:08 -0400 Subject: [PATCH 2/6] Assert sample ticket renders a built-in callout without raising The "Make your payment" built-in was recently renamed, leaving the options=all spec asserting on stale copy. Assert on a stable built-in ("Frequently asked questions") so the test still exercises rendering hidden built-ins against the unsaved sample's sentinel slug. Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/requests/events_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index 5422778b18..2cbedfdc40 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -182,9 +182,9 @@ def offer_ce!(target_event) create(:registration_ticket_callout, :hidden, event: event, title: "Draft note") get sample_ticket_event_path(event, options: "all") expect(response.body).to include("Draft note") - # The payment built-in seeds hidden, so it only appears here; it also - # exercises the card rendering against the unsaved sample's sentinel slug. - expect(response.body).to include("Make your payment") + # The built-ins seed hidden, so they only appear here — and the ticket + # renders them against the unsaved sample's sentinel slug without raising. + expect(response.body).to include("Frequently asked questions") end it "models a typical registrant by default, hiding scholarship and CE" do From fac42bf56f09793ac0ec7ffba5661d4e7e32a7e0 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 15 Jul 2026 01:24:30 -0400 Subject: [PATCH 3/6] Base the sample ticket on a real registration so behavioral cards are clickable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behavioral built-in cards (payment, forms, CE, …) link to per-registration pages, so they only resolve for a real slug — the built sample's sentinel slug 404s, leaving them non-navigating. Prefer the event's first active registration (name masked to "Sample Person", never persisted) so those cards click through to live pages; fall back to the unsaved sample only when the event has no registrations yet, where content/custom callouts still preview. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/controllers/events_controller.rb | 45 +++++++++++++------ .../event_registrations/_ticket.html.erb | 5 ++- spec/requests/events_spec.rb | 25 ++++++++++- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 33c12ef0b0..238fd2a5a0 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -74,19 +74,10 @@ def sample_ticket BuiltinCallouts.seed(@event) @show_all_options = params[:options] == "all" - registrant = Person.new(first_name: "Sample", last_name: "Registrant") - @event_registration = @event.event_registrations.new( - registrant: registrant, - slug: "sample", - status: "registered", - intends_to_pay: true, - w9_requested: @show_all_options, - invoice_requested: @show_all_options, - scholarship_requested: @show_all_options, - shoutout: @show_all_options, - created_at: Time.current - ) - build_sample_ce_registration if @show_all_options + @event_registration = sample_registration + # The built sample has a sentinel slug that doesn't resolve, so only spin up + # the extra CE preview for it; a real registration carries its own CE. + build_sample_ce_registration if @show_all_options && @event_registration.new_record? end def background @@ -516,6 +507,34 @@ def copy_registration_form # Build (unsaved) a CE registration on the sample ticket so the "Show all # options" preview renders a populated CE card. Mirrors a complete, paid-looking # CE record without touching the database. + # The registration the sample ticket previews. Prefer a real active + # registration so the per-registration callout links (payment, forms, CE, …) + # resolve to live pages the admin can click through — its registrant name is + # masked so the preview doesn't surface a specific person. When the event has + # no registrations yet, fall back to an unsaved sample with a sentinel slug; + # its content/custom callout pages still preview, but behavioral cards (which + # need a real slug) render non-navigating. + def sample_registration + existing = @event.event_registrations.active.includes(:registrant).first + if existing + existing.registrant.assign_attributes(first_name: "Sample", last_name: "Person") + existing + else + registrant = Person.new(first_name: "Sample", last_name: "Person") + @event.event_registrations.new( + registrant: registrant, + slug: "sample", + status: "registered", + intends_to_pay: true, + w9_requested: @show_all_options, + invoice_requested: @show_all_options, + scholarship_requested: @show_all_options, + shoutout: @show_all_options, + created_at: Time.current + ) + end + end + def build_sample_ce_registration license = ProfessionalLicense.new(person: @event_registration.registrant, number: "SAMPLE-12345") @event_registration.continuing_education_registrations.build( diff --git a/app/views/event_registrations/_ticket.html.erb b/app/views/event_registrations/_ticket.html.erb index eaba809bca..fe39298ce7 100644 --- a/app/views/event_registrations/_ticket.html.erb +++ b/app/views/event_registrations/_ticket.html.erb @@ -128,7 +128,10 @@ <% if callout.behavioral_builtin? %> <% card = builtin_cards.card_for(callout) %> <% if card %> - <%= render "event_registrations/callout_card", callout: card, linked: !preview %> + <%# Behavioral cards link to per-registration pages: clickable on a real + ticket, and in the preview only when it's based on a real registration + (a built sample's sentinel slug wouldn't resolve). %> + <%= render "event_registrations/callout_card", callout: card, linked: !preview || event_registration.persisted? %> <% elsif preview && show_all %> - <%= render "event_registrations/callout_card", callout: callout, href: nil, linked: false %> + <% sample_href = sample_callout_path(event_registration.event, callout) %> + <%= render "event_registrations/callout_card", callout: callout, + href: sample_href, linked: sample_href.present? %> <% end %> <% else %> <%= render "event_registrations/callout_card", diff --git a/app/views/events/callouts/_callout_page.html.erb b/app/views/events/callouts/_callout_page.html.erb index 1261743ae0..983defbb54 100644 --- a/app/views/events/callouts/_callout_page.html.erb +++ b/app/views/events/callouts/_callout_page.html.erb @@ -5,7 +5,7 @@ callers without a registration can override it with a `back_path` local, or pass `back_path: nil` to drop the eyebrow. Its label defaults to "Back to ticket"; pass a `back_label` local to point it at a different origin. %> -<% back_path = local_assigns.fetch(:back_path) { registration_ticket_path(@event_registration.slug) } %> +<% back_path = local_assigns.fetch(:back_path) { sample_preview? ? sample_ticket_event_path(@event) : registration_ticket_path(@event_registration.slug) } %> <% back_label = local_assigns.fetch(:back_label, "Back to ticket") %> <%# Most callout pages are a narrow card; the single-resource page passes a wider value so a multi-page PDF preview has room for the browser's page sidebar. %> diff --git a/app/views/events/callouts/ce.html.erb b/app/views/events/callouts/ce.html.erb index 9ba558ed80..a73ccf2815 100644 --- a/app/views/events/callouts/ce.html.erb +++ b/app/views/events/callouts/ce.html.erb @@ -32,7 +32,7 @@ Admin-only jump to the management surface for this CE registration. Hidden from registrants; opens in a new tab so the registrant view is kept. %> - <% if ce_registration && allowed_to?(:edit?, ce_registration) %> + <% if ce_registration && allowed_to?(:edit?, ce_registration) && !sample_preview? %>
<%= link_to edit_continuing_education_registration_path(ce_registration), target: "_blank", rel: "noopener", class: "inline-flex items-center gap-1 text-xs rounded-full border px-2 py-0.5 bg-sky-100 text-sky-700 border-sky-200 hover:bg-sky-200 transition" do %> @@ -86,10 +86,15 @@

Payment due by <%= @event.ce_payment_due_deadline.to_fs(:long) %>

<% end %>
- <%= button_to "Pay with Credit Card", - registration_ce_pay_path(@event_registration.slug), - data: { turbo: false }, - class: "btn btn-accent px-6 py-2 text-sm uppercase font-telefon" %> + <% if sample_preview? %> + + <% else %> + <%= button_to "Pay with Credit Card", + registration_ce_pay_path(@event_registration.slug), + data: { turbo: false }, + class: "btn btn-accent px-6 py-2 text-sm uppercase font-telefon" %> + <% end %>
<% end %> @@ -114,7 +119,7 @@

Your professional license

- <% if license_on_file && !editing_license && !license_locked %> + <% if license_on_file && !editing_license && !license_locked && !sample_preview? %> <%= link_to registration_ce_path(@event_registration.slug, editing: "license", return_to: params[:return_to].presence), class: "shrink-0 inline-flex items-center gap-1.5 rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-600 hover:bg-gray-50" do %> diff --git a/app/views/events/callouts/payment.html.erb b/app/views/events/callouts/payment.html.erb index e1fb3a0087..7982d6cd43 100644 --- a/app/views/events/callouts/payment.html.erb +++ b/app/views/events/callouts/payment.html.erb @@ -29,10 +29,15 @@ <% if @event_registration.remaining_cost.positive? %>
- <%= button_to "Pay with Credit Card", - registration_pay_path(@event_registration.slug), - data: { turbo: false }, - class: "btn btn-accent px-6 py-2 text-sm uppercase font-telefon" %> + <% if sample_preview? %> + + <% else %> + <%= button_to "Pay with Credit Card", + registration_pay_path(@event_registration.slug), + data: { turbo: false }, + class: "btn btn-accent px-6 py-2 text-sm uppercase font-telefon" %> + <% end %>
diff --git a/config/routes.rb b/config/routes.rb index cf5f39ab2b..f1641c215a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -135,6 +135,14 @@ member do get :dashboard get :sample_ticket + # Admin-only in-memory previews of the behavioral built-in callout pages, + # linked from the sample ticket. They reuse Events::CalloutsController's + # actions/views with an unsaved sample registration (see its sample mode). + get "sample_ticket/payment", to: "events/callouts#payment", defaults: { sample: "1" }, as: :sample_payment + get "sample_ticket/certificate", to: "events/callouts#certificate", defaults: { sample: "1" }, as: :sample_certificate + get "sample_ticket/scholarship", to: "events/callouts#scholarship", defaults: { sample: "1" }, as: :sample_scholarship + get "sample_ticket/ce", to: "events/callouts#ce", defaults: { sample: "1" }, as: :sample_ce + get "sample_ticket/videoconference", to: "events/callouts#videoconference", defaults: { sample: "1" }, as: :sample_videoconference get :background get :registrants get :onboarding diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index 5e1d640556..e31d1b2d7b 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -195,6 +195,11 @@ def offer_ce!(target_event) expect(response.body).to include("Frequently asked questions") end + it "links behavioral built-in cards to their in-memory sample preview pages" do + get sample_ticket_event_path(event, options: "all") + expect(response.body).to include(sample_payment_event_path(event)) + end + it "models a typical registrant by default, hiding scholarship and CE" do get sample_ticket_event_path(event) expect(response.body).not_to include("Your scholarship request and award") @@ -224,6 +229,50 @@ def offer_ce!(target_event) end end + describe "sample callout previews" do + let(:event) { create(:event, ce_hours_offered: 6, videoconference_url: "https://example.com/vc") } + + sample_paths = { + "payment" => :sample_payment_event_path, + "certificate" => :sample_certificate_event_path, + "scholarship" => :sample_scholarship_event_path, + "ce" => :sample_ce_event_path, + "videoconference" => :sample_videoconference_event_path + } + + context "as admin" do + before { sign_in admin } + + sample_paths.each do |name, helper| + it "renders the #{name} preview for a data-free sample, back-linked to the ticket, without persisting" do + expect { get public_send(helper, event) } + .not_to change(EventRegistration, :count) + expect(response).to have_http_status(:ok) + expect(response.body).to include(sample_ticket_event_path(event)) + end + end + + it "does not expose a Pay action on the payment preview" do + event.update!(cost_cents: 5000) + get sample_payment_event_path(event) + expect(response.body).not_to include(registration_pay_path("sample")) + end + end + + context "as a non-admin" do + it "redirects a signed-in non-admin" do + sign_in user + get sample_payment_event_path(event) + expect(response).to redirect_to(root_path) + end + + it "requires login" do + get sample_payment_event_path(event) + expect(response).to redirect_to(new_user_session_path) + end + end + end + describe "GET /new" do context "as admin" do it "renders successfully" do From 5c8dffaec2a79866406cedfeceb15c868a7c69c7 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 15 Jul 2026 01:59:03 -0400 Subject: [PATCH 6/6] Bypass callout config gaps in the sample-ticket preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (a cost, scholarship form, CE hours). On the sample ticket that suppressed published cards the admin wants to preview, so a BuiltinCalloutCards preview flag now skips the gap — the admin can see and click through the card while still finishing the event's setup. Real tickets are unchanged (the gap still applies). Co-Authored-By: Claude Opus 4.8 (1M context) --- app/services/builtin_callout_cards.rb | 7 +++---- spec/requests/events_spec.rb | 11 +++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/services/builtin_callout_cards.rb b/app/services/builtin_callout_cards.rb index 922dd35192..1a3b47c638 100644 --- a/app/services/builtin_callout_cards.rb +++ b/app/services/builtin_callout_cards.rb @@ -92,13 +92,12 @@ def self.config_gap(event, builtin_key) end end + # `preview: true` is the sample ticket: it bypasses the config gaps so an admin + # can preview (and click through) a published built-in card even before the + # event carries the config it depends on (a cost, a scholarship form, CE hours). def initialize(event_registration, preview: false) @registration = event_registration @event = event_registration.event - # The admin sample-ticket preview illustrates a hypothetical registrant with - # options toggled on, so the scholarship / CE cards render from the sample - # registration's options even when the concrete event isn't configured for them - # (their config_gap is skipped). On a real ticket this stays false. @preview = preview end diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index e31d1b2d7b..bc3d4f8bea 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -200,6 +200,17 @@ def offer_ce!(target_event) expect(response.body).to include(sample_payment_event_path(event)) end + it "previews a published behavioral card even when the event config is incomplete" do + free = create(:event, cost_cents: 0) + BuiltinCallouts.seed(free) + # A free event has a payment "config gap" (BuiltinCalloutCards.config_gap) + # that hides the card on a real ticket; the preview shows it anyway so the + # admin can see and click it while finishing setup. + free.registration_ticket_callouts.find_by(builtin_key: "payment").update!(hidden: false) + get sample_ticket_event_path(free) + expect(response.body).to include(sample_payment_event_path(free)) + end + it "models a typical registrant by default, hiding scholarship and CE" do get sample_ticket_event_path(event) expect(response.body).not_to include("Your scholarship request and award")