option('dry-run'); if (! $dryRun && ! $stripe->isConfigured()) { $this->error('Stripe is not configured (STRIPE_SECRET is empty). Nothing was created.'); return self::FAILURE; } $created = 0; foreach (PlanFamily::query()->with('versions.prices')->orderBy('tier')->get() as $family) { $published = $family->versions->filter(fn (PlanVersion $version) => $version->isPublished()); // A family whose versions are all drafts has promised nothing, so // it has no business appearing in Stripe's price list yet. if ($published->isEmpty()) { continue; } $productId = $family->stripe_product_id; if ($productId === null) { $this->line(" product {$family->key} — {$family->name}"); $created++; if (! $dryRun) { $productId = $stripe->createProduct( $family->name, ['plan_family' => $family->key, 'plan_family_id' => (string) $family->id], // Keyed on our row, so a crash between Stripe creating // the product and us storing its id gives back the same // product on the next run rather than a second one. idempotencyKey: "clupilot-product-{$family->id}", ); $family->update(['stripe_product_id' => $productId]); } } foreach ($published as $version) { foreach ($version->prices as $price) { if ($price->stripe_price_id !== null) { continue; } $this->line(sprintf( ' price %s v%d %s %d %s', $family->key, $version->version, $price->term, $price->amount_cents, $price->currency, )); $created++; if ($dryRun || $productId === null) { continue; } $priceId = $stripe->createPrice( productId: $productId, amountCents: $price->amount_cents, currency: $price->currency, interval: $price->term === 'yearly' ? 'year' : 'month', metadata: [ 'plan_family' => $family->key, 'plan_version' => (string) $version->version, 'plan_version_id' => (string) $version->id, 'plan_price_id' => (string) $price->id, ], idempotencyKey: "clupilot-price-{$price->id}", ); // Written straight through the query builder: stripe_price_id // is not part of what publication froze, and the model would // otherwise have to be re-read first. PlanPrice::query()->whereKey($price->id)->update(['stripe_price_id' => $priceId]); } } } $this->newLine(); if ($created === 0) { $this->info('Stripe is already in step with the catalogue.'); return self::SUCCESS; } $this->info($dryRun ? "{$created} object(s) would be created. Run without --dry-run to create them." : "{$created} object(s) created in Stripe."); return self::SUCCESS; } /** Versions whose prices are live in Stripe, for the status line. */ public static function syncedVersions(): int { return PlanVersion::query() ->whereNotNull('published_at') ->whereHas('prices', fn ($q) => $q->whereNotNull('stripe_price_id')) ->count(); } }