v4.0.0-beta.459 (#7750)
This commit is contained in:
commit
b7e0f5577d
10 changed files with 211 additions and 39 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -371,7 +371,7 @@ public function handle(): void
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
|
$this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
|
||||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
$this->graceful_shutdown_container($this->deployment_uuid, skipRemove: true);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Log but don't fail - container cleanup errors are expected when container is already gone
|
// Log but don't fail - container cleanup errors are expected when container is already gone
|
||||||
\Log::warning('Failed to shutdown container '.$this->deployment_uuid.': '.$e->getMessage());
|
\Log::warning('Failed to shutdown container '.$this->deployment_uuid.': '.$e->getMessage());
|
||||||
|
|
@ -1968,7 +1968,7 @@ private function prepare_builder_image(bool $firstTry = true)
|
||||||
$this->application_deployment_queue->addLogEntry('Preparing container with helper image with updated envs.');
|
$this->application_deployment_queue->addLogEntry('Preparing container with helper image with updated envs.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
$this->graceful_shutdown_container($this->deployment_uuid, skipRemove: true);
|
||||||
$this->execute_remote_command(
|
$this->execute_remote_command(
|
||||||
[
|
[
|
||||||
$runCommand,
|
$runCommand,
|
||||||
|
|
@ -1983,8 +1983,8 @@ private function prepare_builder_image(bool $firstTry = true)
|
||||||
|
|
||||||
private function restart_builder_container_with_actual_commit()
|
private function restart_builder_container_with_actual_commit()
|
||||||
{
|
{
|
||||||
// Stop and remove the current helper container
|
// Stop the current helper container (no need for rm -f as it was started with --rm)
|
||||||
$this->graceful_shutdown_container($this->deployment_uuid);
|
$this->graceful_shutdown_container($this->deployment_uuid, skipRemove: true);
|
||||||
|
|
||||||
// Clear cached env_args to force regeneration with actual SOURCE_COMMIT value
|
// Clear cached env_args to force regeneration with actual SOURCE_COMMIT value
|
||||||
$this->env_args = null;
|
$this->env_args = null;
|
||||||
|
|
@ -3171,14 +3171,20 @@ private function build_image()
|
||||||
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function graceful_shutdown_container(string $containerName)
|
private function graceful_shutdown_container(string $containerName, bool $skipRemove = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$timeout = isDev() ? 1 : 30;
|
$timeout = isDev() ? 1 : 30;
|
||||||
$this->execute_remote_command(
|
if ($skipRemove) {
|
||||||
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
$this->execute_remote_command(
|
||||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
$this->execute_remote_command(
|
||||||
|
["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||||
|
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (Exception $error) {
|
} catch (Exception $error) {
|
||||||
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
|
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -110,26 +110,6 @@ public function restartSentinel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedIsSentinelDebugEnabled($value)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$this->submit();
|
|
||||||
$this->restartSentinel();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return handleError($e, $this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatedIsMetricsEnabled($value)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$this->submit();
|
|
||||||
$this->restartSentinel();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return handleError($e, $this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatedIsSentinelEnabled($value)
|
public function updatedIsSentinelEnabled($value)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -175,6 +155,16 @@ public function submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function instantSave()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->syncData(true);
|
||||||
|
$this->restartSentinel();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.server.sentinel');
|
return view('livewire.server.sentinel');
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.458',
|
'version' => '4.0.0-beta.459',
|
||||||
'helper_version' => '1.0.12',
|
'helper_version' => '1.0.12',
|
||||||
'realtime_version' => '1.0.10',
|
'realtime_version' => '1.0.10',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.458"
|
"version": "4.0.0-beta.459"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.459"
|
"version": "4.0.0-beta.460"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.12"
|
"version": "1.0.12"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ services:
|
||||||
- DOLI_CRON=${DOLI_CRON:-0}
|
- DOLI_CRON=${DOLI_CRON:-0}
|
||||||
- DOLI_INIT_DEMO=${DOLI_INIT_DEMO:-0}
|
- DOLI_INIT_DEMO=${DOLI_INIT_DEMO:-0}
|
||||||
- DOLI_COMPANY_NAME=${DOLI_COMPANY_NAME:-MyBigCompany}
|
- DOLI_COMPANY_NAME=${DOLI_COMPANY_NAME:-MyBigCompany}
|
||||||
|
volumes:
|
||||||
|
- dolibarr_docs:/var/www/documents
|
||||||
|
- dolibarr_custom:/var/www/html/custom
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
|
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
|
||||||
interval: 2s
|
interval: 2s
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ services:
|
||||||
config:
|
config:
|
||||||
hide_credentials: true
|
hide_credentials: true
|
||||||
supabase-studio:
|
supabase-studio:
|
||||||
image: supabase/studio:2025.06.02-sha-8f2993d
|
image: supabase/studio:2025.12.17-sha-43f4f7f
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
services:
|
services:
|
||||||
superset:
|
superset:
|
||||||
image: amancevice/superset:latest
|
image: amancevice/superset:6.0.0
|
||||||
environment:
|
environment:
|
||||||
- SERVICE_URL_SUPERSET_8088
|
- SERVICE_URL_SUPERSET_8088
|
||||||
- SECRET_KEY=${SERVICE_BASE64_64_SUPERSETSECRETKEY}
|
- SECRET_KEY=${SERVICE_BASE64_64_SUPERSETSECRETKEY}
|
||||||
|
|
@ -63,13 +63,13 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17-alpine
|
image: postgres:18
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
||||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-superset-db}
|
- POSTGRES_DB=${POSTGRES_DB:-superset-db}
|
||||||
volumes:
|
volumes:
|
||||||
- superset_postgres_data:/var/lib/postgresql/data
|
- superset_postgres_data:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
@ -77,7 +77,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:8-alpine
|
image: redis:8
|
||||||
volumes:
|
volumes:
|
||||||
- superset_redis_data:/data
|
- superset_redis_data:/data
|
||||||
command: redis-server --requirepass ${SERVICE_PASSWORD_REDIS}
|
command: redis-server --requirepass ${SERVICE_PASSWORD_REDIS}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.458"
|
"version": "4.0.0-beta.459"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.459"
|
"version": "4.0.0-beta.460"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.12"
|
"version": "1.0.12"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue