*/ public array $duplicates = []; public function __construct(private readonly StripeClient $stripe) {} /** * The id of an existing Stripe Product to use instead of creating one. * * @param array $metadata what the create call would send * @param array $identifying metadata keys that mark a Product as ours */ public function __invoke(array $metadata, array $identifying): ?string { $metadata = array_map(fn ($value) => (string) $value, $metadata); $candidates = []; foreach ($this->stripe->activeProducts() as $product) { if ($this->contradicts($product['metadata'], $metadata)) { continue; } if (! $this->confirms($product['metadata'], $metadata, $identifying)) { continue; } $candidates[] = $product; } if ($candidates === []) { return null; } // Oldest first, and on a tie the lower id, so two runs agree. The tie is // not merely theoretical: createProduct()'s counter and plantProduct()'s // default both start at 1, so a minted Product and a planted orphan can // share a `created` — the same collision AdoptStripePrice's usort breaks // the same way, documented at FakeStripeClient::createPrice(). usort($candidates, fn (array $a, array $b) => [$a['created'], $a['id']] <=> [$b['created'], $b['id']]); $adopted = array_shift($candidates); foreach ($candidates as $duplicate) { $this->duplicates[] = $duplicate['id']; Log::warning('stripe: a second product claims to be the same thing — left alone, not deactivated', [ 'product' => $duplicate['id'], 'adopted' => $adopted['id'], 'metadata' => $metadata, ]); } Log::info('stripe: adopted an existing product instead of creating a second one', [ 'product' => $adopted['id'], 'metadata' => $metadata, ]); return $adopted['id']; } /** * @param array $found * @param array $expected */ private function contradicts(array $found, array $expected): bool { foreach ($expected as $key => $value) { if (array_key_exists($key, $found) && $found[$key] !== $value) { return true; } } return false; } /** * Failing to contradict is not enough — an empty metadata bag contradicts * nothing, and this list holds every Product on the account. * * @param array $found * @param array $expected * @param array $identifying */ private function confirms(array $found, array $expected, array $identifying): bool { foreach ($identifying as $key) { if (isset($found[$key]) && $found[$key] === ($expected[$key] ?? null)) { return true; } } return false; } }