acceptances = Mockery::mock(AcceptanceRepository::class); $this->policies = Mockery::mock(PolicyRepository::class); $this->policyVersions = Mockery::mock(PolicyVersionRepository::class); $this->answers = Mockery::mock(AnswerRepository::class); $this->questions = Mockery::mock(QuestionRepository::class); $this->payments = Mockery::mock(PaymentRepository::class); $this->credits = Mockery::mock(CreditRepository::class); $this->history = new StudentHistory( $this->acceptances, $this->policies, $this->policyVersions, $this->answers, $this->questions, $this->payments, $this->credits ); } public function testPolicyAcceptancesResolvePolicyTitleAndVersion(): void { $this->acceptances->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new PolicyAcceptance(9, 5, PolicyAcceptance::REG_ACCOUNT, 5, ipAddress: null, acceptedAt: '2026-06-02 09:00:00', id: 1), ]); $this->policyVersions->shouldReceive('findById')->with(9) ->andReturn(new PolicyVersion(2, 3, null, PolicyVersion::STATUS_PUBLISHED, id: 9)); $this->policies->shouldReceive('findById')->with(2) ->andReturn(new Policy('Waiver', 'waiver', 9, Policy::SCOPE_SIGNUP, 2)); $rows = $this->history->policyAcceptances(5); self::assertSame( [ [ 'policy' => 'Waiver', 'version' => 'v3', 'context' => 'Account signup', 'accepted_at' => '2026-06-02 09:00:00', ], ], $rows ); } public function testPolicyAcceptancesFallBackWhenVersionIsGone(): void { $this->acceptances->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new PolicyAcceptance(9, 5, PolicyAcceptance::REG_LESSON, 12), ]); $this->policyVersions->shouldReceive('findById')->with(9)->andReturn(null); $rows = $this->history->policyAcceptances(5); self::assertSame('#9', $rows[0]['policy']); self::assertSame('—', $rows[0]['version']); self::assertSame('Lesson #12', $rows[0]['context']); self::assertSame('', $rows[0]['accepted_at']); } public function testIntakeAnswersResolveQuestionLabels(): void { $this->answers->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new Answer(4, Answer::REG_ENROLLMENT, 3, 5, 'Beginner', 1), ]); $this->questions->shouldReceive('findById')->with(4) ->andReturn(new Question(1, 'Experience level', id: 4)); $rows = $this->history->intakeAnswers(5); self::assertSame( [ [ 'question' => 'Experience level', 'answer' => 'Beginner', 'context' => 'Enrolment #3', ], ], $rows ); } public function testIntakeAnswersFallBackWhenQuestionIsGone(): void { $this->answers->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new Answer(4, Answer::REG_LESSON, 12, 5, null, 1), ]); $this->questions->shouldReceive('findById')->with(4)->andReturn(null); $rows = $this->history->intakeAnswers(5); self::assertSame('#4', $rows[0]['question']); self::assertSame('—', $rows[0]['answer']); } public function testIntakeAnswersExcludeAccountScopeAnswers(): void { $this->answers->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new Answer(9, Answer::REG_ACCOUNT, 5, 5, 'By a friend', 2), new Answer(4, Answer::REG_LESSON, 12, 5, 'Beginner', 1), ]); // Only the booking-scoped answer is resolved; the account answer is dropped. $this->questions->shouldReceive('findById')->with(4) ->andReturn(new Question(1, 'Experience level', id: 4)); $rows = $this->history->intakeAnswers(5); self::assertCount(1, $rows); self::assertSame('Experience level', $rows[0]['question']); self::assertSame('Lesson #12', $rows[0]['context']); } public function testRegistrationInfoPairsAccountQuestionsWithAnswers(): void { $this->answers->shouldReceive('findByRegistration')->once()->with(Answer::REG_ACCOUNT, 5)->andReturn([ new Answer(4, Answer::REG_ACCOUNT, 5, 5, 'Yes', 1), ]); $this->questions->shouldReceive('findByScope')->once()->with(Question::SCOPE_ACCOUNT)->andReturn([ new Question(null, 'Consent to email', isRequired: true, scope: Question::SCOPE_ACCOUNT, id: 4), new Question(null, 'Anything else?', isRequired: false, scope: Question::SCOPE_ACCOUNT, id: 7), ]); $rows = $this->history->registrationInfo(5); self::assertSame( [ [ 'question' => 'Consent to email', 'answer' => 'Yes', 'required' => true ], [ 'question' => 'Anything else?', 'answer' => '—', 'required' => false ], ], $rows ); } public function testPaymentsBuildDisplayRows(): void { $this->payments->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new Payment( 5, 3, Payment::REG_LESSON, 12, 100.00, currency: 'CAD', method: Payment::METHOD_CARD, status: Payment::STATUS_PAID, taxRate: 13.0, taxAmount: 13.00, receiptNumber: 'USC-7', createdAt: '2026-06-08 09:00:00', id: 50 ), ]); $rows = $this->history->payments(5); self::assertSame( [ [ 'created_at' => '2026-06-08 09:00:00', 'context' => 'Lesson #12', 'method' => Payment::METHOD_CARD, 'status' => Payment::STATUS_PAID, 'amount' => 100.00, 'tax_amount' => 13.00, 'total' => 113.00, 'currency' => 'CAD', 'receipt' => 'USC-7', ], ], $rows ); } public function testPaymentsFallBackWhenUnpaidAndUndated(): void { $this->payments->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new Payment(5, 3, Payment::REG_ENROLLMENT, 3, 40.00, id: 51), ]); $rows = $this->history->payments(5); self::assertSame('', $rows[0]['created_at']); self::assertSame('Enrolment #3', $rows[0]['context']); self::assertSame('—', $rows[0]['receipt']); } public function testCreditBalanceDelegatesToRepository(): void { $this->credits->shouldReceive('availableBalance')->once()->with(5)->andReturn(45.0); self::assertSame(45.0, $this->history->creditBalance(5)); } public function testCreditsBuildDisplayRows(): void { $this->credits->shouldReceive('findByStudent')->once()->with(5)->andReturn([ new Credit(5, 33.00, 13.00, currency: 'CAD', sourcePaymentId: 12, sourceLessonId: 77, reason: 'Credit for cancelled lesson #77', status: Credit::STATUS_AVAILABLE, createdAt: '2026-07-01 09:00:00', id: 300), ]); $rows = $this->history->credits(5); self::assertSame( [ [ 'created_at' => '2026-07-01 09:00:00', 'amount' => 33.00, 'remaining' => 13.00, 'currency' => 'CAD', 'reason' => 'Credit for cancelled lesson #77', 'status' => Credit::STATUS_AVAILABLE, ], ], $rows ); } }