option('dry-run'); $archive = (bool) $this->option('archive'); // Checked on EVERY path, unlike stripe:sync-catalogue's --dry-run, // which touches nothing because everything it compares is already in // our own tables. This command has no local-only mode: the report — // the whole point of it, dry-run or not — is activePricesFor(), a // live call. Skipping this check under --dry-run would not skip the // call, only the graceful error in front of it. if (! $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; } // Said on every path that found something, including after --archive // has already run: the only question asked above is whether a row holds // the id, so an orphan in this list may be one stripe:sync-catalogue // would have adopted. Archiving that one is the failure this class's // docblock describes, and an operator who has just done it needs to // know as much as one who is about to. $this->warn(' Run stripe:sync-catalogue first — an orphan listed here may be one the next sync would adopt.'); $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. Once the sync has run, --archive stops selling what is still listed.", }); 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(); } }