diff --git a/app/Services/Stripe/FakeStripeClient.php b/app/Services/Stripe/FakeStripeClient.php index c6bb114..34b9f81 100644 --- a/app/Services/Stripe/FakeStripeClient.php +++ b/app/Services/Stripe/FakeStripeClient.php @@ -161,7 +161,12 @@ class FakeStripeClient implements StripeClient public function createProduct(string $name, array $metadata = [], ?string $idempotencyKey = null): string { $key = IdempotencyKey::forProduct($idempotencyKey, $name, $metadata); - $parameters = ['name' => $name, 'metadata' => $metadata]; + + // The same parameters forProduct() just fingerprinted, asked for + // through the one method that knows what they are — a local literal + // here could drift from what the key actually covers without a test + // ever seeing it. + $parameters = IdempotencyKey::productParameters($name, $metadata); // Replays the first answer for a repeated key, as Stripe does. $replayed = $this->replay($key, $parameters); @@ -187,13 +192,11 @@ class FakeStripeClient implements StripeClient ?string $idempotencyKey = null, ): string { $key = IdempotencyKey::forPrice($idempotencyKey, $productId, $amountCents, $currency, $interval, $metadata); - $parameters = [ - 'product' => $productId, - 'unit_amount' => $amountCents, - 'currency' => strtolower($currency), - 'interval' => $interval, - 'metadata' => $metadata, - ]; + + // Same reasoning as createProduct(): the parameters forPrice() just + // fingerprinted, not a second literal that has to be kept in step by + // hand. + $parameters = IdempotencyKey::priceParameters($productId, $amountCents, $currency, $interval, $metadata); $replayed = $this->replay($key, $parameters); diff --git a/app/Services/Stripe/IdempotencyKey.php b/app/Services/Stripe/IdempotencyKey.php index f42b79d..d71f8ea 100644 --- a/app/Services/Stripe/IdempotencyKey.php +++ b/app/Services/Stripe/IdempotencyKey.php @@ -38,19 +38,44 @@ final class IdempotencyKey string $interval, array $metadata, ): ?string { - return self::with($key, [ - 'product' => $productId, - 'unit_amount' => $amountCents, - 'currency' => strtolower($currency), - 'interval' => $interval, - 'metadata' => $metadata, - ]); + return self::with($key, self::priceParameters($productId, $amountCents, $currency, $interval, $metadata)); } /** The key for a Product, for the same reason and against the same trap. */ public static function forProduct(?string $key, string $name, array $metadata): ?string { - return self::with($key, ['name' => $name, 'metadata' => $metadata]); + return self::with($key, self::productParameters($name, $metadata)); + } + + /** + * What counts as "the same call" for a Price — public because + * FakeStripeClient has to ask this exact question too, to decide whether a + * repeated key is a retry or a changed call. Built inline a second time in + * the fake, it would only ever match this by coincidence, and a later + * change to what a Price call fingerprints could drift from what the fake + * detects without a single test noticing — which is exactly the gap this + * task exists to close. + */ + public static function priceParameters( + string $productId, + int $amountCents, + string $currency, + string $interval, + array $metadata, + ): array { + return [ + 'product' => $productId, + 'unit_amount' => $amountCents, + 'currency' => strtolower($currency), + 'interval' => $interval, + 'metadata' => $metadata, + ]; + } + + /** What counts as "the same call" for a Product — same reason as priceParameters(). */ + public static function productParameters(string $name, array $metadata): array + { + return ['name' => $name, 'metadata' => $metadata]; } /**