IN-PROGRESS: CE cutover: reroute intake/display, drop ce_* columns (PR 2)#1917
IN-PROGRESS: CE cutover: reroute intake/display, drop ce_* columns (PR 2)#1917maebeale wants to merge 46 commits into
Conversation
| # removes any CE registrations; otherwise the registrant's (single) CE | ||
| # registration is found-or-built against the licence for the typed number, with | ||
| # the editable hours applied. | ||
| def reconcile_ce_registration |
There was a problem hiding this comment.
🤖 From Claude: The admin CE section posts under a separate ce namespace and is reconciled here rather than via accepts_nested_attributes_for. Editing a license number through the CE registration means touching a separate ProfessionalLicense (find-or-create), which nested attributes handle fragilely; this keeps it explicit and covers the common single-license case (multi-license is managed elsewhere).
| # Read CE registrations from the in-memory collection rather than the DB when | ||
| # it's already loaded or this registration isn't persisted (e.g. the unsaved | ||
| # sample-ticket preview builds CE registrations without saving). | ||
| def ce_registrations_in_memory? |
There was a problem hiding this comment.
🤖 From Claude: The aggregators read the in-memory collection when the registration is a new_record? so the unsaved sample-ticket preview (which builds a CE registration without saving) still renders a populated CE card — otherwise exists? would query the DB with a nil id and report no CE.
| } | ||
| # Filter by CE state. "needs_license" is derived (a CE registration sits on a | ||
| # placeholder license); the rest match a ContinuingEducationRegistration#status. | ||
| scope :ce_status, ->(value) { |
There was a problem hiding this comment.
🤖 From Claude: needs_license is derived (a CE registration on a placeholder license, number IS NULL); the other options match a ContinuingEducationRegistration#status. EXISTS subqueries so a registration with several CE registrations matches if any one qualifies.
| # registrant's (first) CE registration via a found-or-created license, mirrors | ||
| # it onto the registration's form answer, then returns to the callout. Plain | ||
| # full-page POST — no Turbo. | ||
| def update_ce_license |
There was a problem hiding this comment.
🤖 From Claude: Public license entry, authorized by the registration slug (the same bearer-token model as the other callouts). Plain full-page POST (no Turbo) — it sets the number on the first CE registration via find-or-create and mirrors it onto the form answer.
a752773 to
043d5ae
Compare
bc77080 to
a96a90d
Compare
1c1c1b4 to
97c480c
Compare
abaee56 to
a7e7d3f
Compare
326da62 to
7aef3fa
Compare
d596879 to
d16bc05
Compare
8deeb08 to
4db3dc2
Compare
…umns Completes the continuing-education cutover started by the foundation models (#1916). Reroutes registration intake, the public callout, and every read site (callouts, onboarding, reminders, CSV) off the flat EventRegistration#ce_* columns and onto ContinuingEducationRegistration / ProfessionalLicense, then drops the ce_credit_requested / ce_hours_requested / ce_license_number columns. Reconciled with #1833 (registrants-index CE column + filter), which merged to main while this branch was in flight and built the same scaffolding on the old columns: - Kept #1833's scaffolding (CE column, dropdown filter, column toggles, layout, onboarding columns, bulk-reminder filter, CSV columns, eyebrow nav) and repointed every CE data read to the new models. - EventRegistration.ce_status is now a derived scope over the CE registrations (needs_license / requested / paid / issued / not_issued), replacing the old ce_credit_requested-based buckets; the roster + reminder dropdowns follow it. - EventRegistration#ce_status_label and the aggregators (ce_requested?, ce_hours_total, ce_amount_owed_cents, ce_license_provided?, ce_paid_in_full?) read the new models; the registrants column gates on a CE record existing rather than the dropped boolean (the "requested, no record yet" state is gone now that intake creates the record). - ReminderRecipientFilter's CE matching flows through the shared ce_status scope (its per-record matchers were redundant with main's scope approach). Migration timestamped to run after the foundation migrations now in main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make continuing education mirror scholarships: - Add a stored ce_requested flag on event_registration (joins the other *_requested flags); intake sets it; the registration edit form drives it. - The CE card shows a Requested toggle only when no CE registration exists; saving it on creates the record (against the selected/only license, else a placeholder), with a flash. Once a record exists the card shows a summary + Issued/Not-issued pill + an Edit link, gated on the event being ce_eligible?. - New ContinuingEducationRegistrationsController + policy + edit page: edit license (promoting a placeholder in place), hours, cost; a Certificate issued toggle (certificate_sent_at); and removal guarded against payments. - Rename the derived ce_requested? (record-exists) to ce_registered? to free the name for the column; readers/views key off whichever they mean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These configure whether an event grants CE and at what cost, so they belong with the event's details rather than buried in the registration-ticket callouts panel. The CE-hours ticket card (its label + details text) stays in the callouts panel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CE card always has three rows (cost·hrs / License / chip) but the scholarship card showed its funder line only when a grant existed, so its status chip sat a row higher and the two orange chips didn't line up. Always render the scholarship card's middle row — "Grantor: <name>" when funded, an empty same-height spacer (padding) when not — so both chips align. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single-license registrant was still shown a one-option license dropdown. Only render the selector when the registrant has more than one license on file; with one the controller uses it, with none it creates an empty (placeholder) license to fill in later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fter The create state is now just the Requested toggle. Saving creates the CE registration against the registrant's existing license (or an empty placeholder), and the selected license is shown in the card summary and editable on the CE registration's own page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reuse (and simplify) the ce-credit-requested Stimulus controller so the CE Requested toggle behaves like scholarship's: the track turns amber while the choice is pending, teal once stored on, gray when off — and the whole card gets a teal ring while it's on. Drops the controller's dead targets (license/ hours/details fields were removed earlier), so it's no longer orphaned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap it in the continuing-education-themed (teal) outer card, use the shared centered event page header (icon + title + event + registrant), and split the fields, certificate, and removal into teal section cards with icon chips — mirroring the scholarship edit layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Expires date field was hand-rolled with f.label + f.date_field while the sibling fields use simple_form (wrapper: false), so its label weight and the raw native date input didn't match the rest of the row. Render it as an HTML5 date input via simple_form so the label and control line up with the others. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CE callout (registrant-facing) and CE registration edit (admin) pages only captured the license type and number, while the person edit form captures the full license (type, number, issuing state, expiry). Surface the same two extra fields on both CE surfaces so a registrant or admin can complete the license in the place they're already working, instead of bouncing to the person page. assign_license now persists issuing_state and expires_on alongside number/kind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add scholarship link was fuchsia/text-sm; recolor to grey/text-xs so it matches the sibling Connect organization prompt. - The CE card's empty (toggle-only) state had no bottom prompt, leaving it barren next to the other two — add a grey hint pinned to the bottom. CE has no separate award page (saving with Requested on creates the record), so it's a hint rather than a link. - The CE box no longer needs JS: drop the ce-credit-requested Stimulus controller (and its index.js registration / AGENTS.md count) along with the data-controller wiring, and recolor the Requested toggle's checked state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add CE registration flow (mirrors scholarship's new/create): - New/create actions + routes + policy; a "+ Add CE registration" link on the registration card opens a prefilled new form in a new tab and returns to the registration. The Requested toggle still auto-creates a stub on save; this is the alternative where the admin fills license/hours/cost up front. - Extract the CE-details form fields into a shared _details_section partial used by both new and edit. Universal number formatter: - Replace ContinuingEducationRegistration.format_hours (CE-specific) and the EventsHelper#ce_hours_display wrapper with a NumberFormatter PORO + a generic plain_number helper, mirroring MoneyFormatter/dollars_from_cents. Any model, service, or view can now format a trailing-zero-free number one way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the registrant already holds licenses, the new/edit CE form now shows a License dropdown: pick an existing one to use it as-is, or "Create new license" to add one from the typed fields. Plain server-rendered select (no JS), so the fields stay the source of truth for a new/edited license while the dropdown only chooses which record to write — a CE registration still never exists without a license. With no license on file there's no picker and the fields create the first one, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, atomic create - Lay the four license fields in a single 4-col grid row (widen the form to max-w-3xl) so Expires no longer wraps; mark Issuing state + Expires "(optional)". - Make create atomic: a brand-new license is a build that persists with the CE registration in one transaction, so opening (then abandoning) the new form never leaves a stray placeholder license, and a failed save rolls back both. update is likewise transactional. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registrants index, CE callout, and CE card each computed their own CE status pill with drifting labels and colors. Consolidate onto one decorator method so the lifecycle reads identically everywhere: Requested -> License # needed -> $X due -> Pending -> Issued, with Pending blue, Issued green, and every actionable/in-progress state amber. The "$X due" state shows the real outstanding balance (cost net of payments), so it persists until paid rather than flipping on the first dollar. ?admin=true on the callout previews the post-payment Pending state without recording a payment, gated on edit access. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uested controller Color each card's section icon via section_icon_class(domain, active?) so a card with content reads as filled and an empty one stays muted — applied across the organizations, scholarship, shout-out, comments, payments, and communications cards. Drop the scholarship-requested Stimulus controller in favor of a plain peer-checked toggle, and align card chips/min-heights so the cards line up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selecting a license in the CE form's dropdown now loads that license's type, number, state, and expiry into the fields (a ce-license-picker Stimulus controller reads each option's data attributes); "Create new license" clears them. Saving therefore edits whichever license is selected in place, so assign_license now writes the typed fields onto the picked existing license rather than just linking to it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cross-links Selecting a license in the CE picker now populates the type/number/state/expiry fields from that license (new ce-license-picker Stimulus controller), so the fields always describe the selected license — and saving corrects that license in place rather than only switching to it. Mirrors the CE edit↔callout pattern onto scholarships: an admin-only sky jump-link each way, plus a return_to=scholarship eyebrow so an admin lands back on the edit page (registrants keep the ticket). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The free-text credentials field is no longer edited (form input removed) or displayed — the profile credential suffix now derives from professional-license types via Person#license_credentials. Drop the column and its permit param; the separate profile_show_credentials toggle is kept. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commenting on every push produced noise on routine changes. Reserve inline diff comments for things a reviewer genuinely needs flagged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public license POST stripped the number twice — once inside assign_license and again to feed record_ce_license_answer. assign_license already normalizes it, so read the saved value back instead of re-stripping the raw param. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It was added in the CE cutover but missing from the controller list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the public registration form's state field on both the CE callout and the person license fields, and mark issuing state / expiry optional. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Issuing state is now a US-states <select> (shared us_states helper, blank option since it's optional), matching the address form, instead of free text. - Grey the Expires native date input's "mm/dd/yyyy" placeholder while empty via a small reusable date-placeholder Stimulus controller — native date inputs have no ::placeholder and render it in the input's text colour, and CSS can't tell an empty optional date from a filled one, so it toggles a grey class on value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The approximate file counts had drifted repo-wide (views ~507→~632, controllers ~71→~77, specs, etc.). True them up to actual, bump the Stimulus controller count to 74, and list the four previously-omitted rake tasks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tighten license management: a license can be removed only when it has no CE registrations at all (not just no paid ones), and add ProfessionalLicensePolicy so a license is editable by an admin or its holder, but locks to admins once any CE registration exists — a registrant must not alter the credentials a CE certificate was issued under. The person form shows CE-tied licenses read-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop submitted changes to a CE-tied license that the current user isn't allowed to edit/destroy (a non-admin owner), so a crafted request can't bypass the read-only view. No-op for admins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The person form is admin-only today, so the server-side backstop in PeopleController#update is dormant. Stub PersonPolicy to simulate the future owner-access state and prove the guard holds against the real ProfessionalLicensePolicy: CE-tied license edits/deletes are dropped while an unlocked license still updates — so flipping PersonPolicy later is safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ional timezone hint
- License picker shows only when the registrant holds more than one license; with
zero or one, the fields edit/create it directly (no picker).
- Identify a license by kind + number, not number alone: widen the per-person
unique index to (kind, number) via migration, update the model uniqueness scope,
and match on kind + number in assign_license. So "LMFT 12345" and "LCSW 12345"
are distinct licenses.
- Timezone hint is now second-person ("You will see…") when you're editing your own
settings and third-person ("User will see…") when an admin edits someone else.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the idempotency guards and explicit up/down for a plain reversible change — the index swap is trivially invertible and the local DB is disposable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…suing state The registration org card's title wraps to two lines, making its header taller than the scholarship card's, so its first chip sat below the "$100"; nudge the chips up by that one-line delta to line them up. Bring the admin CE details issuing-state field in line with the callout and person forms — a US-states dropdown instead of a free-text box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…indexes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After issuance the license is the credential the certificate was issued under, so the registrant-facing CE callout shows it read-only (no Edit link, lock note) and update_ce_license refuses changes. Admins can still correct it on the admin CE edit page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bundler-audit (scan_ruby) started failing on freshly-published advisories: css_parser 2.2.0 (CVE-2026-53727, SSRF/LFD) → 3.0.0, and msgpack 1.8.0 (CVE-2026-54522, use-after-free) → 1.8.3. Both are transitive (premailer, bootsnap) with permissive constraints. Mailer specs pass with css_parser 3.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4db3dc2 to
0b29237
Compare
🤖 PR, suggested 👤 review level: 🔬 Inspect — reroutes CE intake/display onto the new models, drops the old
ce_*/credentialscolumns, and adds the admin CE management + professional-license surfacesWhat is the goal of this PR and why is this important?
#1916 added
ProfessionalLicense+ContinuingEducationRegistrationbut left intake, the callout, and read sites on the flatEventRegistration#ce_*columns. This PR completes the cutover, drops those columns, and builds out the admin + registrant surfaces for managing CE and professional licenses.Cutover (intake, callout, read sites)
(optional)markers, greyed placeholders.#ce_hourspage gated once_eligible?.EventRegistrationaggregators —ce_requested?,ce_hours_total,ce_amount_owed_cents,ce_license_provided?,ce_paid_in_full?,ce_status_labelover the CE registrations;ce_statusfilter scope.ce_credit_requested/ce_hours_requested/ce_license_numbercolumns (+ the legacycredentialscolumn on people) and dead methods.Admin CE management
ContinuingEducationRegistrationsController+ policy): license type/number/issuing-state/expiry, hours + cost (prefilled from the event), certificate issued toggle, and delete.Professional licenses on the person form + edit gating
ProfessionalLicense: type, number, issuing-state, expiry) are managed on the person form; uniqueness scoped to(person, kind, number).PersonPolicyflip is the only remaining step): a license is editable by an admin, or by its holder only while it has no CE registrations; once any CE reg exists it locks to admins — rendered read-only with admin-only styling. Enforced in three layers —ProfessionalLicensePolicy, the view, and a server-side backstop inPeopleController#update.Registration edit + card polish
section_icon_class); "Requested" toggles are pure-CSS in their theme colours.ce_license_picker,date_placeholder; removedce_credit_requested,scholarship_requested(folded into CSS).Migrations
Four, timestamped to run after the foundation migrations now in
main: addce_requested, remove thece_*columns, removecredentialsfrom people, and scope license uniqueness tokind. All CE/credentials data is test data, so this is a clean cutover (no backfill).Reconciliation with #1833 (merged to main mid-flight)
#1833 built the registrants-index scaffolding on the old columns. This PR keeps that scaffolding and repoints the data:
ce_statusis now a derived scope over the CE registrations (needs_license / requested / paid / issued / not_issued); the roster column gates on a CE record existing;ReminderRecipientFilterCE matching flows through the shared scope.