refactor(cloud-commands): consolidate and enhance subscription management commands
- Deleted obsolete CloudCheckSubscription, CloudCleanupSubscriptions, and CloudDeleteUser commands to streamline the codebase. - Introduced new CloudDeleteUser and CloudFixSubscription commands with improved functionality for user deletion and subscription management. - Enhanced subscription handling with options for fixing canceled subscriptions and verifying active subscriptions against Stripe, improving overall command usability and control.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands\Cloud;
|
||||||
|
|
||||||
use App\Actions\Stripe\CancelSubscription;
|
use App\Actions\Stripe\CancelSubscription;
|
||||||
use App\Actions\User\DeleteUserResources;
|
use App\Actions\User\DeleteUserResources;
|
879
app/Console/Commands/Cloud/CloudFixSubscription.php
Normal file
879
app/Console/Commands/Cloud/CloudFixSubscription.php
Normal file
@@ -0,0 +1,879 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Cloud;
|
||||||
|
|
||||||
|
use App\Models\Team;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CloudFixSubscription extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'cloud:fix-subscription
|
||||||
|
{--fix-canceled-subs : Fix canceled subscriptions in database}
|
||||||
|
{--verify-all : Verify all active subscriptions against Stripe}
|
||||||
|
{--fix-verified : Fix discrepancies found during verification}
|
||||||
|
{--dry-run : Show what would be fixed without making changes}
|
||||||
|
{--one : Only fix the first found subscription}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Fix Cloud subscriptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||||
|
|
||||||
|
if ($this->option('verify-all')) {
|
||||||
|
return $this->verifyAllActiveSubscriptions($stripe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('fix-canceled-subs') || $this->option('dry-run')) {
|
||||||
|
return $this->fixCanceledSubscriptions($stripe);
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
|
||||||
|
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
// CSV header
|
||||||
|
fputcsv($out, [
|
||||||
|
'team_id',
|
||||||
|
'invoice_status',
|
||||||
|
'stripe_customer_url',
|
||||||
|
'stripe_subscription_id',
|
||||||
|
'subscription_status',
|
||||||
|
'subscription_url',
|
||||||
|
'note',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($activeSubscribers as $team) {
|
||||||
|
$stripeSubscriptionId = $team->subscription->stripe_subscription_id;
|
||||||
|
$stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
|
||||||
|
$stripeCustomerId = $team->subscription->stripe_customer_id;
|
||||||
|
|
||||||
|
if (! $stripeSubscriptionId && str($stripeInvoicePaid)->lower() != 'past_due') {
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$stripeInvoicePaid,
|
||||||
|
$stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'Missing subscription ID while invoice not past_due',
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $stripeSubscriptionId) {
|
||||||
|
// No subscription ID and invoice is past_due, still record for visibility
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$stripeInvoicePaid,
|
||||||
|
$stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'Missing subscription ID',
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
|
||||||
|
if ($subscription->status === 'active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$stripeInvoicePaid,
|
||||||
|
$stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null,
|
||||||
|
$stripeSubscriptionId,
|
||||||
|
$subscription->status,
|
||||||
|
"https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}",
|
||||||
|
'Subscription not active',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix canceled subscriptions in the database
|
||||||
|
*/
|
||||||
|
private function fixCanceledSubscriptions(\Stripe\StripeClient $stripe)
|
||||||
|
{
|
||||||
|
$isDryRun = $this->option('dry-run');
|
||||||
|
$checkOne = $this->option('one');
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->info('DRY RUN MODE - No changes will be made');
|
||||||
|
if ($checkOne) {
|
||||||
|
$this->info('Checking only the first canceled subscription...');
|
||||||
|
} else {
|
||||||
|
$this->info('Checking for canceled subscriptions...');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($checkOne) {
|
||||||
|
$this->info('Checking and fixing only the first canceled subscription...');
|
||||||
|
} else {
|
||||||
|
$this->info('Checking and fixing canceled subscriptions...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$teamsWithSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
|
||||||
|
$toFixCount = 0;
|
||||||
|
$fixedCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
$canceledSubscriptions = [];
|
||||||
|
|
||||||
|
foreach ($teamsWithSubscriptions as $team) {
|
||||||
|
$subscription = $team->subscription;
|
||||||
|
|
||||||
|
if (! $subscription->stripe_subscription_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stripeSubscription = $stripe->subscriptions->retrieve(
|
||||||
|
$subscription->stripe_subscription_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($stripeSubscription->status === 'canceled') {
|
||||||
|
$toFixCount++;
|
||||||
|
|
||||||
|
// Get team members' emails
|
||||||
|
$memberEmails = $team->members->pluck('email')->toArray();
|
||||||
|
|
||||||
|
$canceledSubscriptions[] = [
|
||||||
|
'team_id' => $team->id,
|
||||||
|
'team_name' => $team->name,
|
||||||
|
'customer_id' => $subscription->stripe_customer_id,
|
||||||
|
'subscription_id' => $subscription->stripe_subscription_id,
|
||||||
|
'status' => 'canceled',
|
||||||
|
'member_emails' => $memberEmails,
|
||||||
|
'subscription_model' => $subscription->toArray(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->warn('Would fix canceled subscription:');
|
||||||
|
$this->line(" Team ID: {$team->id}");
|
||||||
|
$this->line(" Team Name: {$team->name}");
|
||||||
|
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
||||||
|
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
||||||
|
$this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}");
|
||||||
|
$this->line(' Current Subscription Data:');
|
||||||
|
foreach ($subscription->getAttributes() as $key => $value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
$this->line(" - {$key}: null");
|
||||||
|
} elseif (is_bool($value)) {
|
||||||
|
$this->line(" - {$key}: ".($value ? 'true' : 'false'));
|
||||||
|
} else {
|
||||||
|
$this->line(" - {$key}: {$value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->newLine();
|
||||||
|
} else {
|
||||||
|
$this->warn("Found canceled subscription for Team ID: {$team->id}");
|
||||||
|
|
||||||
|
// Send internal notification with all details before fixing
|
||||||
|
$notificationMessage = "Fixing canceled subscription:\n";
|
||||||
|
$notificationMessage .= "Team ID: {$team->id}\n";
|
||||||
|
$notificationMessage .= "Team Name: {$team->name}\n";
|
||||||
|
$notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n";
|
||||||
|
$notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n";
|
||||||
|
$notificationMessage .= "Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}\n";
|
||||||
|
$notificationMessage .= "Subscription Data:\n";
|
||||||
|
foreach ($subscription->getAttributes() as $key => $value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
$notificationMessage .= " - {$key}: null\n";
|
||||||
|
} elseif (is_bool($value)) {
|
||||||
|
$notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n";
|
||||||
|
} else {
|
||||||
|
$notificationMessage .= " - {$key}: {$value}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send_internal_notification($notificationMessage);
|
||||||
|
|
||||||
|
// Apply the same logic as customer.subscription.deleted webhook
|
||||||
|
$team->subscriptionEnded();
|
||||||
|
|
||||||
|
$fixedCount++;
|
||||||
|
$this->info(" ✓ Fixed subscription for Team ID: {$team->id}");
|
||||||
|
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
||||||
|
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
||||||
|
$this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break if --one flag is set
|
||||||
|
if ($checkOne) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Stripe\Exception\InvalidRequestException $e) {
|
||||||
|
if ($e->getStripeCode() === 'resource_missing') {
|
||||||
|
$toFixCount++;
|
||||||
|
|
||||||
|
// Get team members' emails
|
||||||
|
$memberEmails = $team->members->pluck('email')->toArray();
|
||||||
|
|
||||||
|
$canceledSubscriptions[] = [
|
||||||
|
'team_id' => $team->id,
|
||||||
|
'team_name' => $team->name,
|
||||||
|
'customer_id' => $subscription->stripe_customer_id,
|
||||||
|
'subscription_id' => $subscription->stripe_subscription_id,
|
||||||
|
'status' => 'missing',
|
||||||
|
'member_emails' => $memberEmails,
|
||||||
|
'subscription_model' => $subscription->toArray(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->error('Would fix missing subscription (not found in Stripe):');
|
||||||
|
$this->line(" Team ID: {$team->id}");
|
||||||
|
$this->line(" Team Name: {$team->name}");
|
||||||
|
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
||||||
|
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
||||||
|
$this->line(" Subscription ID (missing): {$subscription->stripe_subscription_id}");
|
||||||
|
$this->line(' Current Subscription Data:');
|
||||||
|
foreach ($subscription->getAttributes() as $key => $value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
$this->line(" - {$key}: null");
|
||||||
|
} elseif (is_bool($value)) {
|
||||||
|
$this->line(" - {$key}: ".($value ? 'true' : 'false'));
|
||||||
|
} else {
|
||||||
|
$this->line(" - {$key}: {$value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->newLine();
|
||||||
|
} else {
|
||||||
|
$this->error("Subscription not found in Stripe for Team ID: {$team->id}");
|
||||||
|
|
||||||
|
// Send internal notification with all details before fixing
|
||||||
|
$notificationMessage = "Fixing missing subscription (not found in Stripe):\n";
|
||||||
|
$notificationMessage .= "Team ID: {$team->id}\n";
|
||||||
|
$notificationMessage .= "Team Name: {$team->name}\n";
|
||||||
|
$notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n";
|
||||||
|
$notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n";
|
||||||
|
$notificationMessage .= "Subscription ID (missing): {$subscription->stripe_subscription_id}\n";
|
||||||
|
$notificationMessage .= "Subscription Data:\n";
|
||||||
|
foreach ($subscription->getAttributes() as $key => $value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
$notificationMessage .= " - {$key}: null\n";
|
||||||
|
} elseif (is_bool($value)) {
|
||||||
|
$notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n";
|
||||||
|
} else {
|
||||||
|
$notificationMessage .= " - {$key}: {$value}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send_internal_notification($notificationMessage);
|
||||||
|
|
||||||
|
// Apply the same logic as customer.subscription.deleted webhook
|
||||||
|
$team->subscriptionEnded();
|
||||||
|
|
||||||
|
$fixedCount++;
|
||||||
|
$this->info(" ✓ Fixed missing subscription for Team ID: {$team->id}");
|
||||||
|
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
||||||
|
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break if --one flag is set
|
||||||
|
if ($checkOne) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = "Team ID {$team->id}: ".$e->getMessage();
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = "Team ID {$team->id}: ".$e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('Summary:');
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->info(" - Found {$toFixCount} canceled/missing subscriptions that would be fixed");
|
||||||
|
|
||||||
|
if ($toFixCount > 0) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->comment('Run with --fix-canceled-subs to apply these changes');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info(" - Fixed {$fixedCount} canceled/missing subscriptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($errors)) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->error('Errors encountered:');
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
$this->error(" - {$error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify all active subscriptions against Stripe API
|
||||||
|
*/
|
||||||
|
private function verifyAllActiveSubscriptions(\Stripe\StripeClient $stripe)
|
||||||
|
{
|
||||||
|
$isDryRun = $this->option('dry-run');
|
||||||
|
$shouldFix = $this->option('fix-verified');
|
||||||
|
|
||||||
|
$this->info('Verifying all active subscriptions against Stripe...');
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->info('DRY RUN MODE - No changes will be made');
|
||||||
|
}
|
||||||
|
if ($shouldFix && ! $isDryRun) {
|
||||||
|
$this->warn('FIX MODE - Discrepancies will be corrected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all teams with active subscriptions
|
||||||
|
$teamsWithActiveSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
|
||||||
|
$totalCount = $teamsWithActiveSubscriptions->count();
|
||||||
|
|
||||||
|
$this->info("Found {$totalCount} teams with active subscriptions in database");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// CSV header
|
||||||
|
fputcsv($out, [
|
||||||
|
'team_id',
|
||||||
|
'team_name',
|
||||||
|
'customer_id',
|
||||||
|
'subscription_id',
|
||||||
|
'db_status',
|
||||||
|
'stripe_status',
|
||||||
|
'action',
|
||||||
|
'member_emails',
|
||||||
|
'customer_url',
|
||||||
|
'subscription_url',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total' => $totalCount,
|
||||||
|
'valid_active' => 0,
|
||||||
|
'valid_past_due' => 0,
|
||||||
|
'canceled' => 0,
|
||||||
|
'missing' => 0,
|
||||||
|
'invalid' => 0,
|
||||||
|
'fixed' => 0,
|
||||||
|
'errors' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$processedCount = 0;
|
||||||
|
|
||||||
|
foreach ($teamsWithActiveSubscriptions as $team) {
|
||||||
|
$subscription = $team->subscription;
|
||||||
|
$memberEmails = $team->members->pluck('email')->toArray();
|
||||||
|
|
||||||
|
// Database state
|
||||||
|
$dbStatus = 'active';
|
||||||
|
if ($subscription->stripe_past_due) {
|
||||||
|
$dbStatus = 'past_due';
|
||||||
|
}
|
||||||
|
|
||||||
|
$stripeStatus = null;
|
||||||
|
$action = 'none';
|
||||||
|
|
||||||
|
if (! $subscription->stripe_subscription_id) {
|
||||||
|
$this->line("Team {$team->id}: Missing subscription ID, searching in Stripe...");
|
||||||
|
|
||||||
|
$foundResult = null;
|
||||||
|
$searchMethod = null;
|
||||||
|
|
||||||
|
// Search by customer ID
|
||||||
|
if ($subscription->stripe_customer_id) {
|
||||||
|
$this->line(" → Searching by customer ID: {$subscription->stripe_customer_id}");
|
||||||
|
$foundResult = $this->searchSubscriptionsByCustomer($stripe, $subscription->stripe_customer_id);
|
||||||
|
if ($foundResult) {
|
||||||
|
$searchMethod = $foundResult['method'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->line(' → No customer ID available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by emails if not found
|
||||||
|
if (! $foundResult && count($memberEmails) > 0) {
|
||||||
|
$foundResult = $this->searchSubscriptionsByEmails($stripe, $memberEmails);
|
||||||
|
if ($foundResult) {
|
||||||
|
$searchMethod = $foundResult['method'];
|
||||||
|
|
||||||
|
// Update customer ID if different
|
||||||
|
if (isset($foundResult['customer_id']) && $subscription->stripe_customer_id !== $foundResult['customer_id']) {
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->warn(" ⚠ Would update customer ID from {$subscription->stripe_customer_id} to {$foundResult['customer_id']}");
|
||||||
|
} elseif ($shouldFix) {
|
||||||
|
$subscription->update(['stripe_customer_id' => $foundResult['customer_id']]);
|
||||||
|
$this->info(" ✓ Updated customer ID to {$foundResult['customer_id']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($foundResult && isset($foundResult['subscription'])) {
|
||||||
|
// Check if it's an active/past_due subscription
|
||||||
|
if (in_array($foundResult['status'], ['active', 'past_due'])) {
|
||||||
|
// Found an active subscription, handle update
|
||||||
|
$result = $this->handleFoundSubscription(
|
||||||
|
$team,
|
||||||
|
$subscription,
|
||||||
|
$foundResult['subscription'],
|
||||||
|
$searchMethod,
|
||||||
|
$isDryRun,
|
||||||
|
$shouldFix,
|
||||||
|
$stats
|
||||||
|
);
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$result['id'],
|
||||||
|
$dbStatus,
|
||||||
|
$result['status'],
|
||||||
|
$result['action'],
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
$subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A',
|
||||||
|
$result['url'],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Found subscription but it's canceled/expired - needs to be deactivated
|
||||||
|
$this->warn(" → Found {$foundResult['status']} subscription {$foundResult['subscription']->id} - needs deactivation");
|
||||||
|
|
||||||
|
$result = $this->handleMissingSubscription($team, $subscription, $foundResult['status'], $isDryRun, $shouldFix, $stats);
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$foundResult['subscription']->id,
|
||||||
|
$dbStatus,
|
||||||
|
$foundResult['status'],
|
||||||
|
'needs_fix',
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
$subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A',
|
||||||
|
"https://dashboard.stripe.com/subscriptions/{$foundResult['subscription']->id}",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No subscription found at all
|
||||||
|
$this->line(' → No subscription found');
|
||||||
|
|
||||||
|
$stripeStatus = 'not_found';
|
||||||
|
$result = $this->handleMissingSubscription($team, $subscription, $stripeStatus, $isDryRun, $shouldFix, $stats);
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
'N/A',
|
||||||
|
$dbStatus,
|
||||||
|
$result['status'],
|
||||||
|
$result['action'],
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
$subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A',
|
||||||
|
'N/A',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First validate the subscription ID format
|
||||||
|
if (! str_starts_with($subscription->stripe_subscription_id, 'sub_')) {
|
||||||
|
$this->warn(" ⚠ Invalid subscription ID format (doesn't start with 'sub_')");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stripeSubscription = $stripe->subscriptions->retrieve(
|
||||||
|
$subscription->stripe_subscription_id
|
||||||
|
);
|
||||||
|
|
||||||
|
$stripeStatus = $stripeSubscription->status;
|
||||||
|
|
||||||
|
// Determine if action is needed
|
||||||
|
switch ($stripeStatus) {
|
||||||
|
case 'active':
|
||||||
|
$stats['valid_active']++;
|
||||||
|
$action = 'valid';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'past_due':
|
||||||
|
$stats['valid_past_due']++;
|
||||||
|
$action = 'valid';
|
||||||
|
// Ensure past_due flag is set
|
||||||
|
if (! $subscription->stripe_past_due) {
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->info("Would set stripe_past_due=true for Team {$team->id}");
|
||||||
|
} elseif ($shouldFix) {
|
||||||
|
$subscription->update(['stripe_past_due' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'canceled':
|
||||||
|
case 'incomplete_expired':
|
||||||
|
case 'unpaid':
|
||||||
|
case 'incomplete':
|
||||||
|
$stats['canceled']++;
|
||||||
|
$action = 'needs_fix';
|
||||||
|
|
||||||
|
// Only output problematic subscriptions
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$subscription->stripe_subscription_id,
|
||||||
|
$dbStatus,
|
||||||
|
$stripeStatus,
|
||||||
|
$action,
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
"https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}",
|
||||||
|
"https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->info("Would deactivate subscription for Team {$team->id} - status: {$stripeStatus}");
|
||||||
|
} elseif ($shouldFix) {
|
||||||
|
$this->fixSubscription($team, $subscription, $stripeStatus);
|
||||||
|
$stats['fixed']++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$stats['invalid']++;
|
||||||
|
$action = 'unknown';
|
||||||
|
|
||||||
|
// Only output problematic subscriptions
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$subscription->stripe_subscription_id,
|
||||||
|
$dbStatus,
|
||||||
|
$stripeStatus,
|
||||||
|
$action,
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
"https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}",
|
||||||
|
"https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}",
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Stripe\Exception\InvalidRequestException $e) {
|
||||||
|
$this->error(' → Error: '.$e->getMessage());
|
||||||
|
|
||||||
|
if ($e->getStripeCode() === 'resource_missing' || $e->getHttpStatus() === 404) {
|
||||||
|
// Subscription doesn't exist, try to find by customer ID
|
||||||
|
$this->warn(" → Subscription not found, checking customer's subscriptions...");
|
||||||
|
|
||||||
|
$foundResult = null;
|
||||||
|
if ($subscription->stripe_customer_id) {
|
||||||
|
$foundResult = $this->searchSubscriptionsByCustomer($stripe, $subscription->stripe_customer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($foundResult && isset($foundResult['subscription']) && in_array($foundResult['status'], ['active', 'past_due'])) {
|
||||||
|
// Found an active subscription with different ID
|
||||||
|
$this->warn(" → ID mismatch! DB: {$subscription->stripe_subscription_id}, Stripe: {$foundResult['subscription']->id}");
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
"WRONG ID: {$subscription->stripe_subscription_id} → {$foundResult['subscription']->id}",
|
||||||
|
$dbStatus,
|
||||||
|
$foundResult['status'],
|
||||||
|
'id_mismatch',
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
"https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}",
|
||||||
|
"https://dashboard.stripe.com/subscriptions/{$foundResult['subscription']->id}",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->warn(" → Would update subscription ID to {$foundResult['subscription']->id}");
|
||||||
|
} elseif ($shouldFix) {
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_subscription_id' => $foundResult['subscription']->id,
|
||||||
|
'stripe_invoice_paid' => true,
|
||||||
|
'stripe_past_due' => $foundResult['status'] === 'past_due',
|
||||||
|
]);
|
||||||
|
$stats['fixed']++;
|
||||||
|
$this->info(' → Updated subscription ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats[$foundResult['status'] === 'active' ? 'valid_active' : 'valid_past_due']++;
|
||||||
|
} else {
|
||||||
|
// No active subscription found
|
||||||
|
$stripeStatus = $foundResult ? $foundResult['status'] : 'not_found';
|
||||||
|
$result = $this->handleMissingSubscription($team, $subscription, $stripeStatus, $isDryRun, $shouldFix, $stats);
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$subscription->stripe_subscription_id,
|
||||||
|
$dbStatus,
|
||||||
|
$result['status'],
|
||||||
|
$result['action'],
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
$subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A',
|
||||||
|
$foundResult && isset($foundResult['subscription']) ? "https://dashboard.stripe.com/subscriptions/{$foundResult['subscription']->id}" : 'N/A',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other API error
|
||||||
|
$stats['errors']++;
|
||||||
|
$this->error(' → API Error - not marking as deleted');
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$subscription->stripe_subscription_id,
|
||||||
|
$dbStatus,
|
||||||
|
'error: '.$e->getStripeCode(),
|
||||||
|
'error',
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
$subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A',
|
||||||
|
$subscription->stripe_subscription_id ? "https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}" : 'N/A',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(' → Unexpected error: '.$e->getMessage());
|
||||||
|
$stats['errors']++;
|
||||||
|
|
||||||
|
fputcsv($out, [
|
||||||
|
$team->id,
|
||||||
|
$team->name,
|
||||||
|
$subscription->stripe_customer_id,
|
||||||
|
$subscription->stripe_subscription_id,
|
||||||
|
$dbStatus,
|
||||||
|
'error',
|
||||||
|
'error',
|
||||||
|
implode(', ', $memberEmails),
|
||||||
|
$subscription->stripe_customer_id ? "https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}" : 'N/A',
|
||||||
|
$subscription->stripe_subscription_id ? "https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}" : 'N/A',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedCount++;
|
||||||
|
if ($processedCount % 100 === 0) {
|
||||||
|
$this->info("Processed {$processedCount}/{$totalCount} subscriptions...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($out);
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
$this->newLine(2);
|
||||||
|
$this->info('=== Verification Summary ===');
|
||||||
|
$this->info("Total subscriptions checked: {$stats['total']}");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$this->info('Valid subscriptions in Stripe:');
|
||||||
|
$this->line(" - Active: {$stats['valid_active']}");
|
||||||
|
$this->line(" - Past Due: {$stats['valid_past_due']}");
|
||||||
|
$validTotal = $stats['valid_active'] + $stats['valid_past_due'];
|
||||||
|
$this->info(" Total valid: {$validTotal}");
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn('Invalid subscriptions:');
|
||||||
|
$this->line(" - Canceled/Expired: {$stats['canceled']}");
|
||||||
|
$this->line(" - Missing/Not Found: {$stats['missing']}");
|
||||||
|
$this->line(" - Unknown status: {$stats['invalid']}");
|
||||||
|
$invalidTotal = $stats['canceled'] + $stats['missing'] + $stats['invalid'];
|
||||||
|
$this->warn(" Total invalid: {$invalidTotal}");
|
||||||
|
|
||||||
|
if ($stats['errors'] > 0) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->error("Errors encountered: {$stats['errors']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shouldFix && ! $isDryRun) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Fixed subscriptions: {$stats['fixed']}");
|
||||||
|
} elseif ($invalidTotal > 0 && ! $shouldFix) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->comment('Run with --fix-verified to fix the discrepancies');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix a subscription based on its status
|
||||||
|
*/
|
||||||
|
private function fixSubscription($team, $subscription, $status)
|
||||||
|
{
|
||||||
|
$message = "Fixing subscription for Team ID: {$team->id} (Status: {$status})\n";
|
||||||
|
$message .= "Team Name: {$team->name}\n";
|
||||||
|
$message .= "Customer ID: {$subscription->stripe_customer_id}\n";
|
||||||
|
$message .= "Subscription ID: {$subscription->stripe_subscription_id}\n";
|
||||||
|
|
||||||
|
send_internal_notification($message);
|
||||||
|
|
||||||
|
// Call the team's subscription ended method which properly cleans up
|
||||||
|
$team->subscriptionEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for subscriptions by customer ID
|
||||||
|
*/
|
||||||
|
private function searchSubscriptionsByCustomer(\Stripe\StripeClient $stripe, $customerId, $requireActive = false)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$subscriptions = $stripe->subscriptions->all([
|
||||||
|
'customer' => $customerId,
|
||||||
|
'limit' => 10,
|
||||||
|
'status' => 'all',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->line(' → Found '.count($subscriptions->data).' subscription(s) for customer');
|
||||||
|
|
||||||
|
// Look for active/past_due first
|
||||||
|
foreach ($subscriptions->data as $sub) {
|
||||||
|
$this->line(" - Subscription {$sub->id}: status={$sub->status}");
|
||||||
|
if (in_array($sub->status, ['active', 'past_due'])) {
|
||||||
|
$this->info(" ✓ Found active/past_due subscription: {$sub->id}");
|
||||||
|
|
||||||
|
return ['subscription' => $sub, 'status' => $sub->status, 'method' => 'customer_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not requiring active and there are subscriptions, return first one
|
||||||
|
if (! $requireActive && count($subscriptions->data) > 0) {
|
||||||
|
$sub = $subscriptions->data[0];
|
||||||
|
$this->warn(" ⚠ Only found {$sub->status} subscription: {$sub->id}");
|
||||||
|
|
||||||
|
return ['subscription' => $sub, 'status' => $sub->status, 'method' => 'customer_id_first'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(' → Error searching by customer ID: '.$e->getMessage());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for subscriptions by team member emails
|
||||||
|
*/
|
||||||
|
private function searchSubscriptionsByEmails(\Stripe\StripeClient $stripe, $emails)
|
||||||
|
{
|
||||||
|
$this->line(' → Searching by team member emails...');
|
||||||
|
|
||||||
|
foreach ($emails as $email) {
|
||||||
|
$this->line(" → Checking email: {$email}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$customers = $stripe->customers->all([
|
||||||
|
'email' => $email,
|
||||||
|
'limit' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (count($customers->data) === 0) {
|
||||||
|
$this->line(' - No customers found');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line(' - Found '.count($customers->data).' customer(s)');
|
||||||
|
|
||||||
|
foreach ($customers->data as $customer) {
|
||||||
|
$this->line(" - Checking customer {$customer->id}");
|
||||||
|
|
||||||
|
$result = $this->searchSubscriptionsByCustomer($stripe, $customer->id, true);
|
||||||
|
if ($result) {
|
||||||
|
$result['method'] = "email:{$email}";
|
||||||
|
$result['customer_id'] = $customer->id;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(" - Error searching for email {$email}: ".$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle found subscription update (only for active/past_due subscriptions)
|
||||||
|
*/
|
||||||
|
private function handleFoundSubscription($team, $subscription, $foundSub, $searchMethod, $isDryRun, $shouldFix, &$stats)
|
||||||
|
{
|
||||||
|
$stripeStatus = $foundSub->status;
|
||||||
|
$this->info(" ✓ FOUND active/past_due subscription {$foundSub->id} (status: {$stripeStatus})");
|
||||||
|
|
||||||
|
// Only update if it's active or past_due
|
||||||
|
if (! in_array($stripeStatus, ['active', 'past_due'])) {
|
||||||
|
$this->error(" ERROR: handleFoundSubscription called with {$stripeStatus} subscription!");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $foundSub->id,
|
||||||
|
'status' => $stripeStatus,
|
||||||
|
'action' => 'error',
|
||||||
|
'url' => "https://dashboard.stripe.com/subscriptions/{$foundSub->id}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$this->warn(" → Would update subscription ID to {$foundSub->id} (status: {$stripeStatus})");
|
||||||
|
} elseif ($shouldFix) {
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_subscription_id' => $foundSub->id,
|
||||||
|
'stripe_invoice_paid' => true,
|
||||||
|
'stripe_past_due' => $stripeStatus === 'past_due',
|
||||||
|
]);
|
||||||
|
$stats['fixed']++;
|
||||||
|
$this->info(" → Updated subscription ID to {$foundSub->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
$stats[$stripeStatus === 'active' ? 'valid_active' : 'valid_past_due']++;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => "FOUND: {$foundSub->id}",
|
||||||
|
'status' => $stripeStatus,
|
||||||
|
'action' => "will_update (via {$searchMethod})",
|
||||||
|
'url' => "https://dashboard.stripe.com/subscriptions/{$foundSub->id}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle missing subscription
|
||||||
|
*/
|
||||||
|
private function handleMissingSubscription($team, $subscription, $status, $isDryRun, $shouldFix, &$stats)
|
||||||
|
{
|
||||||
|
$stats['missing']++;
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$statusMsg = $status !== 'not_found' ? "status: {$status}" : 'no subscription found in Stripe';
|
||||||
|
$this->warn(" → Would deactivate subscription - {$statusMsg}");
|
||||||
|
} elseif ($shouldFix) {
|
||||||
|
$this->fixSubscription($team, $subscription, $status);
|
||||||
|
$stats['fixed']++;
|
||||||
|
$this->info(' → Deactivated subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => 'N/A',
|
||||||
|
'status' => $status,
|
||||||
|
'action' => 'needs_fix',
|
||||||
|
'url' => 'N/A',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,319 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Models\Team;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class CloudCheckSubscription extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'cloud:check-subscription
|
|
||||||
{--fix : Fix canceled subscriptions in database}
|
|
||||||
{--dry-run : Show what would be fixed without making changes}
|
|
||||||
{--one : Only check/fix the first found subscription}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Check Cloud subscriptions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
|
||||||
|
|
||||||
if ($this->option('fix') || $this->option('dry-run')) {
|
|
||||||
return $this->fixCanceledSubscriptions($stripe);
|
|
||||||
}
|
|
||||||
|
|
||||||
$activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
|
|
||||||
|
|
||||||
$out = fopen('php://output', 'w');
|
|
||||||
// CSV header
|
|
||||||
fputcsv($out, [
|
|
||||||
'team_id',
|
|
||||||
'invoice_status',
|
|
||||||
'stripe_customer_url',
|
|
||||||
'stripe_subscription_id',
|
|
||||||
'subscription_status',
|
|
||||||
'subscription_url',
|
|
||||||
'note',
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($activeSubscribers as $team) {
|
|
||||||
$stripeSubscriptionId = $team->subscription->stripe_subscription_id;
|
|
||||||
$stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
|
|
||||||
$stripeCustomerId = $team->subscription->stripe_customer_id;
|
|
||||||
|
|
||||||
if (! $stripeSubscriptionId && str($stripeInvoicePaid)->lower() != 'past_due') {
|
|
||||||
fputcsv($out, [
|
|
||||||
$team->id,
|
|
||||||
$stripeInvoicePaid,
|
|
||||||
$stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
'Missing subscription ID while invoice not past_due',
|
|
||||||
]);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $stripeSubscriptionId) {
|
|
||||||
// No subscription ID and invoice is past_due, still record for visibility
|
|
||||||
fputcsv($out, [
|
|
||||||
$team->id,
|
|
||||||
$stripeInvoicePaid,
|
|
||||||
$stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
'Missing subscription ID',
|
|
||||||
]);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
|
|
||||||
if ($subscription->status === 'active') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fputcsv($out, [
|
|
||||||
$team->id,
|
|
||||||
$stripeInvoicePaid,
|
|
||||||
$stripeCustomerId ? "https://dashboard.stripe.com/customers/{$stripeCustomerId}" : null,
|
|
||||||
$stripeSubscriptionId,
|
|
||||||
$subscription->status,
|
|
||||||
"https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}",
|
|
||||||
'Subscription not active',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fix canceled subscriptions in the database
|
|
||||||
*/
|
|
||||||
private function fixCanceledSubscriptions(\Stripe\StripeClient $stripe)
|
|
||||||
{
|
|
||||||
$isDryRun = $this->option('dry-run');
|
|
||||||
$checkOne = $this->option('one');
|
|
||||||
|
|
||||||
if ($isDryRun) {
|
|
||||||
$this->info('DRY RUN MODE - No changes will be made');
|
|
||||||
if ($checkOne) {
|
|
||||||
$this->info('Checking only the first canceled subscription...');
|
|
||||||
} else {
|
|
||||||
$this->info('Checking for canceled subscriptions...');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($checkOne) {
|
|
||||||
$this->info('Checking and fixing only the first canceled subscription...');
|
|
||||||
} else {
|
|
||||||
$this->info('Checking and fixing canceled subscriptions...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$teamsWithSubscriptions = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
|
|
||||||
$toFixCount = 0;
|
|
||||||
$fixedCount = 0;
|
|
||||||
$errors = [];
|
|
||||||
$canceledSubscriptions = [];
|
|
||||||
|
|
||||||
foreach ($teamsWithSubscriptions as $team) {
|
|
||||||
$subscription = $team->subscription;
|
|
||||||
|
|
||||||
if (! $subscription->stripe_subscription_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stripeSubscription = $stripe->subscriptions->retrieve(
|
|
||||||
$subscription->stripe_subscription_id
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($stripeSubscription->status === 'canceled') {
|
|
||||||
$toFixCount++;
|
|
||||||
|
|
||||||
// Get team members' emails
|
|
||||||
$memberEmails = $team->members->pluck('email')->toArray();
|
|
||||||
|
|
||||||
$canceledSubscriptions[] = [
|
|
||||||
'team_id' => $team->id,
|
|
||||||
'team_name' => $team->name,
|
|
||||||
'customer_id' => $subscription->stripe_customer_id,
|
|
||||||
'subscription_id' => $subscription->stripe_subscription_id,
|
|
||||||
'status' => 'canceled',
|
|
||||||
'member_emails' => $memberEmails,
|
|
||||||
'subscription_model' => $subscription->toArray(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($isDryRun) {
|
|
||||||
$this->warn('Would fix canceled subscription:');
|
|
||||||
$this->line(" Team ID: {$team->id}");
|
|
||||||
$this->line(" Team Name: {$team->name}");
|
|
||||||
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
|
||||||
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
|
||||||
$this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}");
|
|
||||||
$this->line(' Current Subscription Data:');
|
|
||||||
foreach ($subscription->getAttributes() as $key => $value) {
|
|
||||||
if (is_null($value)) {
|
|
||||||
$this->line(" - {$key}: null");
|
|
||||||
} elseif (is_bool($value)) {
|
|
||||||
$this->line(" - {$key}: ".($value ? 'true' : 'false'));
|
|
||||||
} else {
|
|
||||||
$this->line(" - {$key}: {$value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->newLine();
|
|
||||||
} else {
|
|
||||||
$this->warn("Found canceled subscription for Team ID: {$team->id}");
|
|
||||||
|
|
||||||
// Send internal notification with all details before fixing
|
|
||||||
$notificationMessage = "Fixing canceled subscription:\n";
|
|
||||||
$notificationMessage .= "Team ID: {$team->id}\n";
|
|
||||||
$notificationMessage .= "Team Name: {$team->name}\n";
|
|
||||||
$notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n";
|
|
||||||
$notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n";
|
|
||||||
$notificationMessage .= "Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}\n";
|
|
||||||
$notificationMessage .= "Subscription Data:\n";
|
|
||||||
foreach ($subscription->getAttributes() as $key => $value) {
|
|
||||||
if (is_null($value)) {
|
|
||||||
$notificationMessage .= " - {$key}: null\n";
|
|
||||||
} elseif (is_bool($value)) {
|
|
||||||
$notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n";
|
|
||||||
} else {
|
|
||||||
$notificationMessage .= " - {$key}: {$value}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send_internal_notification($notificationMessage);
|
|
||||||
|
|
||||||
// Apply the same logic as customer.subscription.deleted webhook
|
|
||||||
$team->subscriptionEnded();
|
|
||||||
|
|
||||||
$fixedCount++;
|
|
||||||
$this->info(" ✓ Fixed subscription for Team ID: {$team->id}");
|
|
||||||
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
|
||||||
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
|
||||||
$this->line(" Subscription URL: https://dashboard.stripe.com/subscriptions/{$subscription->stripe_subscription_id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break if --one flag is set
|
|
||||||
if ($checkOne) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Stripe\Exception\InvalidRequestException $e) {
|
|
||||||
if ($e->getStripeCode() === 'resource_missing') {
|
|
||||||
$toFixCount++;
|
|
||||||
|
|
||||||
// Get team members' emails
|
|
||||||
$memberEmails = $team->members->pluck('email')->toArray();
|
|
||||||
|
|
||||||
$canceledSubscriptions[] = [
|
|
||||||
'team_id' => $team->id,
|
|
||||||
'team_name' => $team->name,
|
|
||||||
'customer_id' => $subscription->stripe_customer_id,
|
|
||||||
'subscription_id' => $subscription->stripe_subscription_id,
|
|
||||||
'status' => 'missing',
|
|
||||||
'member_emails' => $memberEmails,
|
|
||||||
'subscription_model' => $subscription->toArray(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($isDryRun) {
|
|
||||||
$this->error('Would fix missing subscription (not found in Stripe):');
|
|
||||||
$this->line(" Team ID: {$team->id}");
|
|
||||||
$this->line(" Team Name: {$team->name}");
|
|
||||||
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
|
||||||
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
|
||||||
$this->line(" Subscription ID (missing): {$subscription->stripe_subscription_id}");
|
|
||||||
$this->line(' Current Subscription Data:');
|
|
||||||
foreach ($subscription->getAttributes() as $key => $value) {
|
|
||||||
if (is_null($value)) {
|
|
||||||
$this->line(" - {$key}: null");
|
|
||||||
} elseif (is_bool($value)) {
|
|
||||||
$this->line(" - {$key}: ".($value ? 'true' : 'false'));
|
|
||||||
} else {
|
|
||||||
$this->line(" - {$key}: {$value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->newLine();
|
|
||||||
} else {
|
|
||||||
$this->error("Subscription not found in Stripe for Team ID: {$team->id}");
|
|
||||||
|
|
||||||
// Send internal notification with all details before fixing
|
|
||||||
$notificationMessage = "Fixing missing subscription (not found in Stripe):\n";
|
|
||||||
$notificationMessage .= "Team ID: {$team->id}\n";
|
|
||||||
$notificationMessage .= "Team Name: {$team->name}\n";
|
|
||||||
$notificationMessage .= 'Team Members: '.implode(', ', $memberEmails)."\n";
|
|
||||||
$notificationMessage .= "Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}\n";
|
|
||||||
$notificationMessage .= "Subscription ID (missing): {$subscription->stripe_subscription_id}\n";
|
|
||||||
$notificationMessage .= "Subscription Data:\n";
|
|
||||||
foreach ($subscription->getAttributes() as $key => $value) {
|
|
||||||
if (is_null($value)) {
|
|
||||||
$notificationMessage .= " - {$key}: null\n";
|
|
||||||
} elseif (is_bool($value)) {
|
|
||||||
$notificationMessage .= " - {$key}: ".($value ? 'true' : 'false')."\n";
|
|
||||||
} else {
|
|
||||||
$notificationMessage .= " - {$key}: {$value}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send_internal_notification($notificationMessage);
|
|
||||||
|
|
||||||
// Apply the same logic as customer.subscription.deleted webhook
|
|
||||||
$team->subscriptionEnded();
|
|
||||||
|
|
||||||
$fixedCount++;
|
|
||||||
$this->info(" ✓ Fixed missing subscription for Team ID: {$team->id}");
|
|
||||||
$this->line(' Team Members: '.implode(', ', $memberEmails));
|
|
||||||
$this->line(" Customer URL: https://dashboard.stripe.com/customers/{$subscription->stripe_customer_id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break if --one flag is set
|
|
||||||
if ($checkOne) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$errors[] = "Team ID {$team->id}: ".$e->getMessage();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = "Team ID {$team->id}: ".$e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Summary:');
|
|
||||||
|
|
||||||
if ($isDryRun) {
|
|
||||||
$this->info(" - Found {$toFixCount} canceled/missing subscriptions that would be fixed");
|
|
||||||
|
|
||||||
if ($toFixCount > 0) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->comment('Run with --fix to apply these changes');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->info(" - Fixed {$fixedCount} canceled/missing subscriptions");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($errors)) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->error('Errors encountered:');
|
|
||||||
foreach ($errors as $error) {
|
|
||||||
$this->error(" - {$error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Events\ServerReachabilityChanged;
|
|
||||||
use App\Models\Team;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class CloudCleanupSubscriptions extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'cloud:cleanup-subs';
|
|
||||||
|
|
||||||
protected $description = 'Cleanup subcriptions teams';
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (! isCloud()) {
|
|
||||||
$this->error('This command can only be run on cloud');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->info('Cleaning up subcriptions teams');
|
|
||||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
|
||||||
|
|
||||||
$teams = Team::all()->filter(function ($team) {
|
|
||||||
return $team->id !== 0;
|
|
||||||
})->sortBy('id');
|
|
||||||
foreach ($teams as $team) {
|
|
||||||
if ($team) {
|
|
||||||
$this->info("Checking team {$team->id}");
|
|
||||||
}
|
|
||||||
if (! data_get($team, 'subscription')) {
|
|
||||||
$this->disableServers($team);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
|
|
||||||
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
|
|
||||||
$this->info("Resetting invoice paid status for team {$team->id}");
|
|
||||||
|
|
||||||
$team->subscription->update([
|
|
||||||
'stripe_invoice_paid' => false,
|
|
||||||
'stripe_trial_already_ended' => false,
|
|
||||||
'stripe_subscription_id' => null,
|
|
||||||
]);
|
|
||||||
$this->disableServers($team);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
|
|
||||||
$status = data_get($subscription, 'status');
|
|
||||||
if ($status === 'active') {
|
|
||||||
$team->subscription->update([
|
|
||||||
'stripe_invoice_paid' => true,
|
|
||||||
'stripe_trial_already_ended' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->info('Subscription status: '.$status);
|
|
||||||
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
|
|
||||||
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
|
|
||||||
if (! $confirm) {
|
|
||||||
$this->info("Skipping team {$team->id}");
|
|
||||||
} else {
|
|
||||||
$this->info("Cancelling subscription for team {$team->id}");
|
|
||||||
$team->subscription->update([
|
|
||||||
'stripe_invoice_paid' => false,
|
|
||||||
'stripe_trial_already_ended' => false,
|
|
||||||
'stripe_subscription_id' => null,
|
|
||||||
]);
|
|
||||||
$this->disableServers($team);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error($e->getMessage());
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function disableServers(Team $team)
|
|
||||||
{
|
|
||||||
foreach ($team->servers as $server) {
|
|
||||||
if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') {
|
|
||||||
$this->info("Disabling server {$server->id} {$server->name}");
|
|
||||||
$server->settings()->update([
|
|
||||||
'is_usable' => false,
|
|
||||||
'is_reachable' => false,
|
|
||||||
]);
|
|
||||||
$server->update([
|
|
||||||
'ip' => '1.2.3.4',
|
|
||||||
]);
|
|
||||||
|
|
||||||
ServerReachabilityChanged::dispatch($server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user