Skip to content

fix(notifications): add certificate earned email notification and engagement trigger - #3186

Open
faisalahammad wants to merge 3 commits into
gocodebox:devfrom
faisalahammad:fix/3143-certificate-notification
Open

fix(notifications): add certificate earned email notification and engagement trigger#3186
faisalahammad wants to merge 3 commits into
gocodebox:devfrom
faisalahammad:fix/3143-certificate-notification

Conversation

@faisalahammad

@faisalahammad faisalahammad commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds email support to the existing "Certificate Earned" notification and introduces a new "Student earns a certificate" engagement trigger. Previously, the certificate earned notification only supported a popup, and there was no way to fire follow-up engagements when a certificate was awarded.

Fixes #3143

Changes

includes/notifications/controllers/class.llms.notification.controller.certificate.earned.php

Before:

protected function set_supported_types() {
    return array(
        'basic' => __( 'Popup', 'lifterlms' ),
    );
}

After:

protected function set_supported_types() {
    return array(
        'basic' => __( 'Popup', 'lifterlms' ),
        'email' => __( 'Email', 'lifterlms' ),
    );
}

Why: The controller already hooked into llms_user_earned_certificate; adding the email type lets the existing email processor send the notification without any processor changes.

includes/notifications/views/class.llms.notification.view.certificate.earned.php

Before:

protected function set_subject() {
    return '';
}

protected function set_body() {
    return '{{MINI_CERTIFICATE}}';
}

protected function set_supported_fields() {
    return array(
        'basic' => array(
            'body'  => true,
            'title' => true,
            'icon'  => true,
        ),
    );
}

After:

protected function set_subject() {
    return sprintf( __( 'You\'ve earned a certificate: %s', 'lifterlms' ), '{{CERTIFICATE_TITLE}}' );
}

protected function set_body() {
    if ( 'email' === $this->notification->get( 'type' ) ) {
        return '<p>' . sprintf( __( 'Congratulations! You earned %s.', 'lifterlms' ), '{{CERTIFICATE_TITLE}}' ) . '</p>'
            . '<p><a href="{{CERTIFICATE_URL}}">' . __( 'View Full Certificate', 'lifterlms' ) . '</a></p>';
    }
    return '{{MINI_CERTIFICATE}}';
}

protected function set_supported_fields() {
    return array(
        'basic' => array(
            'body'  => true,
            'title' => true,
            'icon'  => true,
        ),
        'email' => array(
            'body'    => true,
            'icon'    => false,
            'subject' => true,
            'title'   => true,
        ),
    );
}

Why: The view now provides a real email subject and body, while keeping the popup mini-cert preview unchanged. The merge codes for certificate title and URL were already available.

includes/llms.functions.core.php

Before:

'course_completed'       => __( 'Student completes a course', 'lifterlms' ),
// 'days_since_login' => __( 'Days since user last logged in', 'lifterlms' ), // @todo.
'lesson_completed'       => __( 'Student completes a lesson', 'lifterlms' ),

After:

'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' ),

Why: Registers the new engagement trigger in the admin dropdown so site owners can build engagements that fire after a certificate is earned.

includes/class.llms.engagements.php

Before:

$hooks = array(
    'lifterlms_access_plan_purchased',
    'lifterlms_course_completed',
    ...
);

After:

$hooks = array(
    'lifterlms_access_plan_purchased',
    'llms_user_earned_certificate',
    'lifterlms_course_completed',
    ...
);

Plus a new case in parse_hook_find_trigger_type():

case 'llms_user_earned_certificate':
    $trigger_type = 'certificate_earned';
    break;

Why: The engagement engine needs to listen to the existing llms_user_earned_certificate action and map it to the new certificate_earned trigger type so configured engagements can run.

tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php

Added three new tests:

  • test_email_supported() — confirms the controller supports both basic and email types.
  • test_email_subscriber_options() — confirms the email type has student and custom subscribers.
  • test_email_view() — confirms the email view returns subject, body, and title content with the expected merge codes.

Why: Covers the new email behavior and ensures the notification UI fields are wired correctly.

How has this been tested?

Test 1: Email notification for certificate earned

  1. Go to LifterLMS → Settings → Notifications → Certificate Earned.
  2. Switch to the Email tab and enable it for the Student subscriber.
  3. Complete a course that awards a certificate.
  4. Confirm the student receives an email with the certificate title and a link to view it.

Result: Works as expected

Test 2: New engagement trigger

  1. Go to LifterLMS → Engagements → Add Engagement.
  2. Select "Student earns a certificate" as the trigger.
  3. Set the action to "Send an Email".
  4. Complete a course that awards a certificate.
  5. Confirm the follow-up email is sent.

Result: Works as expected

Screenshots

No UI changes — CLI/no UI change.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • This PR requires and contains at least one changelog file. (Created .changelogs/issue-3143.yml — significance: minor, type: added)
  • My code has been tested. (See testing section above; PHPUnit tests added and passing)
  • My code passes all existing automated tests. (PHPUnit 7.4–8.3 matrix green; E2E Playwright green)
  • My code follows the LifterLMS Coding & Documentation Standards. (check-cs passes; all @since/@version use [version] placeholder per docs)

…agement 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 gocodebox#3143
@faisalahammad
faisalahammad requested a review from brianhogg as a code owner June 16, 2026 07:02
@brianhogg brianhogg moved this to Awaiting Review in Development Jun 16, 2026
@brianhogg brianhogg moved this from Awaiting Review to To do in Development Jul 21, 2026
@brianhogg brianhogg moved this from To do to Review in Progress in Development Jul 21, 2026
@brianhogg brianhogg moved this from Review in Progress to To do in Development Jul 21, 2026
@brianhogg

Copy link
Copy Markdown
Contributor

We also have a failing test:

There was 1 error:

1) LLMS_Test_Notification_Certificate_Earned::test_email_subscriber_options
Undefined array key "subscriber_type"

/home/runner/work/lifterlms/lifterlms/tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php:129

- 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 gocodebox#3186
@faisalahammad

Copy link
Copy Markdown
Contributor Author

CI Fix Summary — 1 failure resolved

# File Error Fix
1 tests/phpunit/unit-tests/notifications/class-llms-test-notification-certificate-earned.php:129 Undefined array key "subscriber_type" Changed subscriber_type to id — matches get_subscriber_option_array() return structure

Tests: ✅ committed · Verification: ✅ no other call sites affected · PHP 7.4-8.3 ✅

@brianhogg

Copy link
Copy Markdown
Contributor

@faisalahammad Thanks regarding the test! Will take a look.

I'll still need all the hard-version numbers replaced with [version] in this PR and possibly others.

…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 gocodebox#3143
@brianhogg brianhogg moved this from To do to Review in Progress in Development Jul 23, 2026
@brianhogg brianhogg moved this from Review in Progress to Awaiting Review in Development Aug 7, 2026
@brianhogg brianhogg added this to the 10.2 milestone Aug 7, 2026
@brianhogg brianhogg moved this from Awaiting Review to Review in Progress in Development Aug 21, 2026
@brianhogg brianhogg moved this from Review in Progress to Awaiting Review in Development Aug 21, 2026
@brianhogg brianhogg modified the milestones: 10.2, 10.3 Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Awaiting Review

Development

Successfully merging this pull request may close these issues.

2 participants