Skip to content

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

Open
maebeale wants to merge 16 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 16 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).

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.

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.

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 11 commits August 15, 2026 11:10
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
A facilitator training only transfers to another facilitator training, and a
non-training only to another non-training, so the destination picker matches
the source event's facilitator_training flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integrates with main's dashboard status breakdown (#2041): transferred_in is no
longer an attendance status, so its row raised KeyError on
ATTENDANCE_STATUS_LABELS.fetch and drilled into an empty roster filter. Remove
the row and update the status-based specs accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transferred-in is now an FK-backed dimension (an incoming reg keeps its own
attendance status), so designate it wherever status is shown:
- roster badge gains an "In" marker alongside the real status
- dashboard attendance breakdown regains a transferred-in row, counted via the
  transfer link with a working drill-down
- the attendance-status filter (roster + reminder recipients) offers a
  "Transferred in" option, routed through the attendance_status scope to the FK
- CSV exports annotate the Status column with "(transferred in)"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An incoming registration's scholarship/CE/payments stay on the source reg it
transferred from. This makes that concrete across the app:

- Transfer follow-up now fires from every status-change entry point — the inline
  roster/onboarding Turbo badge, not just the edit-form save.
- The edit form shows a distinct, read-only "financials on the original
  registration" summary (payment/scholarship/CE) that links back to the source
  reg's section anchors, instead of its own editable cards. Added anchor ids to
  the scholarship and CE cards.
- A transferred-in reg no longer reads as owing this event: payment_status_label
  says "Transferred in", readiness stops flagging "Payment due", and payment
  access derives from the source.
- Reporting excludes transferred-in regs from the new event's financial totals
  (dashboard billable basis + revenue figures, kept in parity) and from the bulk
  payment allocation list, while still counting them in headcount/attendance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A transferred-in reg's scholarship lives on the source, so the intended event
never showed them as a recipient. Add EventRegistration#effective_scholarship /
the dashboard's recipient recognition — scholarship_by_recipient, the recipient
count, and the recipients page — so they "get the hat" while the dollars stay on
the source (financial totals unchanged). The roster badge and the edit-form
summary now designate them a recipient and link to the actual award record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Credit follows the person: a CE registration's hours are certified at the event
its home registration transferred to (else its own event), while the record +
payment stay on the source. Derive it from the transfer FK instead of a stored
column — certified_at_registration = event_registration.transferred_to_registration
|| event_registration; certified_ce_registrations is a has_many through the
transfer link. Certificate issuance and certificate_available? operate on the CE
a reg actually certifies (own + transferred-in), so the intended event can issue
the earned hours and the source no longer issues transferred-out hours. The
source CE card notes where the hours are certified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
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>
Copilot AI review requested due to automatic review settings August 15, 2026 15:23
@maebeale
maebeale force-pushed the maebeale/issue-1944 branch from 1d3ed18 to 313579e Compare August 15, 2026 15: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.

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>
Copilot AI review requested due to automatic review settings August 16, 2026 11:42

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.

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>
Copilot AI review requested due to automatic review settings August 16, 2026 16:29

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.

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>
Copilot AI review requested due to automatic review settings August 16, 2026 19: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.

WHERE events.id = billing_reg.event_id
), 0)
SQL
scope :paid_in_full, -> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 From Claude: These shared paid_in_full/not_paid_in_full scopes are also used by the revenue drilldown, so a transferred-in reg now appears under paid/unpaid there based on its source's balance (money totals are unaffected — they come from the billable basis, which still excludes transfer-ins).

maebeale and others added 2 commits August 16, 2026 20:44
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>
Copilot AI review requested due to automatic review settings August 17, 2026 01:17

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.

# a no-show — including a transferred-out reg's paid stub (counted here, where it
# was paid) and a transferred-in reg's own carried record (counted at the event it
# now credits). (#1944)
def ce_basis_registration_ids

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 From Claude: Deliberate departure from the registration-money model: CE is counted per record on any non-cancelled/no-show reg of the event, so a transferred-out reg's paid stub counts here and a transfer-in's carried record counts at the destination. That's why CE uses this basis (and its own registrant map) instead of the not_transferred_in billable basis registration money uses — no double count, since the stub holds only the paid portion and the carried record the balance.

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

2 participants