alias(static fn (string $name, $default = '') => ''); } private function student(string $email, string $name = 'Alex Student'): \WP_User { $student = Mockery::mock(\WP_User::class); $student->user_email = $email; $student->display_name = $name; return $student; } public function testReturnsFalseWithoutRecipient(): void { $items = [[ 'label' => 'x', 'amount' => 10.0, 'currency' => 'CAD', 'due_date' => '2026-07-14', 'etransfer_email' => null ]]; self::assertFalse((new PaymentDueMailer())->send($this->student(''), $items)); } public function testReturnsFalseWithNoItems(): void { self::assertFalse((new PaymentDueMailer())->send($this->student('a@b.test'), [])); } public function testConsolidatesItemsWithGrandTotal(): void { Functions\expect('wp_mail') ->once() ->with( 'a@b.test', Mockery::type('string'), Mockery::on(static function (string $body): bool { return str_contains($body, 'Piano') && str_contains($body, 'Jul 15, 2026') && str_contains($body, 'Guitar') && str_contains($body, 'Jul 22, 2026') && str_contains($body, '35.00') && str_contains($body, '40.00') // 35 + 40 grand total && str_contains($body, '75.00'); }) ) ->andReturn(true); $items = [ [ 'label' => 'Piano', 'amount' => 35.0, 'currency' => 'CAD', 'due_date' => '2026-07-15', 'etransfer_email' => null ], [ 'label' => 'Guitar', 'amount' => 40.0, 'currency' => 'CAD', 'due_date' => '2026-07-22', 'etransfer_email' => null ], ]; self::assertTrue((new PaymentDueMailer())->send($this->student('a@b.test'), $items)); } public function testIncludesReferenceWhenProvided(): void { Functions\expect('wp_mail') ->once() ->with( 'a@b.test', Mockery::type('string'), Mockery::on(static fn (string $body): bool => str_contains($body, 'REF12345')) ) ->andReturn(true); $items = [[ 'label' => 'Piano', 'amount' => 35.0, 'currency' => 'CAD', 'due_date' => '2026-07-15', 'etransfer_email' => null ]]; self::assertTrue((new PaymentDueMailer())->send($this->student('a@b.test'), $items, 'REF12345')); } public function testCreditReducesTheTotalDue(): void { Functions\expect('wp_mail') ->once() ->with( 'a@b.test', Mockery::type('string'), Mockery::on(static function (string $body): bool { // Line shows the full 35.00; credit line shows -20.00; total due 15.00. return str_contains($body, '35.00') && str_contains($body, '-CAD 20.00') && str_contains($body, 'Total due: CAD 15.00'); }) ) ->andReturn(true); $items = [[ 'label' => 'Piano', 'amount' => 35.0, 'currency' => 'CAD', 'due_date' => '2026-07-15', 'etransfer_email' => 'pay@studio.test' ]]; self::assertTrue((new PaymentDueMailer())->send($this->student('a@b.test'), $items, 'REF1', 20.0)); } public function testCreditCoveringEverythingLeavesZeroDueAndNoEtransferLine(): void { Functions\expect('wp_mail') ->once() ->with( 'a@b.test', Mockery::type('string'), Mockery::on(static function (string $body): bool { return str_contains($body, 'Total due: CAD 0.00') && ! str_contains($body, 'pay@studio.test'); }) ) ->andReturn(true); $items = [[ 'label' => 'Piano', 'amount' => 35.0, 'currency' => 'CAD', 'due_date' => '2026-07-15', 'etransfer_email' => 'pay@studio.test' ]]; self::assertTrue((new PaymentDueMailer())->send($this->student('a@b.test'), $items, '', 35.0)); } public function testIncludesEtransferDestination(): void { Functions\expect('wp_mail') ->once() ->with( 'a@b.test', Mockery::type('string'), Mockery::on(static fn (string $body): bool => str_contains($body, 'pay@studio.test')) ) ->andReturn(true); $items = [[ 'label' => 'Piano', 'amount' => 35.0, 'currency' => 'CAD', 'due_date' => '2026-07-15', 'etransfer_email' => 'pay@studio.test' ]]; self::assertTrue((new PaymentDueMailer())->send($this->student('a@b.test'), $items)); } public function testRendersCustomTemplateWithTokens(): void { // A stored template overrides the default; tokens are substituted with // the real values gathered from the items and student. Functions\when('get_option')->alias(static function (string $name) { if ($name === PaymentDueEmailTemplate::OPT_SUBJECT) { return 'Hi {student_name} — {total_due}'; } if ($name === PaymentDueEmailTemplate::OPT_BODY) { return "Dear {student_name},\n{items}\nOwing: {total_due}"; } if ($name === PaymentDueEmailTemplate::OPT_ITEM_LINE) { return '* {label} = {currency} {amount}'; } return ''; }); Functions\expect('wp_mail') ->once() ->with( 'a@b.test', 'Hi Jordan — CAD 35.00', Mockery::on(static function (string $body): bool { return str_contains($body, 'Dear Jordan,') && str_contains($body, '* Piano = CAD 35.00') && str_contains($body, 'Owing: CAD 35.00'); }) ) ->andReturn(true); $items = [[ 'label' => 'Piano', 'amount' => 35.0, 'currency' => 'CAD', 'due_date' => '2026-07-15', 'etransfer_email' => null ]]; self::assertTrue((new PaymentDueMailer())->send($this->student('a@b.test', 'Jordan'), $items)); } }