Files
InvoiceShelf/tests/Feature/Ai/Tools/RankTopCustomersToolTest.php
T
Darko Gjorgjijoski 5ef7804e60 refactor: adopt modular domain architecture (#747)
* refactor: stabilize model identities for domain migration

* refactor: extract module platform context

* refactor: assign models to domain contexts

* refactor: extract ai platform context

* refactor: extract storage platform context

* refactor: extract mail platform context

* refactor: extract pdf platform context

* refactor: extract operations platform context

* refactor: move installation into operations platform

* refactor: extract money domain context

* refactor: extract taxation domain context

* refactor: extract catalog domain context

* refactor: extract metadata domain context

* refactor: extract reporting domain context

* refactor: extract purchases domain context

* refactor: extract receivables domain context

* refactor: extract accounts domain context

* refactor: complete reporting statement boundary

* refactor: extract contacts domain context

* refactor: extract sales domain context

* refactor: remove legacy application layers

* fix: migrate legacy bouncer role identities
2026-08-05 17:40:03 +02:00

184 lines
7.5 KiB
PHP

<?php
use App\Domains\Accounts\Models\Company;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Receivables\Models\Payment;
use App\Domains\Sales\Models\Invoice;
use App\Platform\Ai\Application\Tools\RankTopCustomersTool;
use Illuminate\Support\Facades\Artisan;
beforeEach(function () {
Artisan::call('db:seed', ['--class' => 'DatabaseSeeder', '--force' => true]);
Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]);
});
test('rank_top_customers ranks by invoiced_total correctly', function () {
$company = Company::first();
$bigSpender = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Big Spender']);
$midSpender = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Mid Spender']);
$smallSpender = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Small Spender']);
// Big: 2 invoices totalling 100000. Mid: 1 invoice totalling 50000. Small: 1 invoice totalling 10000.
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $bigSpender->id, 'total' => 60000]);
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $bigSpender->id, 'total' => 40000]);
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $midSpender->id, 'total' => 50000]);
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $smallSpender->id, 'total' => 10000]);
$result = (new RankTopCustomersTool)->execute(
['metric' => 'invoiced_total', 'limit' => 3],
$company->id,
1,
);
expect($result['metric'])->toBe('invoiced_total');
expect($result['customers'])->toHaveCount(3);
expect($result['customers'][0]['name'])->toBe('Big Spender');
expect($result['customers'][0]['metric_value'])->toBe(100000.0);
expect($result['customers'][0]['invoice_count'])->toBe(2);
expect($result['customers'][1]['name'])->toBe('Mid Spender');
expect($result['customers'][2]['name'])->toBe('Small Spender');
});
test('rank_top_customers ranks by invoice_count correctly', function () {
$company = Company::first();
$busy = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Busy Bee']);
$quiet = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Quiet One']);
// Busy has 5 tiny invoices, Quiet has 1 big one.
for ($i = 0; $i < 5; $i++) {
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $busy->id, 'total' => 100]);
}
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $quiet->id, 'total' => 99999]);
$result = (new RankTopCustomersTool)->execute(
['metric' => 'invoice_count'],
$company->id,
1,
);
expect($result['customers'][0]['name'])->toBe('Busy Bee');
expect($result['customers'][0]['metric_value'])->toBe(5);
expect($result['customers'][1]['name'])->toBe('Quiet One');
expect($result['customers'][1]['metric_value'])->toBe(1);
});
test('rank_top_customers ranks by outstanding_balance and ignores period', function () {
$company = Company::first();
$debtor = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Owes A Lot']);
$upToDate = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Up To Date']);
// Debtor has an unpaid invoice with 75000 due_amount.
Invoice::factory()->create([
'company_id' => $company->id,
'customer_id' => $debtor->id,
'total' => 100000,
'due_amount' => 75000,
'paid_status' => 'PARTIALLY_PAID',
]);
// Up To Date has only a fully-paid invoice — should NOT appear.
Invoice::factory()->create([
'company_id' => $company->id,
'customer_id' => $upToDate->id,
'total' => 50000,
'due_amount' => 0,
'paid_status' => 'PAID',
]);
// Pass an obviously wrong period — outstanding_balance should ignore it.
$result = (new RankTopCustomersTool)->execute(
['metric' => 'outstanding_balance', 'period' => 'today'],
$company->id,
1,
);
expect($result['period'])->toBe('current');
expect(collect($result['customers'])->pluck('name'))
->toContain('Owes A Lot')
->not->toContain('Up To Date');
});
test('rank_top_customers respects the limit parameter and default', function () {
$company = Company::first();
// 7 customers, each with one invoice
for ($i = 0; $i < 7; $i++) {
$c = Customer::factory()->create(['company_id' => $company->id]);
Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $c->id, 'total' => 1000]);
}
$tool = new RankTopCustomersTool;
// Default limit is 5
$result = $tool->execute(['metric' => 'invoiced_total'], $company->id, 1);
expect($result['customers'])->toHaveCount(5);
// Explicit limit 2
$result = $tool->execute(['metric' => 'invoiced_total', 'limit' => 2], $company->id, 1);
expect($result['customers'])->toHaveCount(2);
// Over-the-cap limit is clamped to 20
$result = $tool->execute(['metric' => 'invoiced_total', 'limit' => 999], $company->id, 1);
expect(count($result['customers']))->toBeLessThanOrEqual(20);
});
test('rank_top_customers does not leak across companies', function () {
$companyA = Company::first();
$companyB = Company::factory()->create();
$customerA = Customer::factory()->create(['company_id' => $companyA->id, 'name' => 'Company A Cust']);
$customerB = Customer::factory()->create(['company_id' => $companyB->id, 'name' => 'Company B Cust']);
Invoice::factory()->create(['company_id' => $companyA->id, 'customer_id' => $customerA->id, 'total' => 5000]);
Invoice::factory()->create(['company_id' => $companyB->id, 'customer_id' => $customerB->id, 'total' => 999999]);
// Call with companyA — should only see companyA's customer
$result = (new RankTopCustomersTool)->execute(
['metric' => 'invoiced_total'],
$companyA->id,
1,
);
expect(collect($result['customers'])->pluck('name'))
->toContain('Company A Cust')
->not->toContain('Company B Cust');
});
test('rank_top_customers rejects an invalid metric', function () {
$company = Company::first();
$result = (new RankTopCustomersTool)->execute(
['metric' => 'not_a_metric'],
$company->id,
1,
);
expect($result)->toHaveKey('error', 'invalid_metric');
});
test('rank_top_customers ranks by paid_total using payment records', function () {
$company = Company::first();
$topPayer = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Top Payer']);
$lowPayer = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Low Payer']);
// Top Payer has 2 payments totalling 80000. Low Payer has 1 payment for 20000.
Payment::factory()->create(['company_id' => $company->id, 'customer_id' => $topPayer->id, 'amount' => 50000]);
Payment::factory()->create(['company_id' => $company->id, 'customer_id' => $topPayer->id, 'amount' => 30000]);
Payment::factory()->create(['company_id' => $company->id, 'customer_id' => $lowPayer->id, 'amount' => 20000]);
$result = (new RankTopCustomersTool)->execute(
['metric' => 'paid_total'],
$company->id,
1,
);
expect($result['customers'][0]['name'])->toBe('Top Payer');
expect($result['customers'][0]['metric_value'])->toBe(80000.0);
expect($result['customers'][1]['name'])->toBe('Low Payer');
expect($result['customers'][1]['metric_value'])->toBe(20000.0);
});