Notify instructors on new bookings and enrolments
CI / Coding Standards (pull_request) Successful in 27s
CI / Tests (PHP 8.1) (pull_request) Successful in 37s
CI / No Debug Code (pull_request) Successful in 9s
CI / Tests (PHP 8.3) (pull_request) Successful in 36s
CI / Tests (PHP 8.5) (pull_request) Successful in 40s
CI / Tests (PHP 8.2) (pull_request) Successful in 42s
CI / Static Analysis (pull_request) Successful in 48s
CI / Build Plugin Zip (pull_request) Skipped
CI / Coding Standards (pull_request) Successful in 27s
CI / Tests (PHP 8.1) (pull_request) Successful in 37s
CI / No Debug Code (pull_request) Successful in 9s
CI / Tests (PHP 8.3) (pull_request) Successful in 36s
CI / Tests (PHP 8.5) (pull_request) Successful in 40s
CI / Tests (PHP 8.2) (pull_request) Successful in 42s
CI / Static Analysis (pull_request) Successful in 48s
CI / Build Plugin Zip (pull_request) Skipped
Add an opt-in, per-instructor email notice sent when someone books one of their lessons or enrols in one of their group classes. Off by default and set from My Availability → Notifications; covers both the student/guardian REST flows and the studio's wp-admin "book/add for a student" forms. The opt-in check lives in InstructorNotificationMailer so no booking path can drift on who is mailed; lessons fire from LessonBooker::settle (the one step both booking paths reach), enrolments from EnrollmentEndpoint::enroll and GroupClassController::addDirect. The notice is a courtesy and never fails a booking or enrolment that otherwise succeeded. Co-authored-by: anthropic/claude-opus-4-8
This commit is contained in:
co-authored by
anthropic/claude-opus-4-8
parent
56fc0bd57d
commit
12765d8f13
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Auth\InstructorNotificationMailer;
|
||||
use Unsupervised\Schedular\Auth\InstructorNotificationPref;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class InstructorNotificationMailerTest extends TestCase
|
||||
{
|
||||
private InstructorNotificationPref&Mockery\MockInterface $pref;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->pref = Mockery::mock(InstructorNotificationPref::class);
|
||||
}
|
||||
|
||||
private function mailer(): InstructorNotificationMailer
|
||||
{
|
||||
return new InstructorNotificationMailer($this->pref);
|
||||
}
|
||||
|
||||
private function instructor(string $email): \WP_User
|
||||
{
|
||||
$user = Mockery::mock(\WP_User::class);
|
||||
$user->user_email = $email;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function testLessonNoticeGoesToAnOptedInInstructor(): void
|
||||
{
|
||||
$this->pref->shouldReceive('wants')->with(9)->andReturn(true);
|
||||
Functions\when('get_userdata')->justReturn($this->instructor('[email protected]'));
|
||||
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with(
|
||||
'[email protected]',
|
||||
Mockery::on(static fn (string $subject): bool => str_contains($subject, '30 min piano')),
|
||||
Mockery::on(static fn (string $body): bool => str_contains($body, 'Ada') && str_contains($body, 'Jul 1'))
|
||||
)
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM'));
|
||||
}
|
||||
|
||||
public function testWeeklyLessonNoticeCountsTheOccurrences(): void
|
||||
{
|
||||
$this->pref->shouldReceive('wants')->with(9)->andReturn(true);
|
||||
Functions\when('get_userdata')->justReturn($this->instructor('[email protected]'));
|
||||
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with(
|
||||
'[email protected]',
|
||||
Mockery::type('string'),
|
||||
Mockery::on(static fn (string $body): bool => str_contains($body, '3 weekly lessons'))
|
||||
)
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM', 3));
|
||||
}
|
||||
|
||||
public function testEnrollmentNoticeGoesToAnOptedInInstructor(): void
|
||||
{
|
||||
$this->pref->shouldReceive('wants')->with(9)->andReturn(true);
|
||||
Functions\when('get_userdata')->justReturn($this->instructor('[email protected]'));
|
||||
|
||||
Functions\expect('wp_mail')
|
||||
->once()
|
||||
->with(
|
||||
'[email protected]',
|
||||
Mockery::on(static fn (string $subject): bool => str_contains($subject, 'Choir')),
|
||||
Mockery::on(static fn (string $body): bool => str_contains($body, 'Ada') && str_contains($body, 'Choir'))
|
||||
)
|
||||
->andReturn(true);
|
||||
|
||||
self::assertTrue($this->mailer()->notifyEnrollment(9, 'Ada', 'Choir'));
|
||||
}
|
||||
|
||||
public function testSendsNothingWhenTheInstructorHasNotOptedIn(): void
|
||||
{
|
||||
$this->pref->shouldReceive('wants')->with(9)->andReturn(false);
|
||||
// Not even a user lookup: the preference is the first gate.
|
||||
Functions\expect('get_userdata')->never();
|
||||
Functions\expect('wp_mail')->never();
|
||||
|
||||
self::assertFalse($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM'));
|
||||
self::assertFalse($this->mailer()->notifyEnrollment(9, 'Ada', 'Choir'));
|
||||
}
|
||||
|
||||
public function testSendsNothingWhenTheInstructorAccountIsGone(): void
|
||||
{
|
||||
$this->pref->shouldReceive('wants')->with(9)->andReturn(true);
|
||||
Functions\when('get_userdata')->justReturn(false);
|
||||
Functions\expect('wp_mail')->never();
|
||||
|
||||
self::assertFalse($this->mailer()->notifyEnrollment(9, 'Ada', 'Choir'));
|
||||
}
|
||||
|
||||
public function testSendsNothingWhenTheInstructorHasNoEmail(): void
|
||||
{
|
||||
$this->pref->shouldReceive('wants')->with(9)->andReturn(true);
|
||||
Functions\when('get_userdata')->justReturn($this->instructor(''));
|
||||
Functions\expect('wp_mail')->never();
|
||||
|
||||
self::assertFalse($this->mailer()->notifyLessonBooked(9, 'Ada', '30 min piano', 'Jul 1, 2026 10:00 AM'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Auth;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Unsupervised\Schedular\Auth\InstructorNotificationPref;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class InstructorNotificationPrefTest extends TestCase
|
||||
{
|
||||
public function testDefaultsOffWhenTheMetaIsUnset(): void
|
||||
{
|
||||
// Absent meta reads as an empty string, which is not the opt-in value.
|
||||
Functions\when('get_user_meta')->justReturn('');
|
||||
|
||||
self::assertFalse((new InstructorNotificationPref())->wants(7));
|
||||
}
|
||||
|
||||
public function testReadsAStoredOptIn(): void
|
||||
{
|
||||
Functions\when('get_user_meta')->alias(
|
||||
static fn (int $id, string $key): string => 7 === $id && InstructorNotificationPref::META_NOTIFY === $key ? '1' : ''
|
||||
);
|
||||
|
||||
self::assertTrue((new InstructorNotificationPref())->wants(7));
|
||||
}
|
||||
|
||||
public function testAStoredNoReadsAsOff(): void
|
||||
{
|
||||
Functions\when('get_user_meta')->justReturn('0');
|
||||
|
||||
self::assertFalse((new InstructorNotificationPref())->wants(7));
|
||||
}
|
||||
|
||||
public function testNobodyWantsNothing(): void
|
||||
{
|
||||
// A zero id is not a user; it must never read as opted-in.
|
||||
Functions\expect('get_user_meta')->never();
|
||||
|
||||
self::assertFalse((new InstructorNotificationPref())->wants(0));
|
||||
}
|
||||
|
||||
public function testSetStoresTheChoiceAsAStringBoolean(): void
|
||||
{
|
||||
Functions\expect('update_user_meta')->once()->with(7, InstructorNotificationPref::META_NOTIFY, '1')->andReturn(true);
|
||||
|
||||
(new InstructorNotificationPref())->set(7, true);
|
||||
}
|
||||
|
||||
public function testSetStoresADeliberateNoRatherThanDeleting(): void
|
||||
{
|
||||
Functions\expect('update_user_meta')->once()->with(7, InstructorNotificationPref::META_NOTIFY, '0')->andReturn(true);
|
||||
|
||||
(new InstructorNotificationPref())->set(7, false);
|
||||
}
|
||||
|
||||
public function testSetIgnoresANonUser(): void
|
||||
{
|
||||
Functions\expect('update_user_meta')->never();
|
||||
|
||||
(new InstructorNotificationPref())->set(0, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user