php-commerce/tests/Feature/TaxServiceTest.php
Snider 8f27fe85c3 refactor: update Tenant module imports after namespace migration
Updates all references from Core\Mod\Tenant to Core\Tenant following
the monorepo separation. The Tenant module now lives in its own package
with the simplified namespace.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:39:12 +00:00

239 lines
7.5 KiB
PHP

<?php
use Core\Mod\Commerce\Models\TaxRate;
use Core\Mod\Commerce\Services\TaxService;
use Core\Tenant\Models\Workspace;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
$this->workspace = Workspace::factory()->create([
'billing_country' => 'GB',
'billing_state' => null,
'vat_number' => null,
]);
// Seed essential tax rates
TaxRate::create([
'country_code' => 'GB',
'name' => 'UK VAT',
'type' => 'vat',
'rate' => 20.00,
'is_digital_services' => true,
'effective_from' => '2020-01-01',
'is_active' => true,
]);
TaxRate::create([
'country_code' => 'DE',
'name' => 'Germany VAT',
'type' => 'vat',
'rate' => 19.00,
'is_digital_services' => true,
'effective_from' => '2020-01-01',
'is_active' => true,
]);
TaxRate::create([
'country_code' => 'US',
'state_code' => 'TX',
'name' => 'Texas Sales Tax',
'type' => 'sales_tax',
'rate' => 6.25,
'is_digital_services' => true,
'effective_from' => '2020-01-01',
'is_active' => true,
]);
TaxRate::create([
'country_code' => 'US',
'name' => 'US Federal (No Tax)',
'type' => 'sales_tax',
'rate' => 0.00,
'is_digital_services' => true,
'effective_from' => '2020-01-01',
'is_active' => true,
]);
TaxRate::create([
'country_code' => 'AU',
'name' => 'Australia GST',
'type' => 'gst',
'rate' => 10.00,
'is_digital_services' => true,
'effective_from' => '2020-01-01',
'is_active' => true,
]);
$this->service = app(TaxService::class);
});
describe('TaxService', function () {
describe('calculate() method', function () {
it('calculates UK VAT at 20%', function () {
$this->workspace->update(['billing_country' => 'GB']);
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->taxRate)->toBe(20.0)
->and($result->taxAmount)->toBe(20.00)
->and($result->jurisdiction)->toBe('GB')
->and($result->taxType)->toBe('vat');
});
it('calculates German VAT at 19%', function () {
$this->workspace->update(['billing_country' => 'DE']);
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->taxRate)->toBe(19.0)
->and($result->taxAmount)->toBe(19.00)
->and($result->jurisdiction)->toBe('DE');
});
it('calculates Texas sales tax at 6.25%', function () {
$this->workspace->update([
'billing_country' => 'US',
'billing_state' => 'TX',
]);
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->taxRate)->toBe(6.25)
->and($result->taxAmount)->toBe(6.25)
->and($result->jurisdiction)->toBe('US-TX');
});
it('falls back to federal rate for US states without specific rate', function () {
$this->workspace->update([
'billing_country' => 'US',
'billing_state' => 'MT', // Montana - no state sales tax
]);
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->taxRate)->toBe(0.0)
->and($result->taxAmount)->toBe(0.00);
});
it('calculates Australian GST at 10%', function () {
$this->workspace->update(['billing_country' => 'AU']);
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->taxRate)->toBe(10.0)
->and($result->taxAmount)->toBe(10.00)
->and($result->taxType)->toBe('gst');
});
it('returns zero tax for countries without rates', function () {
$this->workspace->update(['billing_country' => 'XX']); // Unknown
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->taxRate)->toBe(0.0)
->and($result->taxAmount)->toBe(0.00);
});
it('rounds tax amount to two decimal places', function () {
$this->workspace->update(['billing_country' => 'GB']);
$result = $this->service->calculate($this->workspace, 33.33);
expect($result->taxAmount)->toBe(6.67); // 33.33 * 0.20 = 6.666
});
});
describe('B2B reverse charge', function () {
it('applies zero rate for valid EU VAT numbers', function () {
$this->workspace->update([
'billing_country' => 'DE',
'tax_id' => 'DE123456789',
]);
$result = $this->service->calculate($this->workspace, 100.00);
expect($result->isExempt)->toBeTrue()
->and($result->taxAmount)->toBe(0.00)
->and($result->exemptionReason)->toContain('reverse charge');
});
it('does not apply reverse charge for UK to UK sales', function () {
$this->workspace->update([
'billing_country' => 'GB',
'tax_id' => 'GB123456789',
]);
$result = $this->service->calculate($this->workspace, 100.00);
// UK to UK is not reverse charge
expect($result->taxRate)->toBe(20.0)
->and($result->taxAmount)->toBe(20.00);
});
});
describe('getRateForLocation() method', function () {
it('returns rate for country', function () {
$rate = $this->service->getRateForLocation('GB');
expect($rate)->not->toBeNull()
->and((float) $rate->rate)->toBe(20.00);
});
it('returns state-specific rate when available', function () {
$rate = $this->service->getRateForLocation('US', 'TX');
expect($rate)->not->toBeNull()
->and((float) $rate->rate)->toBe(6.25)
->and($rate->state_code)->toBe('TX');
});
it('returns null for unknown location', function () {
$rate = $this->service->getRateForLocation('XX');
expect($rate)->toBeNull();
});
});
});
describe('TaxRate model', function () {
it('calculates tax correctly', function () {
$rate = TaxRate::where('country_code', 'GB')->first();
expect($rate->calculateTax(100.00))->toBe(20.00)
->and($rate->calculateTax(50.00))->toBe(10.00);
});
it('checks if rate is effective', function () {
$rate = TaxRate::where('country_code', 'GB')->first();
expect($rate->isEffective())->toBeTrue();
// Create a future rate
$futureRate = TaxRate::create([
'country_code' => 'ZZ',
'name' => 'Future Tax',
'type' => 'vat',
'rate' => 25.00,
'is_digital_services' => true,
'effective_from' => now()->addYear(),
'is_active' => true,
]);
expect($futureRate->isEffective())->toBeFalse();
});
it('scopes to effective rates', function () {
$effectiveRates = TaxRate::effective()->get();
expect($effectiveRates->count())->toBeGreaterThan(0);
expect($effectiveRates->every(fn ($r) => $r->isEffective()))->toBeTrue();
});
it('finds rate for location', function () {
$rate = TaxRate::findForLocation('GB');
expect($rate)->not->toBeNull()
->and($rate->country_code)->toBe('GB');
});
});