59 lines
1.9 KiB
PHP
59 lines
1.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Billing;
|
|
|
|
use App\Models\Customer;
|
|
|
|
/**
|
|
* Which VAT applies to a customer, and why.
|
|
*
|
|
* Only the two clear-cut cases are decided here:
|
|
*
|
|
* - a customer with a VAT ID registered in another EU country pays no VAT to
|
|
* us; the liability shifts to them (reverse charge), and the invoice has to
|
|
* say so;
|
|
* - everyone else is charged the seller's domestic rate.
|
|
*
|
|
* Deliberately NOT handled: cross-border sales to private individuals, which
|
|
* are taxed at the customer's own country's rate under the OSS scheme. Doing
|
|
* that needs a maintained rate table per member state and a tax adviser's sign
|
|
* off, not a guess in a config file — so those customers fall back to the
|
|
* domestic rate, which over-collects rather than under-collects.
|
|
*/
|
|
final readonly class TaxTreatment
|
|
{
|
|
private function __construct(
|
|
public float $rate,
|
|
public bool $reverseCharge,
|
|
) {}
|
|
|
|
public static function for(?Customer $customer): self
|
|
{
|
|
$domestic = (float) config('provisioning.tax.rate_percent', 0) / 100;
|
|
|
|
$vatId = strtoupper(preg_replace('/\s+/', '', (string) $customer?->vat_id) ?? '');
|
|
if ($vatId === '') {
|
|
return new self($domestic, false);
|
|
}
|
|
|
|
// A VAT ID starts with its country's code. Same country as ours means a
|
|
// domestic business sale, which is taxed normally.
|
|
$sellerCountry = strtoupper((string) config('provisioning.tax.seller_country', 'AT'));
|
|
$customerCountry = substr($vatId, 0, 2);
|
|
|
|
return $customerCountry !== '' && $customerCountry !== $sellerCountry
|
|
? new self(0.0, true)
|
|
: new self($domestic, false);
|
|
}
|
|
|
|
public function grossCents(int $netCents): int
|
|
{
|
|
return (int) round($netCents * (1 + $this->rate));
|
|
}
|
|
|
|
public function percentLabel(): string
|
|
{
|
|
return rtrim(rtrim(number_format($this->rate * 100, 1, ',', '.'), '0'), ',');
|
|
}
|
|
}
|