*/ private function rows(): array { return [ [ 'date' => '2026-06-02', 'student' => 'Ada', 'instructor' => 'Pat', 'method' => 'etransfer', 'status' => 'paid', 'amount' => 100.00, 'tax_rate' => 13.0, 'tax_amount' => 13.00, 'total' => 113.00, ], [ 'date' => '2026-06-09', 'student' => 'Grace', 'instructor' => 'Pat', 'method' => 'card', 'status' => 'paid', 'amount' => 50.00, 'tax_rate' => 13.0, 'tax_amount' => 6.50, 'total' => 56.50, ], ]; } public function testTotalsAggregateAmountsAndTax(): void { $report = new PaymentReport($this->rows()); self::assertSame(2, $report->count()); self::assertSame(150.00, $report->totalAmount()); self::assertSame(19.50, $report->totalTax()); self::assertSame(169.50, $report->grandTotal()); } public function testEmptyReportHasZeroTotals(): void { $report = new PaymentReport([]); self::assertSame(0, $report->count()); self::assertSame(0.0, $report->totalTax()); self::assertSame(0.0, $report->grandTotal()); } public function testCsvIncludesHeaderRowsAndTotals(): void { $csv = (new PaymentReport($this->rows()))->toCsv(); $lines = explode("\n", trim($csv)); // header + 2 data rows + totals row. self::assertCount(4, $lines); self::assertStringContainsString('"Date","Student","Instructor"', $lines[0]); self::assertStringContainsString('"Ada"', $lines[1]); self::assertStringContainsString('"Totals"', $lines[3]); self::assertStringContainsString('"19.50"', $lines[3]); self::assertStringContainsString('"169.50"', $lines[3]); } public function testCsvEscapesEmbeddedQuotes(): void { $rows = $this->rows(); $rows[0]['student'] = 'Ada "The Great"'; $csv = (new PaymentReport($rows))->toCsv(); self::assertStringContainsString('"Ada ""The Great"""', $csv); } }