Files
InvoiceShelf/tests/Feature/DomainSpec/MoneyDomainTest.php
T
Darko Gjorgjijoski 605d700f75 test: add the behavioral spec suites (pilot + ten domains)
98 tests pinning observable behavior ahead of the authorship rewrite.
2026-08-20 17:19:26 +02:00

127 lines
6.0 KiB
PHP

<?php
// Domain behavioural suite — Money (spec: money-domain-spec.md).
use App\Domains\Accounts\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Laravel\Sanctum\Sanctum;
use function Pest\Laravel\deleteJson;
use function Pest\Laravel\getJson;
use function Pest\Laravel\postJson;
beforeEach(function () {
Artisan::call('db:seed', ['--class' => 'DatabaseSeeder', '--force' => true]);
$user = User::where('role', 'super admin')->first();
$this->companyId = $user->companies()->first()->id;
$this->withHeaders(['company' => $this->companyId]);
Sanctum::actingAs($user, ['*']);
});
it('lists currencies common-first, then the rest by name', function () {
$codes = collect(getJson('/api/v1/currencies')->assertOk()->json('data'))->pluck('code');
expect($codes->take(10)->values()->all())
->toBe(['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'CNY', 'INR', 'BRL']);
$restNames = collect(getJson('/api/v1/currencies')->json('data'))->skip(10)->pluck('name')->values();
expect($restNames->all())->toBe($restNames->sort()->values()->all());
});
it('creates a provider after live validation and enforces the one-active-provider-per-currency rule', function () {
Http::fake(['api.currencyfreaks.com/*' => Http::response(['rates' => ['INR' => '83.1']])]);
postJson('/api/v1/exchange-rate-providers', [
'driver' => 'currency_freak', 'key' => 'k1', 'currencies' => ['USD'], 'active' => true,
])->assertSuccessful();
postJson('/api/v1/exchange-rate-providers', [
'driver' => 'currency_freak', 'key' => 'k2', 'currencies' => ['USD'], 'active' => true,
])->assertStatus(422)->assertJson(['error' => 'currency_used']);
});
it('maps an invalid provider key to the invalid-key error', function () {
Http::fake(['api.currencyfreaks.com/*' => Http::response([
'success' => false, 'error' => ['status' => 404, 'message' => 'bad key'],
])]);
postJson('/api/v1/exchange-rate-providers', [
'driver' => 'currency_freak', 'key' => 'bad', 'currencies' => ['USD'], 'active' => true,
])->assertStatus(422)->assertJson(['error' => 'invalid_key']);
});
it('refuses to delete an active provider and deletes an inactive one', function () {
Http::fake(['api.currencyfreaks.com/*' => Http::response(['rates' => ['INR' => '83.1']])]);
$id = postJson('/api/v1/exchange-rate-providers', [
'driver' => 'currency_freak', 'key' => 'k', 'currencies' => ['USD'], 'active' => true,
])->json('data.id');
deleteJson("/api/v1/exchange-rate-providers/{$id}")
->assertStatus(422)->assertJson(['error' => 'provider_active']);
DB::table('exchange_rate_providers')->where('id', $id)->update(['active' => false]);
deleteJson("/api/v1/exchange-rate-providers/{$id}")->assertOk()->assertJson(['success' => true]);
});
it('resolves rates live first, then from the log, then reports none', function () {
$usd = DB::table('currencies')->where('code', 'USD')->value('id');
$base = DB::table('company_settings')->where('company_id', $this->companyId)
->where('option', 'currency')->value('value');
getJson("/api/v1/currencies/{$usd}/exchange-rate")
->assertOk()->assertJson(['error' => 'no_exchange_rate_available']);
DB::table('exchange_rate_logs')->insert([
'exchange_rate' => 82.5, 'base_currency_id' => $usd, 'currency_id' => $base,
'company_id' => $this->companyId, 'created_at' => now(), 'updated_at' => now(),
]);
getJson("/api/v1/currencies/{$usd}/exchange-rate")
->assertOk()->assertJsonPath('exchangeRate.0', 82.5);
Http::fake(['api.currencyfreaks.com/*' => Http::response(['rates' => ['INR' => '99.9']])]);
postJson('/api/v1/exchange-rate-providers', [
'driver' => 'currency_freak', 'key' => 'k', 'currencies' => ['USD'], 'active' => true,
])->assertSuccessful();
getJson("/api/v1/currencies/{$usd}/exchange-rate")
->assertOk()->assertJsonPath('exchangeRate.0', '99.9');
});
it('gates the historical backfill and reproduces its defective arithmetic', function () {
$usd = DB::table('currencies')->where('code', 'USD')->value('id');
$customerId = postJson('/api/v1/customers', ['name' => 'Backfill Co', 'currency_id' => $usd])
->assertSuccessful()->json('data.id');
$invoice = postJson('/api/v1/invoices', [
'invoice_date' => '2026-01-10', 'customer_id' => $customerId,
'invoice_number' => 'INV-000001', 'discount' => 0, 'discount_val' => 0,
'sub_total' => 1000, 'total' => 1000, 'tax' => 0, 'template_name' => 'invoice1',
'exchange_rate' => 3, 'currency_id' => $usd,
'items' => [['name' => 'Thing', 'quantity' => 1, 'price' => 1000, 'description' => '',
'discount_type' => 'fixed', 'discount' => 0, 'discount_val' => 0, 'tax' => 0, 'total' => 1000]],
])->assertSuccessful()->json('data');
// The gate: any value other than NO means nothing happens.
postJson('/api/v1/currencies/bulk-update-exchange-rate', [
'currencies' => [['id' => $usd, 'exchange_rate' => 2]],
])->assertOk()->assertExactJson(['error' => false]);
DB::table('company_settings')->where('company_id', $this->companyId)
->where('option', 'bulk_exchange_rate_configured')->update(['value' => 'NO']);
DB::table('invoices')->where('id', $invoice['id'])->update(['exchange_rate' => null]);
postJson('/api/v1/currencies/bulk-update-exchange-rate', [
'currencies' => [['id' => $usd, 'exchange_rate' => 2]],
])->assertOk()->assertJson(['success' => true]);
$row = DB::table('invoices')->where('id', $invoice['id'])->first();
expect((float) $row->exchange_rate)->toBe(2.0);
expect((int) $row->base_sub_total)->toBe(2000);
expect((int) $row->base_total)->toBe(2000);
// Defect, kept deliberately: base discount sourced from the sub-total.
expect((int) $row->base_discount_val)->toBe(2000);
expect(DB::table('company_settings')->where('company_id', $this->companyId)
->where('option', 'bulk_exchange_rate_configured')->value('value'))->toBe('YES');
});