Guard course_welcome email against re-welcoming students with course history#8046
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds PHPUnit coverage to capture the currently-broken behavior where the course_welcome email is re-sent when an enrolment transition re-fires for a student who already has progress/completion history in the course. This is a tests-only PR intended to go red until the production guard is implemented.
Changes:
- Add a failing test asserting no welcome email is sent for a student who already completed the course.
- Add a failing test asserting no welcome email is sent for a student who already completed a lesson within the course.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the per-lesson get_completed_lesson_ids() loop with a single user_started_lesson_count() query, avoiding N+1 on large courses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persist a per-student/per-course "welcome sent" flag as user meta so the Course Welcome email is never sent twice when the enrolment status event re-fires (enrol/unenrol/re-enrol, recalculation). The flag is read and written independently of HPPS, so it behaves identically in every storage mode. Add an upgrade backfill job that marks all existing course-progress relationships as already welcomed (enumerated through the resolved course progress repository, so it works for both comments and custom-tables backends). This ensures existing students are never (re-)welcomed after upgrade. Register the new user meta for uninstall cleanup, and also clean up the previously-omitted sensei_email_unsubscribed_* user meta. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDPuYvcqYgz4fHN44YxaWp
The upgrade gate compared against 4.27.0 while this ships as a 4.26.2 patch, so the backfill would re-enqueue on every upgrade until a 4.27.0 release. Retarget the method, gate, and tests to 4.26.2. The welcome-sent user meta was stored unprefixed, so on multisite the same course ID would collide across sites and suppress a legitimate welcome. Blog-prefix the key to match course enrolment results storage, and update the data cleaner pattern and tests accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enabling the welcome email does not send to existing students by itself; a later enrolment transition would. Reword the backfill docblock to match. Collapse the two course-welcome changelog entries into one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop unused closure params and use pre-increment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avoids a new PossiblyNullArgument that pushed the file past its Psalm baseline occurrence count. Behavior-neutral: is_upgrade already implies a stored version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test presence rather than truthiness so a pre-existing falsy value is never overwritten. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the bespoke tables-mode test that manually created the schema and forced the tables repository. The backfill behaves identically across backends, so follow the grading-suite idiom: run the existing tests against comments normally and against tables under the ENABLE_HPPS run. Add maybe_enable_hpps_tables_repository()/maybe_reset_hpps_repository() wrappers to the HPPS helper trait so the is_hpps_tables_mode() guard isn't duplicated at each call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow testMethodName_Conditions_Expectation per the Sensei unit-test conventions (senseip2, 2022): drop the "When" prefix from the conditions segment for the tests added in this PR (enqueue, run_batch, and the Course_Welcome guard/flag tests). Pre-existing tests are left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude Please review. |
|
The overall approach is sound — meta-flag primary guard paired with the history-guard fallback and async backfill is a sensible layered design. A few things to address before merging: Convention: short-array syntax in two test methods Consistency: In return (bool) get_user_meta( $student_id, self::get_welcome_sent_meta_key( $course_id ), true );In the backfill: if ( ! metadata_exists( 'user', $user_id, $meta_key ) ) {Both check "does the flag exist", but Design note: flag set unconditionally after |
…lag-set Use metadata_exists in has_welcome_email_been_sent to match the backfill's existence check, so a flag stored with a falsy value still suppresses re-sends. Convert the two remaining short-array test methods to array() and add an inline note explaining why the sent flag is set regardless of delivery outcome. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @claude — all three addressed in f0d4f8f:
|
|
Confirmed all three look good:
The rest of the diff also looks correct: the backfill guard against re-runs ( |
Fixes Automattic/sensei-pro#2633
Problem
The
course_welcomeemail fires on thenot-enrolled → enrolledtransition. When the enrolment provider set changes (e.g. de/reactivating WC Paid Courses), Sensei runs a site-wide enrolment recalculation that un-enrols and re-enrols provider-only students — re-firing the transition and resending the welcome, in bulk on paid sites.Change
Test plan
Requires a split install: Sensei LMS and Sensei Pro (WC Paid Courses) active as separate plugins, plus WooCommerce (deactivating WC Paid Courses must leave Sensei LMS active — on the all-in-one bundle it deactivates Sensei too and does not reproduce).
sensei_calculate_learner_enrolments; wait for a recent one under Complete.sensei_calculate_learner_enrolmentsComplete after reactivation.Known limitation
The backfill is async. Until it finishes after upgrade, a provider change that re-fires a transition could re-welcome a not-yet-processed existing student. Bounded by backfill duration; engaged students still covered by the history guard.