invoiceninja/tests/Unit/ClientBalanceReportOptimiza...

338 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Unit;
use Tests\TestCase;
use Tests\MockAccountData;
use Illuminate\Support\Facades\DB;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Services\Report\ClientBalanceReport;
use App\Models\Invoice;
use App\Models\Client;
/**
* Test suite for Client Balance Report optimization
*
* Validates that optimized single-query approach produces identical results
* to legacy per-client query approach while reducing database queries.
*/
class ClientBalanceReportOptimizationTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
}
/**
* Test that optimized approach produces identical results to legacy
*/
public function testOptimizedMatchesLegacyResults()
{
// Create test data: 10 clients with varying invoice counts
$clients = Client::factory()->count(10)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
foreach ($clients as $index => $client) {
// Create 0-5 invoices per client
$invoiceCount = $index % 6;
for ($i = 0; $i < $invoiceCount; $i++) {
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 100 + ($i * 50),
'amount' => 100 + ($i * 50),
]);
}
}
// Run both implementations
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$legacyReport = new ClientBalanceReport($this->company, $input);
$optimizedReport = new ClientBalanceReport($this->company, $input);
// Count queries for legacy
DB::enableQueryLog();
DB::flushQueryLog();
$legacyOutput = $legacyReport->run();
$legacyQueries = count(DB::getQueryLog());
// Count queries for optimized (we'll implement this in the service)
DB::flushQueryLog();
// This will use optimized path when we implement it
$optimizedOutput = $optimizedReport->run();
$optimizedQueries = count(DB::getQueryLog());
DB::disableQueryLog();
// For now, both use same implementation, so they should match
$this->assertEquals($legacyOutput, $optimizedOutput);
// After optimization, we expect significant reduction
// Legacy: ~2N queries (N clients × 2 queries/client)
// Optimized: ~2 queries (1 for clients, 1 for aggregates)
$this->assertGreaterThan(10, $legacyQueries, 'Legacy should make many queries');
}
/**
* Test query count reduction with optimized approach
*/
public function testQueryCountReduction()
{
$clientCount = 50;
// Create clients with invoices
$clients = Client::factory()->count($clientCount)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
foreach ($clients as $client) {
Invoice::factory()->count(3)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 500,
]);
}
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
DB::enableQueryLog();
DB::flushQueryLog();
$report->run();
$queryCount = count(DB::getQueryLog());
DB::disableQueryLog();
// Optimized: ~10-15 queries (client fetch + aggregate + framework overhead)
// Legacy: 100 queries (50 clients × 2)
$this->assertLessThan($clientCount * 0.5, $queryCount,
"Expected < " . ($clientCount * 0.5) . " queries (optimized), got {$queryCount}");
}
/**
* Test with clients having no invoices
*/
public function testClientsWithNoInvoices()
{
// Create clients without invoices
Client::factory()->count(5)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
$output = $report->run();
// Should return 0 for invoice count and balance
$this->assertNotEmpty($output);
$lines = array_filter(explode("\n", $output), fn($line) => !empty($line));
$this->assertGreaterThanOrEqual(5, count($lines));
}
/**
* Test with date range filtering
*/
public function testDateRangeFiltering()
{
$client = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
// Create invoices at different dates
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 100,
'created_at' => now()->subDays(5),
]);
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 200,
'created_at' => now()->subDays(45),
]);
// Test last 7 days filter
$input = ['date_range' => 'last7', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
$output = $report->run();
$this->assertNotEmpty($output);
}
/**
* Test with different invoice statuses
*/
public function testInvoiceStatusFiltering()
{
$client = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
// Create invoices with different statuses
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 100,
]);
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_DRAFT,
'balance' => 200,
]);
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_PARTIAL,
'balance' => 150,
]);
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
$output = $report->run();
// Should only include SENT and PARTIAL invoices
$this->assertNotEmpty($output);
$lines = array_filter(explode("\n", $output), fn($line) => !empty($line));
$this->assertGreaterThanOrEqual(5, count($lines));
}
/**
* Test with large dataset to measure performance improvement
*/
public function testLargeDatasetPerformance()
{
$clientCount = 100;
// Create 100 clients with 5 invoices each
$clients = Client::factory()->count($clientCount)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
foreach ($clients as $client) {
Invoice::factory()->count(5)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 1000,
]);
}
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
DB::enableQueryLog();
DB::flushQueryLog();
$startTime = microtime(true);
$output = $report->run();
$endTime = microtime(true);
$queryCount = count(DB::getQueryLog());
DB::disableQueryLog();
$executionTime = $endTime - $startTime;
// Optimized: ~10-20 queries (aggregate query + framework overhead)
// Legacy: 200 queries (100 clients × 2)
$this->assertLessThan(50, $queryCount, "Expected < 50 queries (optimized), got {$queryCount}");
$this->assertNotEmpty($output);
// Log performance metrics for comparison
echo "\nPerformance Metrics (Optimized):\n";
echo " Clients: {$clientCount}\n";
echo " Queries: {$queryCount}\n";
echo " Time: " . number_format($executionTime, 3) . "s\n";
echo " Legacy queries: ~" . ($clientCount * 2) . "\n";
echo " Improvement: " . round(($clientCount * 2) / max($queryCount, 1), 1) . "x\n";
}
/**
* Test with zero balance invoices
*/
public function testZeroBalanceInvoices()
{
$client = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
// Create invoices with zero balance (paid)
Invoice::factory()->count(3)->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 0,
'amount' => 100,
]);
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
$output = $report->run();
// Should still count the invoices
$this->assertNotEmpty($output);
$lines = array_filter(explode("\n", $output), fn($line) => !empty($line));
$this->assertGreaterThanOrEqual(5, count($lines));
}
/**
* Test report output structure
*/
public function testReportOutputStructure()
{
$client = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'name' => 'Test Client',
'number' => 'CLI-001',
'id_number' => 'TAX-123',
]);
Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'status_id' => Invoice::STATUS_SENT,
'balance' => 500,
]);
$input = ['date_range' => 'all', 'report_keys' => [], 'user_id' => $this->user->id];
$report = new ClientBalanceReport($this->company, $input);
$output = $report->run();
// Verify output contains client data
$this->assertNotEmpty($output);
$lines = array_filter(explode("\n", $output), fn($line) => !empty($line));
$this->assertGreaterThanOrEqual(5, count($lines));
}
}