option('dry-run'); $archive = (bool) $this->option('archive'); if (! $dryRun && ! $stripe->isConfigured()) { $this->error('Stripe is not configured (STRIPE_SECRET is empty). Nothing was read.'); return self::FAILURE; } $known = StripeAddonPrice::query()->pluck('stripe_price_id') ->merge(StripePlanPrice::query()->pluck('stripe_price_id')) ->filter() ->flip(); $found = 0; foreach ($this->products() as $productId) { foreach ($stripe->activePricesFor($productId) as $price) { if ($known->has($price['id'])) { continue; } $found++; $this->line(sprintf( ' orphan %s %d %s %s product %s %s', $price['id'], $price['unit_amount'], $price['currency'], $price['interval'], $productId, $price['metadata'] === [] ? 'no metadata' : json_encode($price['metadata']), )); if ($archive && ! $dryRun) { $stripe->archivePrice($price['id']); } } } $this->newLine(); if ($found === 0) { $this->info('Every active price at our products is accounted for.'); return self::SUCCESS; } $this->info(match (true) { $dryRun && $archive => "{$found} orphan(s) would be archived. Run without --dry-run to archive them.", $archive => "{$found} orphan(s) archived. Existing subscriptions on them keep billing.", default => "{$found} orphan(s) found. Run with --archive to stop selling them.", }); return self::SUCCESS; } /** * Every Stripe Product this platform owns: one per plan family, one per * module. Read from our own rows rather than from Stripe, because a Product * we do not know is not one whose Prices we can judge. * * @return array */ private function products(): array { return PlanFamily::query()->whereNotNull('stripe_product_id')->pluck('stripe_product_id') ->merge(StripeAddonPrice::query()->pluck('stripe_product_id')) ->filter() ->unique() ->values() ->all(); } }