feat: Add manual Stripe subscription sync command (#7706)
This commit is contained in:
commit
f77a2674fc
2 changed files with 173 additions and 0 deletions
81
app/Console/Commands/Cloud/SyncStripeSubscriptions.php
Normal file
81
app/Console/Commands/Cloud/SyncStripeSubscriptions.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Cloud;
|
||||||
|
|
||||||
|
use App\Jobs\SyncStripeSubscriptionsJob;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncStripeSubscriptions extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cloud:sync-stripe-subscriptions {--fix : Actually fix discrepancies (default is check only)}';
|
||||||
|
|
||||||
|
protected $description = 'Sync subscription status with Stripe. By default only checks, use --fix to apply changes.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (! isCloud()) {
|
||||||
|
$this->error('This command can only be run on Coolify Cloud.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isStripe()) {
|
||||||
|
$this->error('Stripe is not configured.');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fix = $this->option('fix');
|
||||||
|
|
||||||
|
if ($fix) {
|
||||||
|
$this->warn('Running with --fix: discrepancies will be corrected.');
|
||||||
|
} else {
|
||||||
|
$this->info('Running in check mode (no changes will be made). Use --fix to apply corrections.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$job = new SyncStripeSubscriptionsJob($fix);
|
||||||
|
$result = $job->handle();
|
||||||
|
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
$this->error($result['error']);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Total subscriptions checked: {$result['total_checked']}");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
if (count($result['discrepancies']) > 0) {
|
||||||
|
$this->warn('Discrepancies found: '.count($result['discrepancies']));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
foreach ($result['discrepancies'] as $discrepancy) {
|
||||||
|
$this->line(" - Subscription ID: {$discrepancy['subscription_id']}");
|
||||||
|
$this->line(" Team ID: {$discrepancy['team_id']}");
|
||||||
|
$this->line(" Stripe ID: {$discrepancy['stripe_subscription_id']}");
|
||||||
|
$this->line(" Stripe Status: {$discrepancy['stripe_status']}");
|
||||||
|
$this->newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fix) {
|
||||||
|
$this->info('All discrepancies have been fixed.');
|
||||||
|
} else {
|
||||||
|
$this->comment('Run with --fix to correct these discrepancies.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info('No discrepancies found. All subscriptions are in sync.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($result['errors']) > 0) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->error('Errors encountered: '.count($result['errors']));
|
||||||
|
foreach ($result['errors'] as $error) {
|
||||||
|
$this->line(" - Subscription {$error['subscription_id']}: {$error['error']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
app/Jobs/SyncStripeSubscriptionsJob.php
Normal file
92
app/Jobs/SyncStripeSubscriptionsJob.php
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class SyncStripeSubscriptionsJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public int $tries = 1;
|
||||||
|
|
||||||
|
public int $timeout = 1800; // 30 minutes max
|
||||||
|
|
||||||
|
public function __construct(public bool $fix = false)
|
||||||
|
{
|
||||||
|
$this->onQueue('high');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): array
|
||||||
|
{
|
||||||
|
if (! isCloud() || ! isStripe()) {
|
||||||
|
return ['error' => 'Not running on Cloud or Stripe not configured'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscriptions = Subscription::whereNotNull('stripe_subscription_id')
|
||||||
|
->where('stripe_invoice_paid', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||||
|
$discrepancies = [];
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($subscriptions as $subscription) {
|
||||||
|
try {
|
||||||
|
$stripeSubscription = $stripe->subscriptions->retrieve(
|
||||||
|
$subscription->stripe_subscription_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if Stripe says cancelled but we think it's active
|
||||||
|
if (in_array($stripeSubscription->status, ['canceled', 'incomplete_expired', 'unpaid'])) {
|
||||||
|
$discrepancies[] = [
|
||||||
|
'subscription_id' => $subscription->id,
|
||||||
|
'team_id' => $subscription->team_id,
|
||||||
|
'stripe_subscription_id' => $subscription->stripe_subscription_id,
|
||||||
|
'stripe_status' => $stripeSubscription->status,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only fix if --fix flag is passed
|
||||||
|
if ($this->fix) {
|
||||||
|
$subscription->update([
|
||||||
|
'stripe_invoice_paid' => false,
|
||||||
|
'stripe_past_due' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($stripeSubscription->status === 'canceled') {
|
||||||
|
$subscription->team?->subscriptionEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to avoid Stripe rate limits
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = [
|
||||||
|
'subscription_id' => $subscription->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only notify if discrepancies found and fixed
|
||||||
|
if ($this->fix && count($discrepancies) > 0) {
|
||||||
|
send_internal_notification(
|
||||||
|
'SyncStripeSubscriptionsJob: Fixed '.count($discrepancies)." discrepancies:\n".
|
||||||
|
json_encode($discrepancies, JSON_PRETTY_PRINT)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_checked' => $subscriptions->count(),
|
||||||
|
'discrepancies' => $discrepancies,
|
||||||
|
'errors' => $errors,
|
||||||
|
'fixed' => $this->fix,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue