166 lines
4.7 KiB
PHP
166 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Stripe;
|
|
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* A Stripe that records what it was asked to do.
|
|
*
|
|
* The catalogue sync has to be exercised for real — it is the thing that must
|
|
* be idempotent and must never mint a second Price for the same row — and that
|
|
* is testable without a network. Ids are deterministic so a test can assert
|
|
* which product a price was attached to.
|
|
*/
|
|
class FakeStripeClient implements StripeClient
|
|
{
|
|
public bool $configured = true;
|
|
|
|
/** @var array<int, array{name: string, metadata: array}> */
|
|
public array $products = [];
|
|
|
|
/** @var array<int, array{product: string, amount: int, currency: string, interval: string, metadata: array}> */
|
|
public array $prices = [];
|
|
|
|
/** @var array<int, string> */
|
|
public array $archived = [];
|
|
|
|
/** Idempotency key → the id first returned for it. @var array<string, string> */
|
|
public array $keys = [];
|
|
|
|
/**
|
|
* What Stripe would answer when asked which item a subscription bills
|
|
* through: subscription id → item id.
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
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<int, array{subscription: string, item: string, price: string, proration: string}>
|
|
*/
|
|
public array $priceChanges = [];
|
|
|
|
/**
|
|
* 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<int, array{price: string, success: string, cancel: string, metadata: array, email: ?string}>
|
|
*/
|
|
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;
|
|
}
|
|
|
|
private function failIfAsked(): void
|
|
{
|
|
if ($this->failWith !== null) {
|
|
throw new RuntimeException($this->failWith);
|
|
}
|
|
}
|
|
}
|