200 lines
7.8 KiB
PHP
200 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Stripe;
|
|
|
|
/**
|
|
* The bit of Stripe we drive ourselves: the catalogue.
|
|
*
|
|
* Everything else about recurring billing — retries, dunning, off-session SCA,
|
|
* invoice numbering — is Stripe's job, and we learn about it through webhooks.
|
|
* Our side owns capability, because Stripe does not know how big the VM should
|
|
* be.
|
|
*/
|
|
interface StripeClient
|
|
{
|
|
/**
|
|
* Settle an upgrade at once: Stripe works out the days left in the term,
|
|
* raises its own invoice for the difference and charges it there and then.
|
|
*
|
|
* Chosen over letting the proration ride on the next cycle invoice because
|
|
* the cycle invoice is the one WE issue a document for, from the contract's
|
|
* frozen price. A renewal invoice that said one figure while Stripe took
|
|
* that figure plus a proration would be a document that does not match the
|
|
* money — see App\Actions\MoveStripeSubscriptionPrice.
|
|
*/
|
|
public const PRORATE_IMMEDIATELY = 'always_invoice';
|
|
|
|
/**
|
|
* Change the price and settle nothing. What the next cycle is billed at
|
|
* changes; no credit and no charge is raised for the switch itself.
|
|
*/
|
|
public const PRORATE_NONE = 'none';
|
|
|
|
/** Whether an account is actually configured. */
|
|
public function isConfigured(): bool;
|
|
|
|
/**
|
|
* Open a hosted checkout for one plan Price and return the URL to send the
|
|
* customer to.
|
|
*
|
|
* Hosted, not a payment form of ours: card details, 3-D Secure and every
|
|
* local payment method are Stripe's problem, and the moment a page of ours
|
|
* touches a card number the whole installation is in PCI scope.
|
|
*
|
|
* `$metadata` is what comes back on the webhook and decides what is built —
|
|
* plan, plan version and datacenter. It is set on the SESSION and on the
|
|
* subscription: the session's copy is what StripeWebhookController reads,
|
|
* the subscription's is what any later billing event carries.
|
|
*/
|
|
public function createCheckoutSession(
|
|
string $priceId,
|
|
string $successUrl,
|
|
string $cancelUrl,
|
|
array $metadata = [],
|
|
?string $customerEmail = null,
|
|
?string $idempotencyKey = null,
|
|
): string;
|
|
|
|
/**
|
|
* Create a Product for a plan family; returns its Stripe id.
|
|
*
|
|
* `$idempotencyKey` is Stripe's own guard: a crash between their creating
|
|
* the object and our storing the id would otherwise mint a second one on
|
|
* the next run, and there is no way to tell duplicates apart afterwards.
|
|
*/
|
|
public function createProduct(string $name, array $metadata = [], ?string $idempotencyKey = null): string;
|
|
|
|
/**
|
|
* Create a Price. Stripe Prices are immutable and carry their own interval,
|
|
* which is why one exists per plan version AND term.
|
|
*/
|
|
public function createPrice(
|
|
string $productId,
|
|
int $amountCents,
|
|
string $currency,
|
|
string $interval,
|
|
array $metadata = [],
|
|
?string $idempotencyKey = null,
|
|
): string;
|
|
|
|
/** Stop a Price being offered. The Price itself stays, as Stripe requires. */
|
|
public function archivePrice(string $priceId): void;
|
|
|
|
/**
|
|
* Move a subscription onto another Price.
|
|
*
|
|
* The ITEM is what carries a price, not the subscription, so the swap needs
|
|
* both ids. `$prorationBehaviour` is one of the constants above and is
|
|
* always passed explicitly: Stripe's own default settles a proration
|
|
* silently, and a default deciding what a customer is charged is exactly
|
|
* the kind of thing that must be written down at the call site.
|
|
*/
|
|
public function updateSubscriptionPrice(
|
|
string $subscriptionId,
|
|
string $itemId,
|
|
string $priceId,
|
|
string $prorationBehaviour,
|
|
): void;
|
|
|
|
/**
|
|
* The id of the item a subscription bills its PACKAGE through, or null if it
|
|
* has none.
|
|
*
|
|
* For contracts opened before the item id was stored: Stripe knows it, we
|
|
* did not ask, and asking once is cheaper than being unable to move them.
|
|
* The first item is the package, because that is the one the checkout
|
|
* created; modules are added afterwards and carry their own ids, which are
|
|
* stored on the bookings themselves.
|
|
*/
|
|
public function subscriptionItemId(string $subscriptionId): ?string;
|
|
|
|
/**
|
|
* Put a module on the subscription as an item of its own, and return the
|
|
* item's id.
|
|
*
|
|
* `$prorationBehaviour` is passed explicitly for the same reason as above.
|
|
* A module booked in the middle of a term is charged PRORATE_IMMEDIATELY:
|
|
* Stripe works out the days that are left, raises an invoice for exactly
|
|
* that and takes the money there and then — which is the invoice the
|
|
* customer is owed a document for.
|
|
*/
|
|
public function addSubscriptionItem(
|
|
string $subscriptionId,
|
|
string $priceId,
|
|
int $quantity,
|
|
string $prorationBehaviour,
|
|
?string $idempotencyKey = null,
|
|
): string;
|
|
|
|
/**
|
|
* Change how many of a module the subscription bills for.
|
|
*
|
|
* A pack is sold by the unit, so a second one is a quantity of two on one
|
|
* item rather than a second item at the same price — two items on one price
|
|
* is a thing Stripe allows and nothing could later tell apart.
|
|
*/
|
|
public function updateSubscriptionItemQuantity(
|
|
string $itemId,
|
|
int $quantity,
|
|
string $prorationBehaviour,
|
|
): void;
|
|
|
|
/**
|
|
* Take a module off the subscription.
|
|
*
|
|
* Removed with PRORATE_NONE by everything that calls it: a cancelled module
|
|
* is kept until the end of the term the customer has already paid for and
|
|
* earns no credit, so nothing is to be settled — only the next cycle is
|
|
* smaller.
|
|
*/
|
|
public function removeSubscriptionItem(string $itemId, string $prorationBehaviour): void;
|
|
|
|
/**
|
|
* Send money back, against the payment that took it.
|
|
*
|
|
* The only outgoing money movement this platform makes, and it exists
|
|
* because a consumer who withdraws within fourteen days is owed one — see
|
|
* App\Actions\WithdrawContract. Partial by design: `$amountCents` is what
|
|
* goes back after the pro-rata value of the service already delivered has
|
|
* been kept, and only a withdrawal on the very first day ever equals the
|
|
* whole payment.
|
|
*
|
|
* `$paymentReference` is a PaymentIntent (`pi_…`) or a Charge (`ch_…`) —
|
|
* whichever Stripe reported for the payment. Both are accepted rather than
|
|
* one normalised: which of the two an account produces depends on the API
|
|
* version it is pinned to, and a refund that silently does nothing because
|
|
* the wrong field was read is the worst possible failure here.
|
|
*
|
|
* `$idempotencyKey` is not optional in practice. A refund is the one call
|
|
* whose retry after a timeout would send the customer's money twice.
|
|
*
|
|
* @return string the refund's id
|
|
*/
|
|
public function refund(
|
|
string $paymentReference,
|
|
?int $amountCents = null,
|
|
?string $idempotencyKey = null,
|
|
): string;
|
|
|
|
/**
|
|
* The payment behind a Stripe invoice, or null if it has none.
|
|
*
|
|
* A subscription checkout charges through an invoice, so the invoice id is
|
|
* usually all we have written down — and a refund cannot be issued against
|
|
* an invoice. This is the one hop between the two.
|
|
*/
|
|
public function invoicePaymentReference(string $invoiceId): ?string;
|
|
|
|
/**
|
|
* Every line on a Stripe invoice, for the document we owe it.
|
|
*
|
|
* The webhook payload carries the lines already; this is for the invoice
|
|
* that has more of them than Stripe puts in an event. A document missing a
|
|
* line that was charged is worse than one nobody sent, so the complete list
|
|
* is worth an extra call.
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function invoiceLines(string $invoiceId): array;
|
|
}
|