Skip to content

Track event-reg transfers via a back-link (fix lost attendance on transfer-in) - #2044

Open
maebeale wants to merge 30 commits into
mainfrom
maebeale/issue-1944
Open

Track event-reg transfers via a back-link (fix lost attendance on transfer-in)#2044
maebeale wants to merge 30 commits into
mainfrom
maebeale/issue-1944

Conversation

@maebeale

@maebeale maebeale commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 self-FK + data migration, status-list change, transfer flow, two-record CE split, and financials-on-source semantics across reporting

Closes #1944

What is the goal of this PR and why is this important?

  • Marking a registration transferred_in overwrote its attendance status, losing whether the person actually attended the event they transferred into.
  • Fix: track the transfer as a relationship, not a status. A transferred_from_registration_id self-FK lives on the incoming record; the out stays identifiable by its terminal transferred_out status. Chained transfers form a linked list back to the original.

The transfer flow

  • After a reg is marked Transferred out (from the edit-form save or the inline roster/onboarding Turbo badge), the admin lands on a transfer screen to pick the destination event (same format — on-demand ↔ on-demand, scheduled ↔ scheduled), enforced both in the picker and at the POST endpoint.
  • Double transfers collapse to two live regs. Transferring out a reg that is itself a transfer-in (A→B→C) points the new reg straight at the original source and destroys the middle stop; the deletion is captured in an Ahoy destroy lifecycle log (attributes + associated records).
  • Org links carry forward. The registrant's linked organizations are copied onto the new reg (and kept on the source), so agency context follows them to the new event.
  • Unfinished transfers are flagged. A reg marked transferred-out with no destination recorded yet shows a ⚠ warning on its status badge (roster/onboarding), and reads as the top "Not ready" reason on the readiness Status column, so it can't be lost.
  • Informed consent. The transfer screen spells out, in plain language, exactly what happens to the registrant's data before you confirm — with an extra warning in the chain-collapse (2nd→3rd) case.

Editing & undoing a transfer (Manage-transfer hub)

  • The transfer screen doubles as a "Manage transfer" hub, reached from a "Manage" chip on the roster status badge (amber ⚠ until a destination is set, purple after) and an edit-form link — the status dropdown stays a pure status control.
  • Change destination: re-point a recorded transfer to a different event; the old destination is unlinked and its CE re-merged before re-splitting to the new one.
  • Undo transfer (RevertTransfer): restores the reg to its pre-transfer status; for a completed transfer it also unlinks the destination (it becomes a normal standalone reg, nothing deleted) and re-merges the split CE back onto the source (TransferContinuingEducation#revert).

Money & records resolve to the source registration

  • An incoming reg carries no registration money of its own — balance, payments, invoice, receipt, and scholarship all live on the source (the old event, where they paid).
    • payment_status_label = "Transferred in", readiness stops flagging "Payment due", payment access derives from the source.
    • Balance/receipt/invoice mirror the source: the ticket never re-bills a paid transfer, and the invoice/receipt (reached through the new ticket) document the old event's cost/payments.
    • The roster unpaid filter buckets a transfer by the source's balance.
    • Scholarship creation on a transfer-in is blocked at the URL (redirects to the source), not just hidden.

Continuing education splits into two records (issue #1944)

  • CE is the exception to "money stays on the source": a transfer splits it so each event holds its own.
    • The source keeps a paid, zero-hours stub (cost = amount already paid) — its payments count at the original event and it still shows in that event's CE searches.
    • The destination gets a live record carrying the hours and the outstanding balance, where new payments are received and the certificate is earned. Its cost is snapshotted from the source's balance and admin-locked, with a "from original / paid on original →" link back to the stub.
    • Collapsing a double transfer relocates the middle record forward (not destroyed); transferring back to the origin merges it into the stub.
    • Manual CE creation on a transfer-in is blocked at the URL (the transfer's system-created record is exempt).
    • CE reporting follows the record: the stub counts at the original event, the carried record at the new one.

Transferred-out ticket

  • The original (withdrawn) ticket keeps its financial/credit records (payment/invoice/receipt, scholarship, CE, certificate) but hides participation material (videoconference join, staff, handouts, FAQ, custom callouts).

Reporting

  • Registration totals stay on the original event; the new event excludes transferred-in regs from its registration totals (dashboard billable basis + revenue) and the bulk-payment allocation list — while still counting them in headcount/attendance. CE is counted per-record as above.

Surfaced as a dimension (it's no longer a status)

  • Roster badge "In" marker, dashboard breakdown row (FK-counted) with drill-down, an "Attendance status → Transferred in" filter (roster + reminders), and a CSV "(transferred in)" annotation.

Locking a transferred-out registration

  • A transferred-out reg is historical, so its fields are locked except comments and communications. Enforced server-side (restricted update params + guards on the inline onboarding / certificate / attendance / org-link endpoints and the recipients shout-out), so a locked field is refused with a warning, not silently dropped.
  • Surfaced in the UI: a lock banner + disabled fields on the edit form (disabled <fieldset>s), disabled onboarding/roster inline controls, and hidden "Create user".
  • The scholarship / CE / allocations pages stay reachable but show a caution banner — an admin may still need to edit those deliberately.

Transferred-in editing & the consent screen

  • CE card: when the source had no CE, the transferred-in reg now offers the normal Add CE control (CE decided now attaches to this reg) instead of a dead-end link back to the original.
  • Scholarship card: shown on a transferred-in reg when the source had no scholarship, so an admin can award one here.
  • Financials summary: now also surfaces the source CE payment (No CE / No CE payments / the amount), next to the payment and scholarship it already showed.
  • Consent screen: tightened the per-card copy, added the event's month + year to disambiguate same-titled trainings, and renamed the action to "Perform transfer".

Transferred-out ticket: read-only + banner everywhere

  • The public ticket for a transferred-out reg no longer shows the "Pay with Credit Card" button (join link and manage block were already hidden) — the person is pointed to their new registration instead.
  • The transfer banner (links to the paired registration) is now rendered once from the shared callout wrapper (_callout_page), so it appears on every callout page a transferred in/out registrant can reach, not just the financial ones. The four duplicate explicit renders were removed.

Copilot AI lite review requested due to automatic review settings August 2, 2026 13:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 2, 2026 13:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale marked this pull request as ready for review August 2, 2026 13:34
Copilot AI review requested due to automatic review settings August 2, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale force-pushed the maebeale/issue-1944 branch from abd066d to 3613ba0 Compare August 2, 2026 22:31
Copilot AI review requested due to automatic review settings August 2, 2026 22:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale force-pushed the maebeale/issue-1944 branch from 3613ba0 to b34dcfa Compare August 3, 2026 02:58
Copilot AI review requested due to automatic review settings August 3, 2026 02:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 3, 2026 03:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale force-pushed the maebeale/issue-1944 branch from 51c90b7 to 7711748 Compare August 4, 2026 05:12
Copilot AI review requested due to automatic review settings August 4, 2026 05:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 4, 2026 05:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 10, 2026 03:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

maebeale and others added 13 commits August 18, 2026 07:47
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
A transfer-in is a temporary middle stop: when it's transferred out
again (A→B→C), keep only the endpoints. Point the new reg at the
original source and destroy the middle, so attendance and the transfer
trail stay on the two real registrations. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transfers now match on-demand ↔ on-demand and scheduled ↔ scheduled (was
facilitator-training kind). Picker hint + tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A transferred-in registration carries no money of its own — the balance,
payments, invoice, receipt, and scholarship all live on the source it came
from (the old event, where they actually paid). Route them there so the new
ticket never re-bills a paid transfer, the invoice/receipt document the old
cost, and the roster's unpaid filter buckets a transfer by the source's
balance. Hide participation callouts on a transferred-out ticket (they
withdrew) while keeping their financial/credit records, and block scholarship
creation on a transfer-in at the URL, not just the UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A transfer now keeps two CE records instead of deriving certification across the
link: the source keeps a paid, zero-hours stub (its payments count at the
original event and it still surfaces in that event's CE searches), and the
destination gets a live record carrying the hours and the outstanding balance,
where new payments are received and the certificate is earned. Collapsing a
double transfer relocates the middle record forward instead of destroying it;
transferring back to the origin merges it into the stub. The destination record's
cost is snapshotted from the source's balance and admin-locked, and manual CE
creation on a transfer-in is blocked at the URL. CE reporting follows the record,
so the stub counts at the original event and the carried record at the new one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A transfer keeps the registrant's agency context in both places, so the
new reg should show the same linked orgs as the one they left — copied,
not moved, so the source keeps its own. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reg marked transferred-out but with no destination recorded is a
half-done task that's easy to lose track of; surface a warning triangle
on the badge so an admin can spot and complete it. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transfer screen only vaguely said it "keeps the transfer history."
An admin approving a transfer should know exactly what happens to the
registrant's data first — that money/scholarship stay on the original
event, orgs copy over, and CE hours move. A transfer-in being moved
again also removes the middle registration, so warn about that explicitly
in the chain-collapse case. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reg marked transferred-out with no destination recorded yet is an
unfinished task, but readiness ignored it — a stale "Payment due" could
mask it. Make it the highest-priority pre-event issue so the roster
Status badge and readiness filter surface it. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transfers get done wrong, and there was no way to fix one. The interstitial
becomes a "Manage transfer" hub reached from a roster status-badge chip (amber
when a destination is still missing, purple once set) and an edit-form link,
keeping the status dropdown a pure status control. From the hub an admin can
change the destination event or undo the transfer entirely.

Undo restores the reg to its pre-transfer status; for a completed transfer it
also unlinks the destination (it becomes a normal standalone reg, nothing
deleted) and re-merges the split CE back onto the source. Re-pointing to a new
event reuses the same revert step before re-splitting. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirms RevertTransfer re-merges a relocated CE record back onto the origin
when undoing an A→B→C transfer that collapsed to two regs — origin restored,
destination unlinked, no orphaned hours. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/issue-1944 branch from 71b6003 to a776f29 Compare August 18, 2026 11:50
Copilot AI review requested due to automatic review settings August 18, 2026 11:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

maebeale and others added 5 commits August 18, 2026 08:04
Shorter, plainer bullets read more clearly for a low-tech admin. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two side-by-side columns (this registration vs. the new one) read more
clearly than a flat list. Also surface a "Manage transfer" link on the
transferred-in reg's trail so the hub is reachable from either end. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carry days attended, expected payment method, buddy-pay, and the
recipients-page shout-out flag onto the new reg so it reflects where they
left off (money and CE resolve separately). Note the carried days under the
new reg's attendance checkboxes ("Day 1 completed in prior training"). (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dashboard shout-outs already scope to active_registrations, so a
transferred-out (or no-show/cancelled) reg's shout-out never shows on the
recipients page. With shout-out now copied forward, it surfaces on the new
active reg instead. Add a test pinning the exclusion. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A transferred-out reg is historical: its status, attendance, financials,
linked orgs, and shout-out shouldn't change here — only comments and
communications stay open, and undo runs through the Manage-transfer flow, not
by editing fields. Enforce this server-side (restricted update params + action
guards on the inline onboarding/certificate/attendance/org-link endpoints and
the recipients shout-out) so a locked field is refused with a warning rather
than silently dropped, and surface it in the UI: a lock banner and disabled
fields on the edit form, disabled onboarding/roster inline controls. The
scholarship, CE, and allocations pages stay reachable but show a caution banner,
since an admin may still need to edit those deliberately.

Also tighten the transfer interstitial consent copy, show the event's month and
year to disambiguate same-titled trainings, rename the action to "Perform
transfer", and fix the transferred-in CE card to offer the normal Add-CE
control when the source had no CE (so CE decided now attaches to this reg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 13:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…in reg

A transferred-in reg's financials summary showed only the source's payment and
scholarship, but the CE payment stays on the source too — so surface it (No CE /
No CE payments / the amount), alongside the payment and scholarship it already
shows. And when the source had no scholarship, show this reg's own editable
scholarship card so an admin can award one here: a scholarship decided now
belongs to this registration, the same way a CE decided now does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

So facilitators/admins can see the transfer capability on /features once
an admin syncs the catalog. (#1944)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…every callout page

A transferred-out registrant should be pointed to their new registration, not
transacting on the old ticket. Hide the "Pay with Credit Card" button (and its
"access granted" pill) for a transferred-out reg — the join link and manage
block were already hidden, this closes the last interactive gap; the new-reg
banner already links onward.

Also consolidate the transfer banner into the shared callout wrapper
(`_callout_page`, guarded on @event_registration) so it appears at the top of
every callout page a transferred in/out registrant can reach, and delete the
four now-duplicate explicit renders (payment/scholarship/ce/certificate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event Reg - transferred in / transferred out

3 participants