option('dry-run'); if (empty($plans)) { $this->warn('No plans found in config/subscriptions.php.'); return Command::SUCCESS; } if ($dryRun) { $this->info('Running in dry-run mode — no changes will be made to Stripe.'); } $this->info('Syncing Stripe prices from config...'); $this->newLine(); $created = 0; $transferred = 0; $skipped = 0; foreach ($plans as $planKey => $plan) { $lookupKey = $plan['stripe_lookup_key'] ?? null; if (! $lookupKey) { $this->warn(" [{$planKey}] No stripe_lookup_key defined — skipping."); $skipped++; continue; } $amountInCents = (int) round($plan['price'] * 100); $billingPeriod = $plan['billing_period'] ?? null; $productId = config('subscriptions.products.pro'); $this->line(" {$plan['name']}"); try { $existing = $this->findPriceByLookupKey($lookupKey); if ($existing) { if ($this->priceMatches($existing, $amountInCents, $currency, $billingPeriod)) { $this->line(" ✓ Already in sync ({$existing->id})"); $skipped++; continue; } $this->line(' ↻ Price changed — creating new price and transferring lookup key...'); if (! $dryRun) { $price = $this->createPrice($productId, $amountInCents, $currency, $billingPeriod, $lookupKey, transferLookupKey: true); $this->line(" ✓ Transferred to {$price->id} ({$this->formatAmount($amountInCents, $currency)}/{$billingPeriod})"); } else { $this->line(" [dry-run] Would create new price and transfer lookup key '{$lookupKey}'"); } $transferred++; } else { $this->line(' + No existing price — creating...'); if (! $dryRun) { $price = $this->createPrice($productId, $amountInCents, $currency, $billingPeriod, $lookupKey, transferLookupKey: false); $this->line(" ✓ Created {$price->id} ({$this->formatAmount($amountInCents, $currency)}/{$billingPeriod})"); } else { $this->line(" [dry-run] Would create price '{$lookupKey}' at {$this->formatAmount($amountInCents, $currency)}/{$billingPeriod}"); } $created++; } } catch (ApiErrorException $e) { $this->error(" ✗ Stripe API error: {$e->getMessage()}"); return Command::FAILURE; } } $this->newLine(); $suffix = $dryRun ? ' (dry-run)' : ''; $this->info("{$created} created, {$transferred} updated, {$skipped} skipped{$suffix}."); return Command::SUCCESS; } /** * @return Price|null */ private function findPriceByLookupKey(string $lookupKey): mixed { $response = Cashier::stripe()->prices->all(['lookup_keys' => [$lookupKey], 'limit' => 1]); return $response->data[0] ?? null; } /** * @throws ApiErrorException */ private function createPrice( string $productId, int $amountInCents, string $currency, ?string $billingPeriod, string $lookupKey, bool $transferLookupKey, ): Price { $params = [ 'product' => $productId, 'unit_amount' => $amountInCents, 'currency' => $currency, 'lookup_key' => $lookupKey, 'transfer_lookup_key' => $transferLookupKey, ]; if ($billingPeriod) { $params['recurring'] = ['interval' => $billingPeriod]; } return Cashier::stripe()->prices->create($params); } /** * @param Price $price */ private function priceMatches(mixed $price, int $amountInCents, string $currency, ?string $billingPeriod): bool { $currencyMatches = strtolower((string) $price->currency) === strtolower($currency); $amountMatches = $price->unit_amount === $amountInCents; $intervalMatches = $price->recurring?->interval === $billingPeriod; return $currencyMatches && $amountMatches && $intervalMatches; } private function formatAmount(int $amountInCents, string $currency): string { $symbol = match (strtolower($currency)) { 'eur' => '€', 'gbp' => '£', 'jpy' => '¥', default => strtoupper($currency).' ', }; return $symbol.number_format($amountInCents / 100, 2); } }