mirror of
https://github.com/coollabsio/coolify.git
synced 2025-12-28 05:34:50 +00:00
feat: Add manual Stripe subscription sync command (#7706)
This commit is contained in:
commit
f77a2674fc
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 New Issue
Block a user