*/ public array $products = []; /** @var array */ public array $prices = []; /** @var array */ public array $archived = []; /** Idempotency key → the id first returned for it. @var array */ public array $keys = []; /** * What Stripe would answer when asked which item a subscription bills * through: subscription id → item id. * * @var array */ public array $items = []; /** * Every price swap that was asked for, in order, so a test can assert the * proration behaviour as well as the price. * * @var array */ public array $priceChanges = []; /** * The items on each subscription as this Stripe sees them: item id → what * it bills. Modules are added and removed here, so a test can assert how * many items a contract carries and at which quantity. * * @var array */ public array $subscriptionItems = []; /** * Every item call that was made, in order, so a test can assert the * proration behaviour as well as the outcome. * * @var array> */ public array $itemCalls = []; /** * The lines this Stripe would report for an invoice: invoice id → lines. * Only for the invoice whose lines an event did not carry in full. * * @var array>> */ public array $invoiceLines = []; /** * Every refund that was sent, in order, so a test can assert the amount as * well as the fact of it. * * @var array */ public array $refunds = []; /** * What this Stripe would answer when asked which payment an invoice was * settled by: invoice id → payment reference. * * @var array */ public array $invoicePayments = []; /** * Set to make every call fail, the way an outage, a revoked key or a * network without a route out does. */ public ?string $failWith = null; /** * Every checkout that was opened, in order. * * @var array */ public array $checkouts = []; public function isConfigured(): bool { return $this->configured; } public function createCheckoutSession( string $priceId, string $successUrl, string $cancelUrl, array $metadata = [], ?string $customerEmail = null, ?string $idempotencyKey = null, ): string { $this->failIfAsked(); $this->checkouts[] = [ 'price' => $priceId, 'success' => $successUrl, 'cancel' => $cancelUrl, 'metadata' => $metadata, 'email' => $customerEmail, ]; return 'https://checkout.stripe.test/session/'.count($this->checkouts); } public function createProduct(string $name, array $metadata = [], ?string $idempotencyKey = null): string { // Replays the first answer for a repeated key, as Stripe does. if ($idempotencyKey !== null && isset($this->keys[$idempotencyKey])) { return $this->keys[$idempotencyKey]; } $id = 'prod_'.substr(sha1($name.count($this->products)), 0, 12); $this->products[$id] = ['name' => $name, 'metadata' => $metadata]; if ($idempotencyKey !== null) { $this->keys[$idempotencyKey] = $id; } return $id; } public function createPrice( string $productId, int $amountCents, string $currency, string $interval, array $metadata = [], ?string $idempotencyKey = null, ): string { if ($idempotencyKey !== null && isset($this->keys[$idempotencyKey])) { return $this->keys[$idempotencyKey]; } $id = 'price_'.substr(sha1($productId.$amountCents.$interval.count($this->prices)), 0, 12); $this->prices[$id] = [ 'product' => $productId, 'amount' => $amountCents, 'currency' => $currency, 'interval' => $interval, 'metadata' => $metadata, ]; if ($idempotencyKey !== null) { $this->keys[$idempotencyKey] = $id; } return $id; } public function archivePrice(string $priceId): void { $this->archived[] = $priceId; } public function updateSubscriptionPrice( string $subscriptionId, string $itemId, string $priceId, string $prorationBehaviour, ): void { $this->failIfAsked(); $this->priceChanges[] = [ 'subscription' => $subscriptionId, 'item' => $itemId, 'price' => $priceId, 'proration' => $prorationBehaviour, ]; } public function subscriptionItemId(string $subscriptionId): ?string { $this->failIfAsked(); return $this->items[$subscriptionId] ?? null; } public function addSubscriptionItem( string $subscriptionId, string $priceId, int $quantity, string $prorationBehaviour, ?string $idempotencyKey = null, ): string { $this->failIfAsked(); $id = 'si_'.substr(sha1($subscriptionId.$priceId.count($this->subscriptionItems)), 0, 12); $this->subscriptionItems[$id] = [ 'subscription' => $subscriptionId, 'price' => $priceId, 'quantity' => $quantity, ]; $this->itemCalls[] = [ 'call' => 'add', 'subscription' => $subscriptionId, 'item' => $id, 'price' => $priceId, 'quantity' => $quantity, 'proration' => $prorationBehaviour, ]; return $id; } public function updateSubscriptionItemQuantity( string $itemId, int $quantity, string $prorationBehaviour, ): void { $this->failIfAsked(); if (isset($this->subscriptionItems[$itemId])) { $this->subscriptionItems[$itemId]['quantity'] = $quantity; } $this->itemCalls[] = [ 'call' => 'quantity', 'item' => $itemId, 'quantity' => $quantity, 'proration' => $prorationBehaviour, ]; } public function removeSubscriptionItem(string $itemId, string $prorationBehaviour): void { $this->failIfAsked(); unset($this->subscriptionItems[$itemId]); $this->itemCalls[] = [ 'call' => 'remove', 'item' => $itemId, 'proration' => $prorationBehaviour, ]; } public function refund( string $paymentReference, ?int $amountCents = null, ?string $idempotencyKey = null, ): string { $this->failIfAsked(); // Replays the first answer for a repeated key, as Stripe does — which // is the whole point of sending one on a refund. if ($idempotencyKey !== null && isset($this->keys[$idempotencyKey])) { return $this->keys[$idempotencyKey]; } $this->refunds[] = [ 'payment' => $paymentReference, 'amount' => $amountCents, 'key' => $idempotencyKey, ]; $id = 're_'.substr(sha1($paymentReference.count($this->refunds)), 0, 12); if ($idempotencyKey !== null) { $this->keys[$idempotencyKey] = $id; } return $id; } public function invoicePaymentReference(string $invoiceId): ?string { $this->failIfAsked(); return $this->invoicePayments[$invoiceId] ?? null; } public function invoiceLines(string $invoiceId): array { $this->failIfAsked(); return $this->invoiceLines[$invoiceId] ?? []; } /** The module items this Stripe holds for one subscription. */ public function itemsOn(string $subscriptionId): array { return array_filter( $this->subscriptionItems, fn (array $item) => $item['subscription'] === $subscriptionId, ); } private function failIfAsked(): void { if ($this->failWith !== null) { throw new RuntimeException($this->failWith); } } }