Skip to content

Fix verified enrollment for program-as-course hierarchy#3100

Merged
ChristopherChudzicki merged 19 commits into
mainfrom
cc/10649-fix-verified-enrollments
Mar 25, 2026
Merged

Fix verified enrollment for program-as-course hierarchy#3100
ChristopherChudzicki merged 19 commits into
mainfrom
cc/10649-fix-verified-enrollments

Conversation

@ChristopherChudzicki
Copy link
Copy Markdown
Contributor

@ChristopherChudzicki ChristopherChudzicki commented Mar 25, 2026

What are the relevant tickets?

Description (What does it do?)

Fixes verified enrollment for courses nested inside a "program-as-course" (aka courselike program, crogram).

Problem: When a user has a verified enrollment in a grandparent program (e.g., program-v1:UAI+B2C) but no enrollment yet in a child courselike program (program-v1:UAI+B2C.1), clicking "Start Course" on a grandchild module was opening the enrollment dialog instead of using the verified enrollment API. The backend needs both the parent and grandparent program IDs to create the enrollment chain.

Solution:

  • Updated the verifiedProgramEnrollments API call to use the new signature (request_body: string[] instead of program_id), per Release 1.143.2 mitxonline#3420
  • ProgramAsCourseCard now accepts an optional ancestorProgramEnrollment prop (the grandparent's enrollment) and assembles parentProgramIds from courseProgram.readable_id + the ancestor's readable_id
  • ModuleCard receives simple parentProgramIds: string[] and useVerifiedEnrollment: boolean — it doesn't need to know about enrollment objects
  • EnrollmentDisplay passes the grandparent's enrollment as ancestorProgramEnrollment on the program dashboard

Also includes:

  • Phase 1 dashboard card refactor from #10521 (ProgramAsCourseCard, ModuleCard, ProgressBadge, helpers)
  • Deduplicated render paths in EnrollmentExpandCollapse via renderResource extraction
  • Replaced local EnrollmentMode constants with isVerifiedEnrollmentMode helper across both ModuleCard and DashboardCard
  • Test quality improvements: removed hardcoded IDs/titles, use invariant() instead of non-null assertions, minimal factory overrides

How can this be tested?

  1. Set up a UAI-style program hierarchy: grandparent program → courselike child programs → module courses
  2. Create a verified enrollment in the grandparent program only (e.g., via django admin or via a fake cybersource credit card on the program page)
  3. Navigate to the program dashboard for the grandparent
  4. Click "Start Course" on a module inside a courselike program
  5. You should get an enrollment in the course and the parent program, both should be verified enrollments (check in django admin or api response)

ChristopherChudzicki and others added 17 commits March 25, 2026 10:39
The API changed from program_id path param to request_body array.
Update DashboardCard and the enrollment test to use the new signature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…archy

ModuleCard now accepts ancestorPrograms (array of {readable_id, enrollment_mode})
instead of a single programEnrollment. When any ancestor has verified
enrollment_mode, it calls createVerifiedProgramEnrollment with all ancestor
readable_ids. This enables verified enrollment for courses nested inside a
program-as-course whose grandparent program has the verified enrollment.

ProgramAsCourseCard passes ancestorPrograms through to ModuleCard.
EnrollmentDisplay assembles the array: home dashboard passes the parent
program-as-course; program dashboard passes both parent and grandparent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e, fix legacy test

- Extract renderResource() in EnrollmentExpandCollapse to eliminate
  duplicated map callback between shown/hidden resource lists. This also
  fixes the missing ancestorPrograms prop in the hidden resources path.
- Move AncestorProgram type from ModuleCard to helpers.ts (shared domain
  concept, not card-specific).
- Fix DashboardCard.test.tsx verified enrollment URL to match new API
  signature (courserun_id only, no program_id in path).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use factory defaults instead of hardcoded titles and spreading
throwaway factory instances. Assert on the factory-generated values
instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hand-rolled EnrollmentMode const in ModuleCard with
EnrollmentModeEnum from @mitodl/mitxonline-api-axios. Use the same
type for AncestorProgram.enrollment_mode in helpers.ts for type safety.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d verified flag

Replace ancestorPrograms array with a cleaner design:
- ProgramAsCourseCard accepts optional ancestorProgramEnrollment (the
  grandparent enrollment, singular) and assembles parentProgramIds +
  useVerifiedEnrollment from courseProgram.readable_id + ancestor
- ModuleCard accepts simple parentProgramIds (string[]) and
  useVerifiedEnrollment (boolean) — no enrollment objects needed
- Remove AncestorProgram type from helpers.ts (no longer needed)

This fixes the bug where ancestorPrograms was only populated when
courseProgramEnrollment existed — the exact case we don't need it.
Now the parent readable_id always comes from courseProgram (the program
detail object), which exists regardless of enrollment status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-generate program and module IDs inside setupCardData instead of
requiring callers to pass arbitrary values. Assert on factory-generated
values (cardData.courseProgram.title, cardData.moduleCourses[0].title)
instead of hardcoded strings. Add docstring to setupCardData.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eq_tree

The test verifies display order follows req_tree, not the moduleCourses
array order. Reversing the moduleCourses input is simpler and more
directly tests the behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…des, use invariant

- Add integration test for grandparent verified enrollment flowing
  through EnrollmentDisplay -> ProgramAsCourseCard -> ModuleCard,
  asserting both parent and grandparent readable_ids in POST body
- Split verified enrollment test into two: regular course (one program
  ID) and program-as-course module (two program IDs)
- Extract setupProgramDashboardVerifiedEnrollmentScenario helper (API
  setup only, no render)
- Remove unnecessary grades: [] factory override in ProgramAsCourseCard
  tests
- Remove hardcoded IDs and titles in EnrollmentDisplay verified
  enrollment test
- Replace card\! non-null assertions with invariant() for clearer
  failure messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace local EnrollmentMode constant with the shared
isVerifiedEnrollmentMode helper from @/common/mitxonline, consistent
with ModuleCard and ProgramAsCourseCard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 14:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes one-click verified enrollment behavior for “program-as-course” hierarchies on the dashboard by ensuring the verified enrollment API receives the full parent/ancestor program chain, and refactors related dashboard card plumbing/tests accordingly.

Changes:

  • Pass ancestor program enrollment context down to program-as-course module cards and convert it into parentProgramIds + useVerifiedEnrollment.
  • Update verified program enrollment API usage to the new signature (request_body: string[]) and new URL shape (courserun-only).
  • Refactor dashboard rendering paths and strengthen tests around verified enrollment scenarios.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
yarn.lock Switches @mitodl/mitxonline-api-axios resolution to a GitHub tarball source.
frontends/main/package.json Pins @mitodl/mitxonline-api-axios to a GitHub tarball URL.
frontends/api/package.json Pins @mitodl/mitxonline-api-axios to a GitHub tarball URL.
frontends/api/src/mitxonline/test-utils/urls.ts Updates verified enrollment endpoint helper to courserun-only URL.
frontends/main/src/common/mitxonline.ts Adds isVerifiedEnrollmentMode helper.
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.tsx Adds ancestor enrollment support and passes parentProgramIds/useVerifiedEnrollment to module cards.
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ModuleCard.tsx Switches verified enrollment to request_body: string[] and threads new props through enrollment handler.
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx Passes ancestor enrollment on program dashboard and refactors rendering; updates module-id derivation.
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx Updates verified enrollment mutation to the new request shape; uses isVerifiedEnrollmentMode.
frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/*.test.tsx Updates/extends tests for verified enrollment chain behavior and removes hardcoded test data.

Comment thread frontends/main/package.json Outdated
Comment thread frontends/api/package.json Outdated
@gumaerc gumaerc self-assigned this Mar 25, 2026
Copy link
Copy Markdown
Contributor

@gumaerc gumaerc left a comment

Choose a reason for hiding this comment

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

I was able to create a verified program enrollment and subsequently enroll in modules in my "course-like-program." I didn't notice anything super worrying in the code, just a few small things we could look at before merging. One of them is not included in your changes, but maybe something we should change. In the enrollment hooks at frontends/api/src/mitxonline/hooks/enrollment.index.ts we have:

const useCreateVerifiedProgramEnrollment = () => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: (
      opts: VerifiedProgramEnrollmentsApiVerifiedProgramEnrollmentsCreateRequest,
    ) => verifiedProgramEnrollmentsApi.verifiedProgramEnrollmentsCreate(opts),
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: enrollmentKeys.courseRunEnrollmentsList(),
      })
    },
  })
}

This only invalidates the course run enrollments list, but now that we're displaying programs as part of other programs, maybe we should invalidate the program enrollment list as well?

We could also address the linting warning below:

enabled: Boolean(enrolledInProgram && requiredProgramIds.length > 0),
})

const requiredProgramList = requiredPrograms?.results ?? []
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.

The warnings below don't seem that critical, but could cause unnecessary re-rendering so maybe we should memoize requiredProgramList?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Honestly, I think this is more an indicator that we were over-eager in using useMemo and useCallback. They generally aren't relevant unless you have child components using React.memo, which ours usually don't.

@ChristopherChudzicki ChristopherChudzicki merged commit ecf1808 into main Mar 25, 2026
14 checks passed
@ChristopherChudzicki ChristopherChudzicki deleted the cc/10649-fix-verified-enrollments branch March 25, 2026 17:34
@odlbot odlbot mentioned this pull request Mar 25, 2026
6 tasks
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.

3 participants