chargeCents($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 ? self::reverseCharge() : $domestic; } public function grossCents(int $netCents): int { return (int) round($netCents * (1 + $this->rate)); } /** * What THIS customer's card is charged for a net catalogue figure. * * The one place the amount taken is formed, and therefore the one place that * decides which of a sellable thing's two Stripe Prices a checkout uses, what * the proof register expects to have been taken, and what a document has to * total to. A second reader forming the figure from a rate of its own is how * the charge, the Price and the invoice come to disagree. * * Written on the reverse-charge flag rather than on the rate, although at a * rate of nought grossCents() would return the same number. The two are * different questions that happen to share an answer here: grossCents() says * how a document is SPLIT, and this says what is TAKEN — and if cross-border * B2C is ever taxed at the customer's own rate under OSS, the two part * company and this must go on charging what was quoted. */ public function chargeCents(int $netCents): int { return $this->reverseCharge ? $netCents : $this->grossCents($netCents); } /** * 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'), ','); } }