92 lines
3.7 KiB
PHP
92 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Billing;
|
|
|
|
use App\Support\CompanyProfile;
|
|
|
|
/**
|
|
* The one-off charge for setting a customer up — and the one place it is turned
|
|
* into money.
|
|
*
|
|
* The fee had been advertised for months and charged to nobody. It was on the
|
|
* public price sheet ("Einrichtung kostet einmalig 118,80 €"), on the booking page
|
|
* ("zzgl. Einrichtung einmalig"), and the operator set the figure in the console —
|
|
* and CompanyProfile::setupFeeCents() had no reader outside those two sentences
|
|
* and the form that wrote it. Nothing put it on a checkout, on an invoice item or
|
|
* on any bill at all.
|
|
*
|
|
* It is charged now. The two sentences and the money read the same figure through
|
|
* this class, so a change of rate or of fee cannot move one without moving the
|
|
* others.
|
|
*
|
|
* **What is charged for it follows the same rule as a package.** The console asks
|
|
* the operator for the NET amount, because that is the commercial number and the
|
|
* field says so. What a customer pays is that amount put through their own
|
|
* TaxTreatment: the domestic gross for everybody who is charged VAT, so the
|
|
* figure at the till is the figure the price sheet quoted — and the bare net for
|
|
* a business in another member state whose VAT id is verified, because reverse
|
|
* charge means no VAT is owed to us on the fee either. A fee is a supply like any
|
|
* other; taxing it differently from the package on the same invoice would be a
|
|
* document at two rates.
|
|
*
|
|
* The public price sheet quotes the domestic gross, because it is read by
|
|
* visitors nobody has met — TaxTreatment::advertisedCents(), the same figure the
|
|
* ordinary checkout takes.
|
|
*
|
|
* Zero means there is no such fee. Every reader has to treat that as "no line at
|
|
* all" rather than "a line of nought" — a checkout that itemises nothing owed is
|
|
* worse than one that says nothing.
|
|
*/
|
|
final class SetupFee
|
|
{
|
|
/** The net figure an operator typed, in cents. Zero when there is no fee. */
|
|
public static function netCents(): int
|
|
{
|
|
return CompanyProfile::setupFeeCents();
|
|
}
|
|
|
|
/**
|
|
* What a customer treated so is actually charged for it.
|
|
*
|
|
* The treatment is a required argument rather than the domestic one assumed,
|
|
* so that every call site has to say whose fee it is quoting. The page that
|
|
* quotes it to a stranger passes TaxTreatment::domestic(), and says so.
|
|
*/
|
|
public static function chargedCents(TaxTreatment $treatment): int
|
|
{
|
|
$net = self::netCents();
|
|
|
|
return $net === 0 ? 0 : $treatment->chargeCents($net);
|
|
}
|
|
|
|
/**
|
|
* The fee as the extra, non-recurring line of a checkout session — or null
|
|
* when the operator has set none.
|
|
*
|
|
* `$currency` is the recurring Price's own, not the catalogue default: Stripe
|
|
* requires every line of one session to share a currency, and reading two
|
|
* sources for it is how a checkout ends up refused for a mismatch nobody can
|
|
* see from either page.
|
|
*
|
|
* @return array{amount_cents: int, label: string, currency: string}|null
|
|
*/
|
|
public static function checkoutLine(string $currency, TaxTreatment $treatment): ?array
|
|
{
|
|
$charged = self::chargedCents($treatment);
|
|
|
|
if ($charged === 0) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'amount_cents' => $charged,
|
|
// What the customer reads on Stripe's page and on Stripe's receipt.
|
|
// The same words the price sheet uses, because meeting a different
|
|
// name for the same charge at the till is exactly the moment somebody
|
|
// abandons a checkout.
|
|
'label' => __('checkout.setup_fee_line'),
|
|
'currency' => strtoupper($currency),
|
|
];
|
|
}
|
|
}
|