Skip to content

feat(invitations): add external ID field, CSV header validation, and multi-language parsing#8427

Draft
LWS49 wants to merge 3 commits into
lws49/feat-ext-id-invite-flowfrom
lws49/feat-invite-header-validation
Draft

feat(invitations): add external ID field, CSV header validation, and multi-language parsing#8427
LWS49 wants to merge 3 commits into
lws49/feat-ext-id-invite-flowfrom
lws49/feat-invite-header-validation

Conversation

@LWS49
Copy link
Copy Markdown
Collaborator

@LWS49 LWS49 commented Jun 3, 2026

Summary

Adds an optional External ID field to the course invitation flow (CSV and individual form), so admins can assign institution identifiers during invite. The invite-by-file path gains strict CSV header validation that rejects headerless or malformed uploads with a descriptive error, and the parser now accepts headers in English, Chinese, or Korean (any mix) regardless of column order - matching whatever template the admin downloaded. The result dialog is redesigned into per-outcome sections (new invitees, existing users, failed rows) with a dedicated conflict prompt when an uploaded External ID collides with one already assigned to a course member. Template files are replaced with six localized variants (EN/ZH/KO × timeline/no-timeline), and the download link resolves to the locale-matched file. The External ID column in the Manage Users table is moved to sit after Email, matching the CSV column order.

Design decisions

  • Order-independent parsing via header_alias_map - rather than enforcing canonical column order, the parser builds a {canonical_symbol => column_index} map from the header row, so any permutation of the expected columns is accepted. This lets admins reorder columns in their own spreadsheet tools without rejection.
  • Set equality for header validation - build_header_map! compares the resolved canonical symbol set against EXPECTED_WITH_TIMELINE / EXPECTED_WITHOUT_TIMELINE using Set#==. Extra unrecognised columns are silently ignored (they produce no entry in the map); missing or entirely wrong headers raise CSV::MalformedCSVError with a localised expected-header list.
  • PendingExternalIdUpdates as a raise-to-surface flow - when an upload would change existing External IDs, the service raises rather than proceeding, and the controller catches it to return a pending_conflict response. The frontend then shows a confirmation prompt before the admin can re-submit with external_id_resolution set.
  • Six static template files instead of one - templates are locale-specific CSV files imported at build time via Vite's ?url suffix. A TEMPLATE_MAP in helper/index.ts maps { locale: { timeline, noTimeline } } to URLs, with EN as fallback for unsupported locales.

Regression prevention

Covers: EN/ZH/KO header acceptance, mixed-language headers, order-independent column parsing, headerless CSV rejection, old Phase 1 header rejection, timeline-format CSV on a no-timeline course, error message content, template guard (each static template file passes the parser), External ID round-trip through CSV and individual form, conflict detection and resolution flow, result dialog per-outcome sections.

Manual testing confirmed: happy path invite by CSV (EN/ZH/KO templates), template download locale matching, External ID conflict prompt, individual invite with External ID, ManageUsersTable column order.

Existing behaviour preserved: courses without External ID enabled are unaffected; rows with no External ID column value are accepted with nil; BOM-prefixed files continue to parse correctly.

@LWS49 LWS49 force-pushed the lws49/feat-invite-header-validation branch from 37a5204 to 211f960 Compare June 3, 2026 08:45
@LWS49 LWS49 changed the title Lws49/feat invite header validation feat(invitations): CSV header validation and ext ID conflict resolution Jun 3, 2026
@LWS49 LWS49 force-pushed the lws49/feat-invite-header-validation branch from 211f960 to 67de552 Compare June 3, 2026 09:38
@LWS49 LWS49 changed the title feat(invitations): CSV header validation and ext ID conflict resolution feat(invitations): add external ID field, CSV header validation, and multi-language parsing Jun 4, 2026
@LWS49 LWS49 force-pushed the lws49/feat-invite-header-validation branch from 1314602 to 121b8a7 Compare June 4, 2026 07:09
@LWS49 LWS49 force-pushed the lws49/feat-ext-id-invite-flow branch 3 times, most recently from 23c59ca to 559264e Compare June 4, 2026 15:16
@LWS49 LWS49 force-pushed the lws49/feat-ext-id-invite-flow branch from 546aed9 to b379882 Compare June 4, 2026 15:24
LWS49 added 3 commits June 4, 2026 23:49
… dialog

- add ext_id to CourseUser and UserInvitation with unique-per-course index
- accept ext_id in bulk CSV and individual invite form; upsert for existing
  records (enrolled users and pending invitations)
- conflicts (duplicate email or ext_id) surface in Failed with reasons
… resolution

- validate CSV column headers explicitly instead of checking column count
- detect external ID changes on existing users/invitations before applying;
  surface a confirmation prompt (Keep Existing / Replace) with a side-by-side
  Current / New External ID table capped at 320px for large uploads
- conflict resolution wired into both file upload and individual invite form;
  file ref preserved on Go Back so admin need not re-select
- add ExternalIdResolution and PendingExternalIdConflict types; update invite
  API and operations layer to detect and return conflict payload
- controller rescues PendingExternalIdUpdates, renders jbuilder partial;
  concern branches on @resolution to populate pending vs updated arrays
- i18n: add EN/KO/ZH translations for conflict prompt and new table columns
@LWS49 LWS49 force-pushed the lws49/feat-invite-header-validation branch from 121b8a7 to b8b6ea2 Compare June 4, 2026 15:51
@LWS49 LWS49 force-pushed the lws49/feat-ext-id-invite-flow branch 2 times, most recently from db90f87 to b761414 Compare June 4, 2026 16:18
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.

1 participant