CluPilotCloud/app/Services/Billing/SetupFee.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),
];
}
}