Files
InvoiceShelf/tests/Feature/Modules/EloquentCompanyDataReaderTest.php
T
Darko Gjorgjijoski 0b9ae9ea00 refactor(ai): extract assistant into an official module (#749)
* feat(modules): expose host data boundaries

* feat(modules): add frontend extension surfaces

* refactor(ai): remove assistant from core

* chore(ai): prepare the module extraction

* fix(modules): load extension styles after the host bundle

* chore(modules): lock SDK 3.3.0
2026-08-05 22:10:21 +02:00

92 lines
6.1 KiB
PHP

<?php
use App\Domains\Accounts\Models\Company;
use App\Domains\Catalog\Models\Item;
use App\Domains\Catalog\Models\Unit;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Purchases\Models\Expense;
use App\Domains\Purchases\Models\ExpenseCategory;
use App\Domains\Receivables\Models\Payment;
use App\Domains\Sales\Models\Invoice;
use App\Domains\Sales\Models\InvoiceItem;
use App\Platform\Modules\Infrastructure\EloquentCompanyDataReader;
use Carbon\Carbon;
use Illuminate\Support\Facades\Artisan;
beforeEach(function () {
Artisan::call('db:seed', ['--class' => 'DatabaseSeeder', '--force' => true]);
Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]);
});
function hostItem(int $companyId, string $name, int $price): Item
{
return Item::create([
'name' => $name,
'description' => $name.' description',
'price' => $price,
'company_id' => $companyId,
'unit_id' => Unit::query()->where('company_id', $companyId)->value('id'),
'currency_id' => 1,
]);
}
test('company data reader searches and finds customers and invoices without crossing companies', function () {
$reader = new EloquentCompanyDataReader;
$companyA = Company::firstOrFail();
$companyB = Company::factory()->create();
$customerA = Customer::factory()->create(['company_id' => $companyA->id, 'name' => 'A Customer']);
$customerB = Customer::factory()->create(['company_id' => $companyB->id, 'name' => 'B Customer']);
$invoiceA = Invoice::factory()->create(['company_id' => $companyA->id, 'customer_id' => $customerA->id, 'invoice_number' => 'A-100']);
Invoice::factory()->create(['company_id' => $companyB->id, 'customer_id' => $customerB->id, 'invoice_number' => 'B-100']);
expect(collect($reader->searchCustomers($companyA->id, 'Customer', 10))->pluck('name'))->toContain('A Customer')->not->toContain('B Customer')
->and($reader->findCustomer($companyA->id, $customerB->id))->toBeNull()
->and(collect($reader->searchInvoices($companyA->id, null, null, null, 10))->pluck('invoice_number'))->toContain('A-100')->not->toContain('B-100')
->and($reader->findInvoice($companyA->id, $invoiceA->invoice_number)['customer']['id'])->toBe($customerA->id)
->and($reader->findInvoice($companyB->id, $invoiceA->invoice_number))->toBeNull();
});
test('company data reader returns company scoped payment, category, item, and overdue query payloads', function () {
$reader = new EloquentCompanyDataReader;
$companyA = Company::firstOrFail();
$companyB = Company::factory()->create();
$customer = Customer::factory()->create(['company_id' => $companyA->id]);
$category = ExpenseCategory::factory()->create(['company_id' => $companyA->id, 'name' => 'Travel']);
Expense::factory()->create(['company_id' => $companyA->id, 'expense_category_id' => $category->id, 'amount' => 2200, 'expense_date' => now()->toDateString()]);
Payment::factory()->create(['company_id' => $companyA->id, 'customer_id' => $customer->id, 'payment_date' => now()->toDateString(), 'amount' => 1200]);
$item = hostItem($companyA->id, 'Reader Widget', 500);
$invoice = Invoice::factory()->create(['company_id' => $companyA->id, 'customer_id' => $customer->id, 'overdue' => true, 'due_amount' => 500, 'due_date' => now()->subDay()->toDateString()]);
InvoiceItem::factory()->create(['company_id' => $companyA->id, 'invoice_id' => $invoice->id, 'item_id' => $item->id, 'quantity' => 2, 'total' => 1000]);
hostItem($companyB->id, 'Other Company Item', 999);
expect($reader->recentPayments($companyA->id, now()->subDay()->toDateString(), 10))->toHaveCount(1)
->and($reader->expenseCategories($companyA->id))->toContain(['id' => $category->id, 'name' => 'Travel', 'description' => $category->description])
->and(collect($reader->searchItems($companyA->id, 'Widget', 10))->pluck('name'))->toContain('Reader Widget')->not->toContain('Other Company Item')
->and($reader->overdueInvoices($companyA->id, 10)[0]['id'])->toBe($invoice->id);
});
test('company data reader aggregates stats and rankings with date windows and batched related names', function () {
Carbon::setTestNow('2026-08-05 12:00:00');
$reader = new EloquentCompanyDataReader;
$company = Company::firstOrFail();
$customer = Customer::factory()->create(['company_id' => $company->id, 'name' => 'Top Customer']);
$category = ExpenseCategory::factory()->create(['company_id' => $company->id, 'name' => 'Software']);
$item = hostItem($company->id, 'Top Item', 300);
$invoice = Invoice::factory()->create(['company_id' => $company->id, 'customer_id' => $customer->id, 'invoice_date' => '2026-08-03', 'total' => 3000]);
InvoiceItem::factory()->create(['company_id' => $company->id, 'invoice_id' => $invoice->id, 'item_id' => $item->id, 'quantity' => 10, 'total' => 3000]);
Payment::factory()->create(['company_id' => $company->id, 'customer_id' => $customer->id, 'payment_date' => '2026-08-04', 'amount' => 2500]);
Expense::factory()->create(['company_id' => $company->id, 'expense_category_id' => $category->id, 'expense_date' => '2026-08-02', 'amount' => 700]);
$stats = $reader->companyStats($company->id, '2026-08-01', '2026-08-05');
expect($stats['invoices'])->toBe(['count' => 1, 'total' => 3000.0])
->and($stats['payments'])->toBe(['count' => 1, 'total' => 2500.0])
->and($stats['expenses'])->toBe(['count' => 1, 'total' => 700.0])
->and($reader->rankCustomers($company->id, 'invoiced_total', '2026-08-01', '2026-08-05', 5)[0]['name'])->toBe('Top Customer')
->and($reader->rankCustomers($company->id, 'paid_total', '2026-08-01', '2026-08-05', 5)[0]['metric_value'])->toBe(2500.0)
->and($reader->rankCustomers($company->id, 'invoice_count', '2026-08-01', '2026-08-05', 5)[0]['invoice_count'])->toBe(1)
->and($reader->rankCustomers($company->id, 'outstanding_balance', null, null, 5))->toBeArray()
->and($reader->rankExpenseCategories($company->id, '2026-08-01', '2026-08-05', 5)[0]['name'])->toBe('Software')
->and($reader->rankItems($company->id, 'revenue', '2026-08-01', '2026-08-05', 5)[0])->toMatchArray(['name' => 'Top Item', 'revenue' => 3000.0]);
});