grossCents($netCents); } public static function for(?Customer $customer): self { $domestic = self::domestic(); // Unverified is the normal state, and it must cost the customer nothing // more than the domestic rate they would pay anyway — but it must not // zero the VAT either. Otherwise typing "XX123" is a discount. if ($customer === null || ! $customer->hasVerifiedVatId()) { return $domestic; } // Asked of the recorded type, not of the number. Somebody who has said // they are a private person is a private person, and a VAT ID on their // record — a former sole trader's, a colleague's, a mistake — must not // be able to turn that answer over. Only an explicit "consumer" refuses // here; an unrecorded type falls through and is treated exactly as it // was before this check existed (see the class comment). if ($customer->hasRecordedType() && ! $customer->isBusiness()) { return $domestic; } $vatId = $customer->normalisedVatId(); $country = substr($vatId, 0, 2); $seller = strtoupper((string) config('provisioning.tax.seller_country', 'AT')); // Reverse charge is an intra-EU business rule: it needs a member state // that is not ours, and a number that at least looks like one. $eligible = in_array($country, self::EU_MEMBER_STATES, true) && $country !== $seller && preg_match('/^[A-Z]{2}[0-9A-Z]{8,12}$/', $vatId) === 1; return $eligible ? new self(0.0, true) : $domestic; } public function grossCents(int $netCents): int { return (int) round($netCents * (1 + $this->rate)); } /** * The rate in the form the invoice arithmetic works in — 2000 for 20 %. * * Here rather than at each call site, because the conversion was written out * by hand where an invoice was issued and nowhere else, and a second reader * of `rate` doing its own multiplication is how two documents end up at two * rates. */ public function basisPoints(): int { return (int) round($this->rate * 10000); } public function percentLabel(): string { return rtrim(rtrim(number_format($this->rate * 100, 1, ',', '.'), '0'), ','); } }