From ab59a0faacd58515e1b58fc738d8e5d4da84afab Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Tue, 16 Jun 2026 13:00:51 +0600 Subject: [PATCH 1/3] fix(notifications): add certificate earned email notification and engagement trigger - Add email support to the certificate earned notification controller and view. - Add new 'certificate_earned' engagement trigger so follow-up actions can fire when a student earns a certificate. - Update engagement trigger registry and hook mapping for llms_user_earned_certificate. - Add unit tests for email notification support. Fixes #3143 --- .changelogs/issue-3143.yml | 3 + includes/class.llms.engagements.php | 8 ++- includes/llms.functions.core.php | 2 + ...fication.controller.certificate.earned.php | 12 +++- ...s.notification.view.certificate.earned.php | 17 ++++- ...s-test-notification-certificate-earned.php | 64 +++++++++++++++++++ 6 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 .changelogs/issue-3143.yml diff --git a/.changelogs/issue-3143.yml b/.changelogs/issue-3143.yml new file mode 100644 index 0000000000..bfc0a6d1a8 --- /dev/null +++ b/.changelogs/issue-3143.yml @@ -0,0 +1,3 @@ +significance: minor +type: added +entry: "Added email support to the 'Certificate Earned' notification and added a new 'Student earns a certificate' engagement trigger." diff --git a/includes/class.llms.engagements.php b/includes/class.llms.engagements.php index 9e56ac6895..9644962a4d 100644 --- a/includes/class.llms.engagements.php +++ b/includes/class.llms.engagements.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 2.3.0 - * @version 6.6.0 + * @version 10.0.6 */ defined( 'ABSPATH' ) || exit; @@ -27,6 +27,7 @@ * - Deprecated the `LLMS_Engagements::log()` method. * Engagement debug logging is removed. Use the {@see llms_log()} function directly instead. * - Removed the deprecated `LLMS_Engagements::$_instance` property. + * @since 10.0.6 Added `llms_user_earned_certificate` as an engagement trigger hook. */ class LLMS_Engagements { @@ -223,6 +224,7 @@ protected function get_trigger_hooks() { $hooks = array( 'lifterlms_access_plan_purchased', + 'llms_user_earned_certificate', 'lifterlms_course_completed', 'lifterlms_course_track_completed', 'lifterlms_lesson_completed', @@ -446,6 +448,10 @@ private function parse_hook_find_trigger_type( $action, $related_post_id ) { case 'lifterlms_product_purchased': $trigger_type = str_replace( 'llms_', '', get_post_type( $related_post_id ) ) . '_purchased'; break; + + case 'llms_user_earned_certificate': + $trigger_type = 'certificate_earned'; + break; } return $trigger_type; diff --git a/includes/llms.functions.core.php b/includes/llms.functions.core.php index 936b5f63c8..424f45be63 100644 --- a/includes/llms.functions.core.php +++ b/includes/llms.functions.core.php @@ -443,6 +443,7 @@ function llms_get_donut( $percentage, $text = '', $size = 'default', $classes = * @return array * @since 3.1.0 * @since 3.24.1 + * @since 10.0.6 Added `certificate_earned` trigger. */ function llms_get_engagement_triggers() { /** @@ -460,6 +461,7 @@ function llms_get_engagement_triggers() { 'course_enrollment' => __( 'Student enrolls in a course', 'lifterlms' ), 'course_purchased' => __( 'Student purchases a course', 'lifterlms' ), 'course_completed' => __( 'Student completes a course', 'lifterlms' ), + 'certificate_earned' => __( 'Student earns a certificate', 'lifterlms' ), // 'days_since_login' => __( 'Days since user last logged in', 'lifterlms' ), // @todo. 'lesson_completed' => __( 'Student completes a lesson', 'lifterlms' ), 'quiz_completed' => __( 'Student completes a quiz', 'lifterlms' ), diff --git a/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php b/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php index d6100ad23d..219773522e 100644 --- a/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php +++ b/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php @@ -5,7 +5,7 @@ * @package LifterLMS/Notifications/Controllers/Classes * * @since 3.8.0 - * @version 3.8.0 + * @version 10.0.6 */ defined( 'ABSPATH' ) || exit; @@ -99,7 +99,7 @@ public function get_title() { * @param string $type notification type id * @return array * @since 3.8.0 - * @version 3.8.0 + * @version 10.0.6 */ protected function set_subscriber_options( $type ) { @@ -111,6 +111,11 @@ protected function set_subscriber_options( $type ) { $options[] = $this->get_subscriber_option_array( 'student', 'yes' ); break; + case 'email': + $options[] = $this->get_subscriber_option_array( 'student', 'yes' ); + $options[] = $this->get_subscriber_option_array( 'custom', 'no' ); + break; + } return $options; @@ -123,11 +128,12 @@ protected function set_subscriber_options( $type ) { * * @return array associative array, keys are the ID/db type, values should be translated display types * @since 3.8.0 - * @version 3.8.0 + * @version 10.0.6 */ protected function set_supported_types() { return array( 'basic' => __( 'Popup', 'lifterlms' ), + 'email' => __( 'Email', 'lifterlms' ), ); } } diff --git a/includes/notifications/views/class.llms.notification.view.certificate.earned.php b/includes/notifications/views/class.llms.notification.view.certificate.earned.php index 3bfadaa2ab..57d256fcbd 100644 --- a/includes/notifications/views/class.llms.notification.view.certificate.earned.php +++ b/includes/notifications/views/class.llms.notification.view.certificate.earned.php @@ -5,7 +5,7 @@ * @package LifterLMS/Notifications/Views/Classes * * @since 3.8.0 - * @version 6.0.0 + * @version 10.0.6 */ defined( 'ABSPATH' ) || exit; @@ -78,10 +78,15 @@ private function get_mini_html( $title ) { * Setup body content for output. * * @since 3.8.0 + * @version 10.0.6 * * @return string */ protected function set_body() { + if ( 'email' === $this->notification->get( 'type' ) ) { + return '

' . sprintf( __( 'Congratulations! You earned %s.', 'lifterlms' ), '{{CERTIFICATE_TITLE}}' ) . '

' + . '

' . __( 'View Full Certificate', 'lifterlms' ) . '

'; + } return '{{MINI_CERTIFICATE}}'; } @@ -209,11 +214,12 @@ private function set_merge_data_student_name( $cert ) { * Setup notification subject for output. * * @since 3.8.0 + * @version 10.0.6 * * @return string */ protected function set_subject() { - return ''; + return sprintf( __( 'You\'ve earned a certificate: %s', 'lifterlms' ), '{{CERTIFICATE_TITLE}}' ); } /** @@ -231,6 +237,7 @@ protected function set_title() { * Defines field support for the view. * * @since 3.8.0 + * @version 10.0.6 * * @return array */ @@ -241,6 +248,12 @@ protected function set_supported_fields() { 'title' => true, 'icon' => true, ), + 'email' => array( + 'body' => true, + 'icon' => false, + 'subject' => true, + 'title' => true, + ), ); } } diff --git a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php index cdc087c5dc..75c1eaeddd 100644 --- a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php +++ b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php @@ -7,6 +7,7 @@ * @group notifications * * @since 6.0.0 + * @since 10.0.6 Added tests for email notification support. */ class LLMS_Test_Notification_Certificate_Earned extends LLMS_NotificationTestCase { @@ -94,4 +95,67 @@ public function test_set_merge_data() { } + /** + * Test that the email notification type is supported. + * + * @since 10.0.6 + * + * @return void + */ + public function test_email_supported() { + + $controller = $this->get_controller(); + $types = LLMS_Unit_Test_Util::call_method( $controller, 'get_supported_types' ); + + $this->assertArrayHasKey( 'email', $types ); + $this->assertArrayHasKey( 'basic', $types ); + + } + + /** + * Test email subscriber options. + * + * @since 10.0.6 + * + * @return void + */ + public function test_email_subscriber_options() { + + $controller = $this->get_controller(); + $options = LLMS_Unit_Test_Util::call_method( $controller, 'get_subscriber_options', array( 'email' ) ); + + $types = array(); + foreach ( $options as $option ) { + $types[] = $option['subscriber_type']; + } + + $this->assertContains( 'student', $types ); + $this->assertContains( 'custom', $types ); + + } + + /** + * Test email view subject and body. + * + * @since 10.0.6 + * + * @return void + */ + public function test_email_view() { + + $notification = $this->get_notification(); + $notification->set( 'type', 'email' ); + + $view = llms()->notifications()->get_view( $notification ); + + $subject = LLMS_Unit_Test_Util::call_method( $view, 'set_subject' ); + $body = LLMS_Unit_Test_Util::call_method( $view, 'set_body' ); + $title = LLMS_Unit_Test_Util::call_method( $view, 'set_title' ); + + $this->assertStringContainsString( '{{CERTIFICATE_TITLE}}', $subject ); + $this->assertStringContainsString( '{{CERTIFICATE_URL}}', $body ); + $this->assertStringContainsString( 'certificate', strtolower( $title ) ); + + } + } From 14947be470069cdb9960c3c5eb0452f26ba6cc2e Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Tue, 21 Jul 2026 23:38:44 +0600 Subject: [PATCH 2/3] fix(ci): resolve test_email_subscriber_options CI failure - fix: use correct array key in subscriber options test test_email_subscriber_options accessed $option['subscriber_type'] but get_subscriber_option_array() returns arrays with key 'id', not 'subscriber_type'. All PHP version CI checks affected. Refs #3186 --- .../class-llms-test-notification-certificate-earned.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php index 75c1eaeddd..5ec721568e 100644 --- a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php +++ b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php @@ -126,7 +126,7 @@ public function test_email_subscriber_options() { $types = array(); foreach ( $options as $option ) { - $types[] = $option['subscriber_type']; + $types[] = $option['id']; } $this->assertContains( 'student', $types ); From 76dfbbfbeaafd8360e232302e5ea17cd81f78020 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Wed, 22 Jul 2026 15:57:35 +0600 Subject: [PATCH 3/3] fix(docs): replace hardcoded versions with [version] and follow @since conventions - Replace 10.0.6 with [version] placeholder across notification and engagement files - Drop @since [version] from existing class/function docblocks (git history records changes) - Drop @since from test method docblocks (test files: methods drop @since) Refs #3143 --- includes/class.llms.engagements.php | 3 +-- includes/llms.functions.core.php | 1 - ...ss.llms.notification.controller.certificate.earned.php | 6 +++--- .../class.llms.notification.view.certificate.earned.php | 8 ++++---- .../class-llms-test-notification-certificate-earned.php | 8 +------- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/includes/class.llms.engagements.php b/includes/class.llms.engagements.php index 9644962a4d..3c408d9c87 100644 --- a/includes/class.llms.engagements.php +++ b/includes/class.llms.engagements.php @@ -5,7 +5,7 @@ * @package LifterLMS/Classes * * @since 2.3.0 - * @version 10.0.6 + * @version [version] */ defined( 'ABSPATH' ) || exit; @@ -27,7 +27,6 @@ * - Deprecated the `LLMS_Engagements::log()` method. * Engagement debug logging is removed. Use the {@see llms_log()} function directly instead. * - Removed the deprecated `LLMS_Engagements::$_instance` property. - * @since 10.0.6 Added `llms_user_earned_certificate` as an engagement trigger hook. */ class LLMS_Engagements { diff --git a/includes/llms.functions.core.php b/includes/llms.functions.core.php index 424f45be63..a4cd39d625 100644 --- a/includes/llms.functions.core.php +++ b/includes/llms.functions.core.php @@ -443,7 +443,6 @@ function llms_get_donut( $percentage, $text = '', $size = 'default', $classes = * @return array * @since 3.1.0 * @since 3.24.1 - * @since 10.0.6 Added `certificate_earned` trigger. */ function llms_get_engagement_triggers() { /** diff --git a/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php b/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php index 219773522e..1c6b098d71 100644 --- a/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php +++ b/includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php @@ -5,7 +5,7 @@ * @package LifterLMS/Notifications/Controllers/Classes * * @since 3.8.0 - * @version 10.0.6 + * @version [version] */ defined( 'ABSPATH' ) || exit; @@ -99,7 +99,7 @@ public function get_title() { * @param string $type notification type id * @return array * @since 3.8.0 - * @version 10.0.6 + * @version [version] */ protected function set_subscriber_options( $type ) { @@ -128,7 +128,7 @@ protected function set_subscriber_options( $type ) { * * @return array associative array, keys are the ID/db type, values should be translated display types * @since 3.8.0 - * @version 10.0.6 + * @version [version] */ protected function set_supported_types() { return array( diff --git a/includes/notifications/views/class.llms.notification.view.certificate.earned.php b/includes/notifications/views/class.llms.notification.view.certificate.earned.php index 57d256fcbd..1a20df7421 100644 --- a/includes/notifications/views/class.llms.notification.view.certificate.earned.php +++ b/includes/notifications/views/class.llms.notification.view.certificate.earned.php @@ -5,7 +5,7 @@ * @package LifterLMS/Notifications/Views/Classes * * @since 3.8.0 - * @version 10.0.6 + * @version [version] */ defined( 'ABSPATH' ) || exit; @@ -78,7 +78,7 @@ private function get_mini_html( $title ) { * Setup body content for output. * * @since 3.8.0 - * @version 10.0.6 + * @version [version] * * @return string */ @@ -214,7 +214,7 @@ private function set_merge_data_student_name( $cert ) { * Setup notification subject for output. * * @since 3.8.0 - * @version 10.0.6 + * @version [version] * * @return string */ @@ -237,7 +237,7 @@ protected function set_title() { * Defines field support for the view. * * @since 3.8.0 - * @version 10.0.6 + * @version [version] * * @return array */ diff --git a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php index 5ec721568e..aae1634621 100644 --- a/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php +++ b/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php @@ -7,7 +7,7 @@ * @group notifications * * @since 6.0.0 - * @since 10.0.6 Added tests for email notification support. + * @since [version] Added tests for email notification support. */ class LLMS_Test_Notification_Certificate_Earned extends LLMS_NotificationTestCase { @@ -98,8 +98,6 @@ public function test_set_merge_data() { /** * Test that the email notification type is supported. * - * @since 10.0.6 - * * @return void */ public function test_email_supported() { @@ -115,8 +113,6 @@ public function test_email_supported() { /** * Test email subscriber options. * - * @since 10.0.6 - * * @return void */ public function test_email_subscriber_options() { @@ -137,8 +133,6 @@ public function test_email_subscriber_options() { /** * Test email view subject and body. * - * @since 10.0.6 - * * @return void */ public function test_email_view() {