One source for what counts as the same call

IdempotencyKey::forPrice()/forProduct() built the parameter array they
fingerprint, and FakeStripeClient::createPrice()/createProduct() rebuilt the
identical array literal by hand purely to feed replay()/rememberKey(). Nothing
tied those two literals together — a later change to what a Price call
fingerprints could drift from what the fake checks for a collision, and no
test would catch it, because the HttpStripeClient tests exercise forPrice()
alone and the FakeStripeClient tests exercise its own local array alone.

priceParameters()/productParameters() are now the one answer to "what counts
as the same call"; forPrice()/forProduct() and the fake both ask it instead of
each keeping their own copy. Fingerprint and key format are unchanged — this
is the duplication coming out, not new behaviour. refund(),
cancelSubscription(), addSubscriptionItem() and createCheckoutSession() are
untouched: their local parameter arrays are the fake's own record of the call,
never fingerprinted into a key, so there was nothing to duplicate there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus^2
nexxo 2026-07-30 10:46:46 +02:00
parent 60e93aafd7
commit cdea9e923f
2 changed files with 44 additions and 16 deletions

View File

@ -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);

View File

@ -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];
}
/**