user_email = $email; 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)); } }