From 9f45a210d3eed9820ad8bdd0f087487aa15936c7 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Sun, 2 Aug 2026 09:18:15 -0400
Subject: [PATCH 01/11] Track event-reg transfers via a back-link instead of a
status
Add transferred_from_registration_id (self-FK) so an incoming registration
points back at the one it transferred out of. The in-record keeps its own real
attendance status, fixing the loss of attendance data when it was marked
"transferred_in"; an out stays identifiable by its terminal status. Adds a
follow-up screen to record/link the destination after marking transferred out.
Closes #1944
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../event_registrations_controller.rb | 92 +++++++++++++++----
app/models/event_registration.rb | 32 +++++--
app/policies/event_registration_policy.rb | 2 +
app/views/event_registrations/_form.html.erb | 26 +++++-
.../event_registrations/transfer.html.erb | 55 +++++++++++
config/routes.rb | 2 +
...rom_registration_to_event_registrations.rb | 33 +++++++
db/schema.rb | 3 +
.../event_registration_decorator_spec.rb | 3 +-
spec/models/event_registration_spec.rb | 53 +++++++++--
spec/views/page_bg_class_alignment_spec.rb | 1 +
11 files changed, 266 insertions(+), 36 deletions(-)
create mode 100644 app/views/event_registrations/transfer.html.erb
create mode 100644 db/migrate/20260802131259_add_transferred_from_registration_to_event_registrations.rb
diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb
index 03838b5df0..f8d4691fff 100644
--- a/app/controllers/event_registrations_controller.rb
+++ b/app/controllers/event_registrations_controller.rb
@@ -2,7 +2,7 @@ class EventRegistrationsController < ApplicationController
require "csv"
# show redirects to slug URL; kept for backwards compatibility
- before_action :set_event_registration, only: [ :show, :edit, :update, :destroy, :update_onboarding, :toggle_certificate_issued ]
+ before_action :set_event_registration, only: [ :show, :edit, :update, :destroy, :update_onboarding, :toggle_certificate_issued, :transfer, :process_transfer ]
def index
authorize!
@@ -98,26 +98,35 @@ def update
respond_to do |format|
format.turbo_stream
format.html {
- case params[:return_to]
- when "registrants" then redirect_to helpers.registrants_event_row_path(@event_registration.event, @event_registration.id), notice: notice, status: :see_other
- when "index" then redirect_to event_registrations_path, notice: notice, status: :see_other
- when "ticket" then redirect_to registration_ticket_path(@event_registration.slug), notice: notice, status: :see_other
- when "preview_reminder" then redirect_to preview_reminder_event_path(@event_registration.event), notice: notice, status: :see_other
- when "onboarding" then redirect_to helpers.onboarding_event_row_path(@event_registration.event, @event_registration.id), notice: notice, status: :see_other
- when "attendees" then redirect_to attendees_events_path, notice: notice, status: :see_other
- when "roster" then redirect_to roster_event_path(@event_registration.event), notice: notice, status: :see_other
- # Two ways back to the recipients page: the shout-outs section (the
- # feature-a-shout-out flow) or the recipient's own card (their name).
- when "recipients" then redirect_to recipients_event_path(@event_registration.event, anchor: "shout-outs"), notice: notice, status: :see_other
- when "recipient_card" then redirect_to helpers.recipients_event_card_path(@event_registration.event, @event_registration.slug), notice: notice, status: :see_other
+ # Just marked transferred out with no destination on record yet? Send
+ # the admin straight to the transfer screen to create/link the
+ # incoming registration (issue #1944).
+ if @event_registration.saved_change_to_status? &&
+ @event_registration.transfer_destination_pending? &&
+ allowed_to?(:transfer?, @event_registration)
+ redirect_to transfer_event_registration_path(@event_registration, return_to: params[:return_to]), status: :see_other
else
- # No explicit origin: keep admins in the management context (the
- # registrants list) rather than dropping them on the public
- # registration show.
- if allowed_to?(:manage?, with: EventRegistrationPolicy)
- redirect_to helpers.registrants_event_row_path(@event_registration.event, @event_registration.id), notice: notice, status: :see_other
+ case params[:return_to]
+ when "registrants" then redirect_to helpers.registrants_event_row_path(@event_registration.event, @event_registration.id), notice: notice, status: :see_other
+ when "index" then redirect_to event_registrations_path, notice: notice, status: :see_other
+ when "ticket" then redirect_to registration_ticket_path(@event_registration.slug), notice: notice, status: :see_other
+ when "preview_reminder" then redirect_to preview_reminder_event_path(@event_registration.event), notice: notice, status: :see_other
+ when "onboarding" then redirect_to helpers.onboarding_event_row_path(@event_registration.event, @event_registration.id), notice: notice, status: :see_other
+ when "attendees" then redirect_to attendees_events_path, notice: notice, status: :see_other
+ when "roster" then redirect_to roster_event_path(@event_registration.event), notice: notice, status: :see_other
+ # Two ways back to the recipients page: the shout-outs section (the
+ # feature-a-shout-out flow) or the recipient's own card (their name).
+ when "recipients" then redirect_to recipients_event_path(@event_registration.event, anchor: "shout-outs"), notice: notice, status: :see_other
+ when "recipient_card" then redirect_to helpers.recipients_event_card_path(@event_registration.event, @event_registration.slug), notice: notice, status: :see_other
else
- redirect_to registration_ticket_path(@event_registration.slug), notice: notice, status: :see_other
+ # No explicit origin: keep admins in the management context (the
+ # registrants list) rather than dropping them on the public
+ # registration show.
+ if allowed_to?(:manage?, with: EventRegistrationPolicy)
+ redirect_to helpers.registrants_event_row_path(@event_registration.event, @event_registration.id), notice: notice, status: :see_other
+ else
+ redirect_to registration_ticket_path(@event_registration.slug), notice: notice, status: :see_other
+ end
end
end
}
@@ -173,6 +182,43 @@ def toggle_certificate_issued
end
end
+ # Follow-up screen shown after a registration is marked "transferred out":
+ # pick the destination event so the incoming registration is created/linked
+ # and the transfer trail is preserved (issue #1944).
+ def transfer
+ authorize! @event_registration, to: :transfer?
+ @return_to = params[:return_to]
+ @events = transfer_destination_events
+ end
+
+ def process_transfer
+ authorize! @event_registration, to: :transfer?
+ destination_event = Event.find(params[:destination_event_id])
+
+ # The registrant may already be registered for the destination event, which
+ # would collide with the (registrant, event) uniqueness rule — link that
+ # record as the transfer target instead of creating a duplicate.
+ destination = EventRegistration.find_or_initialize_by(
+ registrant_id: @event_registration.registrant_id,
+ event_id: destination_event.id
+ )
+ destination.transferred_from_registration = @event_registration
+
+ if destination.save
+ redirect_to edit_event_registration_path(destination, return_to: params[:return_to].presence),
+ notice: "Transfer recorded — #{@event_registration.registrant.full_name} is now registered for #{destination_event.title}.",
+ status: :see_other
+ else
+ @return_to = params[:return_to]
+ @events = transfer_destination_events
+ flash.now[:alert] = destination.errors.full_messages.to_sentence
+ render :transfer, status: :unprocessable_content
+ end
+ rescue ActiveRecord::RecordNotFound
+ redirect_to transfer_event_registration_path(@event_registration, return_to: params[:return_to].presence),
+ alert: "Select a destination event to transfer to.", status: :see_other
+ end
+
def confirm
@event_registration = EventRegistration.includes(registrant: :user, event: :location).find(params[:id])
authorize! @event_registration, to: :confirm?
@@ -340,6 +386,14 @@ def set_event_registration
@event_registration = EventRegistration.includes({ registrant: [ :user, { affiliations: :organization } ] }, { event: [ :location, :event_forms ] }, :organizations, comments: [ :created_by, :updated_by ]).find(params[:id])
end
+ # Events a registrant can be transferred into: any published event other than
+ # the one they're transferring out of, most recent first.
+ def transfer_destination_events
+ Event.where(published: true)
+ .where.not(id: @event_registration.event_id)
+ .order(start_date: :desc)
+ end
+
# Creates the audited completion row for a checklist step (recording who/when),
# or removes it — so an unchecked step leaves no trace.
def toggle_checklist_step(step, completed)
diff --git a/app/models/event_registration.rb b/app/models/event_registration.rb
index cdf1071664..7ee5f4be9e 100644
--- a/app/models/event_registration.rb
+++ b/app/models/event_registration.rb
@@ -20,6 +20,15 @@ class EventRegistration < ApplicationRecord
through: :allocations, source: :source, source_type: "Scholarship"
has_many :checklist_completions, class_name: "EventRegistrationChecklistCompletion", dependent: :destroy
+ # Event-transfer trail (issue #1944). The FK lives on the incoming record: an
+ # "in" points back at the "out" it came from, so an in is identifiable directly
+ # (transferred_from_registration_id present) without scanning other rows. The
+ # in keeps its own real attendance status; only the out is marked
+ # "transferred_out". Chained transfers form a linked list back to the original.
+ belongs_to :transferred_from_registration, class_name: "EventRegistration", optional: true
+ has_one :transferred_to_registration, class_name: "EventRegistration",
+ foreign_key: :transferred_from_registration_id, inverse_of: :transferred_from_registration, dependent: :nullify
+
accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: proc { |attrs| attrs["body"].blank? }
accepts_nested_attributes_for :notifications, allow_destroy: true, reject_if: proc { |attrs| attrs["email_subject"].blank? }
# Lets the registration edit form edit the registrant's shout-out text (which
@@ -30,7 +39,7 @@ class EventRegistration < ApplicationRecord
after_update :release_scholarships, if: :status_changed_to_cancelled?
after_commit :send_cancellation_emails, if: :status_changed_to_cancelled?
- ACTIVE_STATUSES = %w[ registered attended incomplete_attendance transferred_in ].freeze
+ ACTIVE_STATUSES = %w[ registered attended incomplete_attendance ].freeze
INACTIVE_STATUSES = %w[ cancelled no_show transferred_out ].freeze
ATTENDANCE_STATUSES = (ACTIVE_STATUSES + INACTIVE_STATUSES).freeze
# Attendance outcomes surfaced as their own participation buckets; every other
@@ -77,7 +86,6 @@ class EventRegistration < ApplicationRecord
"registered" => "Registered",
"attended" => "Attended",
"incomplete_attendance" => "Incomplete attendance",
- "transferred_in" => "Transferred in",
"cancelled" => "Cancelled",
"no_show" => "No show",
"transferred_out" => "Transferred out"
@@ -490,14 +498,26 @@ def attendance_recorded?
status.in?(%w[ attended incomplete_attendance no_show ])
end
- # Transferred out to another event. The trail to where the registrant went is
- # history worth keeping, so it blocks deletion. Transferred_in is deliberately
- # excluded: it's an ordinary active registration here, and the source event's
- # transferred_out record already preserves the transfer trail.
+ # Transferred out to another event. Terminal status, so an out is always
+ # identifiable from its status alone. The trail to where the registrant went is
+ # history worth keeping, so it blocks deletion.
def transferred_out?
status == "transferred_out"
end
+ # Transferred in from another event's registration. Identified by the presence
+ # of the back-link (not by status), so an in keeps recording its own real
+ # attendance (registered/attended/…) without losing the transfer history.
+ def transferred_in?
+ transferred_from_registration_id.present?
+ end
+
+ # A transferred-out registration whose destination hasn't been recorded yet.
+ # Drives the follow-up prompt to create/link the incoming registration.
+ def transfer_destination_pending?
+ transferred_out? && transferred_to_registration.nil?
+ end
+
# Safe to delete only when removing the record would not orphan financial data
# or erase history. Allocations tie the registration to a financial source of
# any kind (payments, scholarships, and others) and have no dependent: :destroy,
diff --git a/app/policies/event_registration_policy.rb b/app/policies/event_registration_policy.rb
index dc82897d61..ca886869eb 100644
--- a/app/policies/event_registration_policy.rb
+++ b/app/policies/event_registration_policy.rb
@@ -11,6 +11,8 @@ def show? = admin?
def show_public? = true
def confirm? = admin?
def process_confirm? = admin?
+ def transfer? = admin?
+ def process_transfer? = admin?
def link_organization? = admin?
def select_organization? = admin?
def create_organization? = admin?
diff --git a/app/views/event_registrations/_form.html.erb b/app/views/event_registrations/_form.html.erb
index f658ccb844..1fd56fd434 100644
--- a/app/views/event_registrations/_form.html.erb
+++ b/app/views/event_registrations/_form.html.erb
@@ -17,7 +17,6 @@
"incomplete_attendance" => "text-amber-600",
"cancelled" => "text-gray-500",
"no_show" => "text-red-600",
- "transferred_in" => "text-teal-600",
"transferred_out" => "text-purple-600"
} %>
<% status_icons = {
@@ -26,7 +25,6 @@
"incomplete_attendance" => "fa-clock",
"cancelled" => "fa-ban",
"no_show" => "fa-circle-xmark",
- "transferred_in" => "fa-right-to-bracket",
"transferred_out" => "fa-right-from-bracket"
} %>
<% current_icon_color = status_icon_colors[f.object.status] || "text-gray-500" %>
@@ -104,6 +102,30 @@
data: { "attendance-status-target": "select", action: "attendance-status#update" },
"aria-label": "Registration status" %>
+
+ <%# ---- Transfer trail (issue #1944) — link the paired registration so
+ the historical in/out relationship stays visible from either end. ---- %>
+ <% if f.object.transferred_out? %>
+ <% destination = f.object.transferred_to_registration %>
+ <% if destination %>
+
+ Recording the destination event creates (or links) this person's registration
+ there and keeps the transfer history. The new registration tracks its own
+ attendance — you can skip this now and record it later.
+
diff --git a/config/routes.rb b/config/routes.rb
index 7d0534d362..7a8fc084ed 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -100,6 +100,8 @@
member do
get :confirm
post :process_confirm
+ get :transfer
+ post :process_transfer
get :link_organization
post :select_organization
post :create_organization
diff --git a/db/migrate/20260802131259_add_transferred_from_registration_to_event_registrations.rb b/db/migrate/20260802131259_add_transferred_from_registration_to_event_registrations.rb
new file mode 100644
index 0000000000..c88d1da20c
--- /dev/null
+++ b/db/migrate/20260802131259_add_transferred_from_registration_to_event_registrations.rb
@@ -0,0 +1,33 @@
+class AddTransferredFromRegistrationToEventRegistrations < ActiveRecord::Migration[8.1]
+ # Records where a registration was transferred *from*: the incoming ("in")
+ # registration points back at the outgoing ("out") one. Putting the FK on the
+ # in-record means an in is identifiable directly (its FK is set) without
+ # scanning every other row, while an out stays identifiable by its terminal
+ # "transferred_out" status. See issue #1944.
+ def up
+ unless column_exists?(:event_registrations, :transferred_from_registration_id)
+ add_column :event_registrations, :transferred_from_registration_id, :bigint
+ end
+ unless index_exists?(:event_registrations, :transferred_from_registration_id)
+ add_index :event_registrations, :transferred_from_registration_id
+ end
+ unless foreign_key_exists?(:event_registrations, column: :transferred_from_registration_id)
+ add_foreign_key :event_registrations, :event_registrations,
+ column: :transferred_from_registration_id, on_delete: :nullify
+ end
+
+ # "transferred_in" is no longer an attendance status — the transfer link now
+ # records the "in" relationship, freeing the status to track real attendance.
+ # Existing transferred_in rows have no link to recover, so reset them to
+ # registered (their default) rather than leaving an invalid status.
+ execute("UPDATE event_registrations SET status = 'registered' WHERE status = 'transferred_in'")
+ end
+
+ def down
+ # Best-effort inverse: rows still carrying a transfer link were the "in"s.
+ execute("UPDATE event_registrations SET status = 'transferred_in' WHERE transferred_from_registration_id IS NOT NULL")
+ remove_foreign_key :event_registrations, column: :transferred_from_registration_id, if_exists: true
+ remove_index :event_registrations, :transferred_from_registration_id, if_exists: true
+ remove_column :event_registrations, :transferred_from_registration_id, if_exists: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b96f6d27d3..49571b7e24 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -501,6 +501,7 @@
t.string "slug"
t.boolean "someone_else_will_pay", default: false, null: false
t.string "status", default: "registered", null: false
+ t.bigint "transferred_from_registration_id"
t.datetime "updated_at", null: false
t.boolean "w9_requested", default: false, null: false
t.index ["checkout_session_id"], name: "index_event_registrations_on_checkout_session_id"
@@ -509,6 +510,7 @@
t.index ["registrant_id", "event_id"], name: "index_event_registrations_on_registrant_id_and_event_id", unique: true
t.index ["registrant_id"], name: "index_event_registrations_on_registrant_id"
t.index ["slug"], name: "index_event_registrations_on_slug", unique: true
+ t.index ["transferred_from_registration_id"], name: "index_event_registrations_on_transferred_from_registration_id"
end
create_table "event_staffs", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
@@ -1801,6 +1803,7 @@
add_foreign_key "event_registration_checklist_completions", "users", column: "completed_by_id"
add_foreign_key "event_registration_organizations", "event_registrations"
add_foreign_key "event_registration_organizations", "organizations"
+ add_foreign_key "event_registrations", "event_registrations", column: "transferred_from_registration_id", on_delete: :nullify
add_foreign_key "event_registrations", "events"
add_foreign_key "event_registrations", "people", column: "registrant_id"
add_foreign_key "event_staffs", "events"
diff --git a/spec/decorators/event_registration_decorator_spec.rb b/spec/decorators/event_registration_decorator_spec.rb
index 97e2543a7e..13e847616f 100644
--- a/spec/decorators/event_registration_decorator_spec.rb
+++ b/spec/decorators/event_registration_decorator_spec.rb
@@ -75,7 +75,8 @@
end
it "is deletable (no reason) when transferred in with no allocations" do
- reg = create(:event_registration, status: "transferred_in")
+ source = create(:event_registration, status: "transferred_out")
+ reg = create(:event_registration, status: "registered", transferred_from_registration: source)
expect(reg.decorate.deletion_blocked_reason).to be_nil
end
diff --git a/spec/models/event_registration_spec.rb b/spec/models/event_registration_spec.rb
index ffbc78926b..1e48f2bf21 100644
--- a/spec/models/event_registration_spec.rb
+++ b/spec/models/event_registration_spec.rb
@@ -41,25 +41,60 @@
reg = create(:event_registration, status: "transferred_out")
expect(reg).not_to be_active
end
-
- it "returns true for transferred_in status" do
- reg = create(:event_registration, status: "transferred_in")
- expect(reg).to be_active
- end
end
describe ".active" do
it "returns only registrations with active statuses" do
active_reg = create(:event_registration, status: "registered")
- transferred_in_reg = create(:event_registration, status: "transferred_in")
cancelled_reg = create(:event_registration, status: "cancelled")
no_show_reg = create(:event_registration, status: "no_show")
transferred_out_reg = create(:event_registration, status: "transferred_out")
results = EventRegistration.active
- expect(results).to include(active_reg, transferred_in_reg)
+ expect(results).to include(active_reg)
expect(results).not_to include(cancelled_reg, no_show_reg, transferred_out_reg)
end
+
+ it "includes a transferred-in registration, which keeps its own active status" do
+ source = create(:event_registration, status: "transferred_out")
+ incoming = create(:event_registration, status: "registered", transferred_from_registration: source)
+
+ expect(EventRegistration.active).to include(incoming)
+ end
+ end
+
+ describe "transfer trail" do
+ let(:source) { create(:event_registration, status: "transferred_out") }
+ let!(:incoming) { create(:event_registration, status: "registered", transferred_from_registration: source) }
+
+ it "links the incoming registration back to the one it came from" do
+ expect(incoming.transferred_from_registration).to eq(source)
+ expect(source.reload.transferred_to_registration).to eq(incoming)
+ end
+
+ it "identifies an in by the back-link, not by status" do
+ expect(incoming).to be_transferred_in
+ expect(incoming).not_to be_transferred_out
+ expect(create(:event_registration, status: "registered")).not_to be_transferred_in
+ end
+
+ it "identifies an out by its terminal status" do
+ expect(source).to be_transferred_out
+ expect(source).not_to be_transferred_in
+ end
+
+ it "reports a pending destination only while an out has no incoming record" do
+ pending = create(:event_registration, status: "transferred_out")
+ expect(pending).to be_transfer_destination_pending
+ expect(source).not_to be_transfer_destination_pending
+ expect(incoming).not_to be_transfer_destination_pending
+ end
+
+ it "nullifies the back-link if the source is destroyed" do
+ source.update_column(:status, "registered") # bypass the deletion guard for the test
+ source.destroy
+ expect(incoming.reload.transferred_from_registration_id).to be_nil
+ end
end
describe "#sync_attendance_status_to_days!" do
@@ -146,7 +181,9 @@
it "returns true for a transferred-in registration with no allocations" do
# Transferred-in is an ordinary active registration here; the source event's
# transferred_out record preserves the transfer history.
- expect(create(:event_registration, status: "transferred_in")).to be_deletable
+ source = create(:event_registration, status: "transferred_out")
+ incoming = create(:event_registration, status: "registered", transferred_from_registration: source)
+ expect(incoming).to be_deletable
end
end
diff --git a/spec/views/page_bg_class_alignment_spec.rb b/spec/views/page_bg_class_alignment_spec.rb
index 7924fcd22a..7bd12e3264 100644
--- a/spec/views/page_bg_class_alignment_spec.rb
+++ b/spec/views/page_bg_class_alignment_spec.rb
@@ -248,6 +248,7 @@
# ─── admin-only confirm/interstitial ───
"app/views/event_registrations/confirm.html.erb" => "admin-only bg-blue-100",
+ "app/views/event_registrations/transfer.html.erb" => "admin-only bg-blue-100",
"app/views/event_registrations/link_organization.html.erb" => "admin-only bg-blue-100",
"app/views/users/confirm_email_change.html.erb" => "admin-only bg-blue-100",
"app/views/users/confirm_email_manual.html.erb" => "admin-only bg-blue-100"
From 16e43ef65773a25ebed97841bb18b242ab413cf4 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Sun, 2 Aug 2026 09:23:00 -0400
Subject: [PATCH 02/11] Add specs for the transfer trail and follow-up flow
Cover the transferred_from/to associations, transferred_in?/transfer_destination_pending?,
the post-save redirect to the transfer screen, and the transfer create/link flow.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
spec/requests/event_registrations_spec.rb | 91 +++++++++++++++++++++++
1 file changed, 91 insertions(+)
diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb
index d8bd3f93ed..a678e38a2b 100644
--- a/spec/requests/event_registrations_spec.rb
+++ b/spec/requests/event_registrations_spec.rb
@@ -477,6 +477,23 @@ def toggle_certificate(value)
expect(response.body).to include("financial records")
expect(response.body).to include("reverted payments still count")
end
+
+ it "prompts to record the destination for a transferred-out registration" do
+ existing_registration.update!(status: "transferred_out")
+
+ get edit_event_registration_path(existing_registration)
+
+ expect(response.body).to include("Record where they transferred to")
+ end
+
+ it "shows the source event on a transferred-in registration" do
+ source = create(:event_registration, event: event, status: "transferred_out")
+ incoming = create(:event_registration, event: new_event, transferred_from_registration: source)
+
+ get edit_event_registration_path(incoming)
+
+ expect(response.body).to include("Transferred in from")
+ end
end
describe "PATCH /event_registrations/:id" do
@@ -533,6 +550,73 @@ def toggle_certificate(value)
expect(existing_registration.reload.someone_else_will_pay).to be(true)
end
+
+ it "redirects to the transfer screen when newly marked transferred out" do
+ patch event_registration_path(existing_registration),
+ params: { event_registration: { status: "transferred_out" } }
+
+ expect(response).to redirect_to(transfer_event_registration_path(existing_registration, return_to: nil))
+ end
+
+ it "does not redirect to the transfer screen once a destination is recorded" do
+ create(:event_registration, transferred_from_registration: existing_registration)
+ existing_registration.update!(status: "transferred_out")
+
+ patch event_registration_path(existing_registration),
+ params: { event_registration: { fee_note: "Settled" } }
+
+ expect(response).not_to redirect_to(transfer_event_registration_path(existing_registration, return_to: nil))
+ end
+ end
+
+ describe "transfer flow" do
+ let!(:source) { create(:event_registration, event: event, status: "transferred_out") }
+
+ describe "GET /event_registrations/:id/transfer" do
+ it "renders the destination picker, excluding the source event" do
+ other = create(:event, title: "Destination Event", published: true)
+
+ get transfer_event_registration_path(source)
+
+ expect(response).to have_http_status(:success)
+ expect(response.body).to include("Destination Event")
+ # The source event isn't offered as a transfer destination.
+ expect(response.body).not_to include("
+ <%# After a transfer, the hours are certified at the destination event
+ while this record + its payment stay here (issue #1944). %>
+ <% if ce_registration.certified_elsewhere? %>
+ <% certified_at = ce_registration.certified_at_registration %>
+
+ <% end %>
+
<%# Pinned to the card's bottom so it lines up with the scholarship card's
chip and the organizations card's "Connect organization" link. %>
diff --git a/app/views/event_registrations/_transferred_in_financials.html.erb b/app/views/event_registrations/_transferred_in_financials.html.erb
index 3fee00ad0b..65fb7e6bc0 100644
--- a/app/views/event_registrations/_transferred_in_financials.html.erb
+++ b/app/views/event_registrations/_transferred_in_financials.html.erb
@@ -98,6 +98,11 @@
<% end %>
<% if source.ce_registered? %>
+ <%# Hours are earned/certified HERE (this event); the CE record + payment
+ stay on the original registration. %>
+
+ Hours earned at this event
+
<%= source.ce_status_label %>
<%= plain_number(source.ce_hours_total) || "0" %> hrs
diff --git a/spec/models/continuing_education_registration_spec.rb b/spec/models/continuing_education_registration_spec.rb
index f341e4ae50..b671a7f79b 100644
--- a/spec/models/continuing_education_registration_spec.rb
+++ b/spec/models/continuing_education_registration_spec.rb
@@ -136,6 +136,37 @@ def ce_reg_for(event:, status:, cost_cents: 0)
ce_reg.mark_certificate_sent!
expect(ce_reg.certificate_sent?).to be(true)
end
+
+ describe "certified at a different event (transfer)" do
+ let(:home_event) { create(:event, ce_hours_offered: 6, start_date: 3.days.ago, end_date: 1.day.ago) }
+ let(:intended_event) { create(:event, ce_hours_offered: 6, start_date: 10.days.from_now, end_date: 12.days.from_now) }
+ let(:home_reg) { create(:event_registration, event: home_event, status: "transferred_out") }
+ let(:ce_reg) do
+ create(:continuing_education_registration, event_registration: home_reg, cost_cents: 0,
+ professional_license: create(:professional_license, person: home_reg.registrant))
+ end
+
+ it "defaults certified_at_registration to the home reg when not transferred" do
+ solo = create(:event_registration, event: home_event, status: "attended")
+ ce = create(:continuing_education_registration, event_registration: solo, cost_cents: 0,
+ professional_license: create(:professional_license, person: solo.registrant))
+ expect(ce.certified_at_registration).to eq(solo)
+ end
+
+ it "certifies against the destination (intended) event, not the home event" do
+ # Same person transfers on to the intended event (derives home_reg.transferred_to).
+ intended_reg = create(:event_registration, event: intended_event, registrant: home_reg.registrant,
+ status: "attended", transferred_from_registration: home_reg)
+
+ expect(ce_reg.reload.certified_at_registration).to eq(intended_reg)
+ # Home event already ended, but hours are certified at the intended event,
+ # which hasn't happened yet → not certifiable until that event ends.
+ expect(ce_reg.certificate_available?).to be(false)
+
+ intended_event.update!(start_date: 3.days.ago, end_date: 1.day.ago)
+ expect(ce_reg.reload.certificate_available?).to be(true)
+ end
+ end
end
# Payment interface comes from Registerable, driven by the CE record's own
diff --git a/spec/models/event_registration_spec.rb b/spec/models/event_registration_spec.rb
index b46f06303c..22d5c4cc71 100644
--- a/spec/models/event_registration_spec.rb
+++ b/spec/models/event_registration_spec.rb
@@ -180,6 +180,31 @@
end
end
+ describe "CE certification across a transfer" do
+ let(:home_event) { create(:event, ce_hours_offered: 6, start_date: 3.days.ago, end_date: 1.day.ago) }
+ let(:intended_event) { create(:event, ce_hours_offered: 6, start_date: 3.days.ago, end_date: 1.day.ago) }
+ let(:person) { create(:person) }
+ let!(:source) { create(:event_registration, event: home_event, registrant: person, status: "transferred_out") }
+ let!(:intended) { create(:event_registration, event: intended_event, registrant: person, status: "attended", transferred_from_registration: source) }
+ let!(:ce) do
+ create(:continuing_education_registration, event_registration: source,
+ professional_license: create(:professional_license, person: person))
+ end
+
+ it "lets the intended event issue the CE certificate for hours earned there" do
+ expect(intended.certifiable_ce_registrations).to eq([ ce ])
+ intended.mark_certificate_issued!(true)
+ expect(ce.reload.certificate_sent?).to be(true)
+ expect(intended.reload.certificate_issued?).to be(true)
+ end
+
+ it "does not issue the transferred-out hours from the source registration" do
+ expect(source.certifiable_ce_registrations).to be_empty
+ source.mark_certificate_issued!(true)
+ expect(ce.reload.certificate_sent?).to be(false)
+ end
+ end
+
describe "#sync_attendance_status_to_days!" do
# A two-day event: start and end one day apart → day_count == 2.
let(:event) { create(:event, start_date: 12.days.from_now, end_date: 13.days.from_now) }
diff --git a/spec/requests/event_registrations_spec.rb b/spec/requests/event_registrations_spec.rb
index 2f5c2b9230..124f06ca84 100644
--- a/spec/requests/event_registrations_spec.rb
+++ b/spec/requests/event_registrations_spec.rb
@@ -496,6 +496,20 @@ def toggle_certificate(value)
expect(response.body).to include("Record where they transferred to")
end
+ it "notes on the source CE card where the hours are certified after a transfer" do
+ source_event = create(:event, ce_hours_offered: 6)
+ source = create(:event_registration, event: source_event, status: "transferred_out")
+ intended = create(:event, title: "Intended Training")
+ create(:event_registration, event: intended, registrant: source.registrant, transferred_from_registration: source)
+ create(:continuing_education_registration, event_registration: source,
+ professional_license: create(:professional_license, person: source.registrant))
+
+ get edit_event_registration_path(source)
+
+ expect(response.body).to include("Hours certified at")
+ expect(response.body).to include("Intended Training")
+ end
+
it "shows the source event on a transferred-in registration" do
source = create(:event_registration, event: event, status: "transferred_out")
incoming = create(:event_registration, event: new_event, transferred_from_registration: source)
@@ -671,6 +685,20 @@ def toggle_certificate(value)
expect(response).to redirect_to(transfer_event_registration_path(source))
end
+
+ it "certifies the source reg's CE hours at the destination reg" do
+ ce = create(:continuing_education_registration, event_registration: source,
+ professional_license: create(:professional_license, person: source.registrant))
+
+ post process_transfer_event_registration_path(source),
+ params: { destination_event_id: destination_event.id }
+
+ incoming = EventRegistration.find_by(registrant: source.registrant, event: destination_event)
+ # Derived from the transfer link — no re-pointing needed.
+ expect(ce.reload.certified_at_registration).to eq(incoming)
+ expect(incoming.certifiable_ce_registrations).to include(ce)
+ expect(source.reload.certifiable_ce_registrations).to be_empty
+ end
end
end
From 8c81a6c2b8f0b462ea2350a1e720224d301e3349 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Mon, 10 Aug 2026 16:49:24 -0400
Subject: [PATCH 09/11] Note transferred-in recipients on the recipient card;
lock attendance exclusion
The event recipients card now flags a transferred-in scholarship recipient as
"Billed to original event" (recognized here, dollars on the source). Add a test
pinning that a transferred-out registration is excluded from the original
event's attendee count/attendance (it's an inactive status).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
app/views/events/_recipient_card.html.erb | 9 +++++++++
spec/requests/events_spec.rb | 1 +
spec/services/event_dashboard_spec.rb | 12 ++++++++++++
3 files changed, 22 insertions(+)
diff --git a/app/views/events/_recipient_card.html.erb b/app/views/events/_recipient_card.html.erb
index ae63dd75b1..34fa65ac59 100644
--- a/app/views/events/_recipient_card.html.erb
+++ b/app/views/events/_recipient_card.html.erb
@@ -152,6 +152,15 @@
<% end %>
<% end %>
+ <%# Transferred in: the award (and its dollars) are billed to the original
+ event; they're recognized here as a recipient (issue #1944). %>
+ <% if dashboard.transferred_in_registrant_ids.include?(person.id) %>
+
+
+ Billed to original event
+
+ <% end %>
<%= render "scholarships/tasks_status", scholarship: scholarship %>
<% if allowed_to?(:edit?, scholarship) %>
<%= link_to edit_scholarship_path(scholarship, return_to: "recipients", participant: participant_slug),
diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb
index 5775921a67..af462c747a 100644
--- a/spec/requests/events_spec.rb
+++ b/spec/requests/events_spec.rb
@@ -3038,6 +3038,7 @@ def ce_chip_text
get recipients_event_path(event)
expect(response.body).to include("Transo Ferredin")
+ expect(response.body).to include("Billed to original event")
end
it "shows a recipient city breakdown, grouped by the registration-linked org, in the lazy charts frame" do
diff --git a/spec/services/event_dashboard_spec.rb b/spec/services/event_dashboard_spec.rb
index 0aeb1e7b13..12df1cdd26 100644
--- a/spec/services/event_dashboard_spec.rb
+++ b/spec/services/event_dashboard_spec.rb
@@ -1244,6 +1244,18 @@ def opt_in(person, text:)
expect(dashboard.registrant_count).to eq(2)
end
+ it "excludes a transferred-out registration from this event's attendee count" do
+ create(:event_registration, event: event, status: "transferred_out")
+
+ # Only paid_reg + the transferred-in incoming reg count; the transferred-out
+ # one withdrew and isn't an attendee here.
+ expect(dashboard.registrant_count).to eq(2)
+ expect(dashboard.expected_attendee_count).to eq(2)
+ expect(dashboard.attendance_registrants("attended", "registered")).not_to include(
+ an_object_having_attributes(id: EventRegistration.find_by(event: event, status: "transferred_out").registrant_id)
+ )
+ end
+
it "excludes the transferred-in registrant from every financial total" do
# Only the fully-paid registrant is billable here; the transferred-in one
# owes nothing to this event (its balance is on the source registration).
From fab48dd3f492a72077c6e23485d3e3341c0b8984 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Mon, 10 Aug 2026 16:57:31 -0400
Subject: [PATCH 10/11] Surface CE certification at the intended event on
reporting/roster
Reflect where CE is certified rather than where it's billed:
- Readiness certificate checks (pending/sent) use the certifiable set (earned
here), so the intended event's roster shows the CE certificate pending and the
source no longer does; payment/license checks stay on the home reg.
- The registrants roster CE column shows a transferred-in reg's certified CE
(linking to the record on the original) instead of offering "Create".
- Add EventDashboard#transferred_in_recipient? (public) and use it for the
recipient-card "billed to original event" note (fixes a private-method call).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
app/services/event_dashboard.rb | 6 ++++
app/services/event_registration_readiness.rb | 21 ++++++++----
app/views/events/_recipient_card.html.erb | 2 +-
.../events/_registrants_results.html.erb | 34 ++++++++++++++-----
.../event_registration_readiness_spec.rb | 25 ++++++++++++++
5 files changed, 72 insertions(+), 16 deletions(-)
diff --git a/app/services/event_dashboard.rb b/app/services/event_dashboard.rb
index 3e6c487f60..3024c30d9e 100644
--- a/app/services/event_dashboard.rb
+++ b/app/services/event_dashboard.rb
@@ -74,6 +74,12 @@ def transferred_in_registrants
people_sorted(transferred_in_registrant_ids)
end
+ # Whether a registrant (Person id) transferred into this event — for surfaces
+ # that recognize them but flag that their money is billed to the source event.
+ def transferred_in_recipient?(person_id)
+ transferred_in_registrant_ids.include?(person_id)
+ end
+
# Registrations with an attendance outcome on record (attended / incomplete /
# no-show).
def attendance_outcome_count
diff --git a/app/services/event_registration_readiness.rb b/app/services/event_registration_readiness.rb
index ceb2640648..3089b568b7 100644
--- a/app/services/event_registration_readiness.rb
+++ b/app/services/event_registration_readiness.rb
@@ -169,7 +169,7 @@ def ce_license_missing?
end
def ce_certificate_pending?
- registration.ce_registered? && !ce_certificate_sent?
+ certifiable_ce.any? && !ce_certificate_sent?
end
# Post-event criteria are only met by a full "attended". "incomplete_attendance"
@@ -179,12 +179,21 @@ def attendance_issue
registration.status == "incomplete_attendance" ? "Attendance incomplete" : "Did not attend"
end
- # The admin-created CE billing records for this registration (preloaded on the
- # roster). Their payment + certificate state drives the CE readiness checks.
+ # The admin-created CE billing records homed on this registration (preloaded on
+ # the roster). Their PAYMENT + license state drives the CE money/license checks;
+ # these stay on the home reg after a transfer.
def ce_registrations
registration.continuing_education_registrations
end
+ # The CE this registration is responsible for CERTIFYING — its own plus any
+ # transferred in to be certified here (a transferred-out reg certifies none).
+ # Certificate checks use this so certification follows the person to the
+ # intended event. (issue #1944)
+ def certifiable_ce
+ registration.certifiable_ce_registrations
+ end
+
# CE is paid once every CE registration is paid in full. A requested-but-not-yet
# -created CE registration counts as unpaid (nothing to pay against yet).
def ce_paid?
@@ -197,9 +206,9 @@ def registration_certificate_sent?
registration.certificate_sent?
end
- # CE certificates are sent once every CE registration's certificate has been
- # sent. No CE registration yet means nothing has been issued.
+ # CE certificates are sent once every CE registration this reg certifies has
+ # been sent. No certifiable CE means nothing has been issued.
def ce_certificate_sent?
- ce_registrations.any? && ce_registrations.all?(&:certificate_sent?)
+ certifiable_ce.any? && certifiable_ce.all?(&:certificate_sent?)
end
end
diff --git a/app/views/events/_recipient_card.html.erb b/app/views/events/_recipient_card.html.erb
index 34fa65ac59..076affd6d0 100644
--- a/app/views/events/_recipient_card.html.erb
+++ b/app/views/events/_recipient_card.html.erb
@@ -154,7 +154,7 @@
<% end %>
<%# Transferred in: the award (and its dollars) are billed to the original
event; they're recognized here as a recipient (issue #1944). %>
- <% if dashboard.transferred_in_registrant_ids.include?(person.id) %>
+ <% if dashboard.transferred_in_recipient?(person.id) %>
diff --git a/app/views/events/_registrants_results.html.erb b/app/views/events/_registrants_results.html.erb
index 6eda46416d..731aa6e80d 100644
--- a/app/views/events/_registrants_results.html.erb
+++ b/app/views/events/_registrants_results.html.erb
@@ -374,16 +374,32 @@
<%# Canonical CE badge (Requested → License # needed → $X due →
Pending → Issued). Each state links to the connected CE
registration's edit page; "Create" opens the new CE form. %>
- <% ce_registration = registration.continuing_education_registrations.first %>
- <% if ce_registration.nil? %>
- <%= render "shared/badge",
- label: "Create",
- classes: "bg-gray-50 text-gray-400 border-gray-200",
- href: new_continuing_education_registration_path(allocatable_sgid: registration.to_sgid.to_s, return_to: "registrants"),
- title: "Add CE registration" %>
+ <% if registration.transferred_in? %>
+ <%# CE is certified here but the record + payment live on the
+ original registration — link there, don't offer "Create". %>
+ <% certified_ce = registration.certifiable_ce_registrations.first %>
+ <% if certified_ce %>
+ <%= render "shared/badge",
+ label: "CE (transferred)",
+ classes: "bg-teal-50 text-teal-700 border-teal-200",
+ href: edit_continuing_education_registration_path(certified_ce, return_to: "registrants"),
+ title: "CE hours certified here; record + payment on the original registration",
+ target: "_blank", rel: "noopener" %>
+ <% else %>
+ —
+ <% end %>
<% else %>
- <%= render "event_registrations/ce_status_badge", registration: registration,
- href: edit_continuing_education_registration_path(ce_registration, return_to: "registrants") %>
+ <% ce_registration = registration.continuing_education_registrations.first %>
+ <% if ce_registration.nil? %>
+ <%= render "shared/badge",
+ label: "Create",
+ classes: "bg-gray-50 text-gray-400 border-gray-200",
+ href: new_continuing_education_registration_path(allocatable_sgid: registration.to_sgid.to_s, return_to: "registrants"),
+ title: "Add CE registration" %>
+ <% else %>
+ <%= render "event_registrations/ce_status_badge", registration: registration,
+ href: edit_continuing_education_registration_path(ce_registration, return_to: "registrants") %>
+ <% end %>
<% end %>
<% end %>
diff --git a/spec/services/event_registration_readiness_spec.rb b/spec/services/event_registration_readiness_spec.rb
index 23f3338514..c57dba6047 100644
--- a/spec/services/event_registration_readiness_spec.rb
+++ b/spec/services/event_registration_readiness_spec.rb
@@ -177,6 +177,31 @@ def award_scholarship(reg, tasks_completed:, amount: 1000)
end
end
+ describe "CE certificate across a transfer" do
+ let(:source) { create(:event_registration, event: create(:event, ce_hours_offered: 6, cost_cents: 0), status: "transferred_out") }
+ let(:intended) do
+ create(:event_registration, event: create(:event, ce_hours_offered: 6, cost_cents: 0),
+ registrant: source.registrant, status: "attended", transferred_from_registration: source)
+ end
+ let!(:ce) do
+ create(:continuing_education_registration, event_registration: source, cost_cents: 0,
+ professional_license: create(:professional_license, person: source.registrant))
+ end
+
+ it "flags the intended event's roster with the certified CE still pending" do
+ expect(described_class.new(intended).completion_issues).to include("CE certificate not sent")
+ end
+
+ it "clears once the certified CE has been issued" do
+ ce.mark_certificate_sent!
+ expect(described_class.new(intended).completion_issues).not_to include("CE certificate not sent")
+ end
+
+ it "does not flag the certified CE on the source (transferred-out) registration" do
+ expect(described_class.new(source).certificate_issues).not_to include("CE certificate not sent")
+ end
+ end
+
describe "#status" do
it "is :not_ready when a pre-event condition is outstanding" do
# default registrant is unpaid on a paid event
From 1d3ed18ad2b3aeff428c3b243314543e5471d0cc Mon Sep 17 00:00:00 2001
From: maebeale
Date: Mon, 10 Aug 2026 17:01:48 -0400
Subject: [PATCH 11/11] Add transfer notices to the ticket and builtin callout
pages
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both the original and the new event's registration have their own ticket, so a
shared _transfer_notice partial explains on each where the money/scholarship/CE
records live and where attendance + the certificate are earned. Rendered on the
ticket and the payment, CE, scholarship, certificate, invoice, and receipt
pages — surfaced to attendee and staff, linking to the paired registration's
ticket.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../event_registrations/_ticket.html.erb | 1 +
.../_transfer_notice.html.erb | 26 +++++++++++++++++++
app/views/events/callouts/ce.html.erb | 1 +
.../events/callouts/certificate.html.erb | 1 +
app/views/events/callouts/payment.html.erb | 1 +
.../events/callouts/scholarship.html.erb | 1 +
.../events/registrations/invoice.html.erb | 1 +
.../events/registrations/receipt.html.erb | 1 +
spec/requests/events/registrations_spec.rb | 21 +++++++++++++++
9 files changed, 54 insertions(+)
create mode 100644 app/views/event_registrations/_transfer_notice.html.erb
diff --git a/app/views/event_registrations/_ticket.html.erb b/app/views/event_registrations/_ticket.html.erb
index a0679417e6..a9ae00658e 100644
--- a/app/views/event_registrations/_ticket.html.erb
+++ b/app/views/event_registrations/_ticket.html.erb
@@ -1,6 +1,7 @@
<% preview = local_assigns.fetch(:preview, false) %>
<% show_all = local_assigns.fetch(:show_all, false) %>
diff --git a/app/views/event_registrations/_transfer_notice.html.erb b/app/views/event_registrations/_transfer_notice.html.erb
new file mode 100644
index 0000000000..7b81347980
--- /dev/null
+++ b/app/views/event_registrations/_transfer_notice.html.erb
@@ -0,0 +1,26 @@
+<%# Transfer notice for the registrant ticket + builtin callout pages (issue
+ #1944). Both the original and the new event's registration have their own
+ ticket; this explains, on each, where the money/scholarship/CE live and where
+ attendance + the certificate are earned — so attendee and staff see accurate
+ info on both. Takes `event_registration`. %>
+<% if event_registration.transferred_in? && (source = event_registration.transferred_from_registration) %>
+
+
+
+ You transferred into <%= event_registration.event.title %>. Your payment,
+ scholarship, and continuing-education records stay on your
+ <%= link_to "original registration", registration_ticket_path(source.slug), class: "font-semibold underline" %>
+ — your attendance here and your certificate for these hours are earned at this event.
+
+ You transferred out of <%= event_registration.event.title %> to
+ <%= link_to "your new registration", registration_ticket_path(destination.slug), class: "font-semibold underline" %>.
+ Attend there and earn your certificate at that event; your payment, scholarship, and
+ continuing-education records remain here.
+
+
+<% end %>
diff --git a/app/views/events/callouts/ce.html.erb b/app/views/events/callouts/ce.html.erb
index 585acf77c5..c78c7612fc 100644
--- a/app/views/events/callouts/ce.html.erb
+++ b/app/views/events/callouts/ce.html.erb
@@ -15,6 +15,7 @@
end %>
<%= render layout: "events/callouts/callout_page", locals: { title: @event.ce_hours_label, **callout_eyebrow } do %>
+ <%= render "event_registrations/transfer_notice", event_registration: @event_registration %>
<%# Requesting CE flips this frame in place: the POST redirects back here and
Turbo swaps in the license-entry branch — no full-page reload. %>
<%= turbo_frame_tag "ce_request_section" do %>
diff --git a/app/views/events/callouts/certificate.html.erb b/app/views/events/callouts/certificate.html.erb
index 554f59d221..48d5f9cd15 100644
--- a/app/views/events/callouts/certificate.html.erb
+++ b/app/views/events/callouts/certificate.html.erb
@@ -119,6 +119,7 @@
<% else %>
<%= render layout: "events/callouts/callout_page", locals: { title: "Certificate of completion" } do %>
+ <%= render "event_registrations/transfer_notice", event_registration: @event_registration %>
<%# Not yet unlocked: show each condition and which are met, like the videoconference page. %>
<% ended = @event.end_date&.past? %>
<% attended = @event_registration.attended? %>
diff --git a/app/views/events/callouts/payment.html.erb b/app/views/events/callouts/payment.html.erb
index 7982d6cd43..2bcb42de1d 100644
--- a/app/views/events/callouts/payment.html.erb
+++ b/app/views/events/callouts/payment.html.erb
@@ -1,6 +1,7 @@
<% content_for(:page_bg_class, "public") %>
<% content_for(:page_title, "Payment — #{@event.title}") %>
<%= render layout: "events/callouts/callout_page", locals: { title: "Payment" } do %>
+ <%= render "event_registrations/transfer_notice", event_registration: @event_registration %>
<% if @allocations.any? %>
diff --git a/spec/requests/events/registrations_spec.rb b/spec/requests/events/registrations_spec.rb
index 2db435d453..36d901b1c0 100644
--- a/spec/requests/events/registrations_spec.rb
+++ b/spec/requests/events/registrations_spec.rb
@@ -18,6 +18,27 @@
get registration_ticket_path(registration.slug)
expect(response).to have_http_status(:success)
end
+
+ it "notes a transfer out and links to the new registration's ticket" do
+ destination = create(:event_registration, event: create(:event), registrant: user.person)
+ registration.update!(status: "transferred_out")
+ destination.update!(transferred_from_registration: registration)
+
+ get registration_ticket_path(registration.slug)
+
+ expect(response.body).to include("You transferred out")
+ expect(response.body).to include(registration_ticket_path(destination.slug))
+ end
+
+ it "notes a transfer in and links back to the original registration's ticket" do
+ source = create(:event_registration, event: create(:event), registrant: user.person, status: "transferred_out")
+ registration.update!(transferred_from_registration: source)
+
+ get registration_ticket_path(registration.slug)
+
+ expect(response.body).to include("You transferred into")
+ expect(response.body).to include(registration_ticket_path(source.slug))
+ end
end
context "as an admin" do