Merge branch 'next' into fix-environement-route
This commit is contained in:
		@@ -3,21 +3,26 @@
 | 
			
		||||
namespace App\Http\Controllers\Webhook;
 | 
			
		||||
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Jobs\ServerLimitCheckJob;
 | 
			
		||||
use App\Jobs\SubscriptionInvoiceFailedJob;
 | 
			
		||||
use App\Models\Subscription;
 | 
			
		||||
use App\Models\Team;
 | 
			
		||||
use App\Jobs\StripeProcessJob;
 | 
			
		||||
use App\Models\Webhook;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
 | 
			
		||||
class Stripe extends Controller
 | 
			
		||||
{
 | 
			
		||||
    protected $webhook;
 | 
			
		||||
 | 
			
		||||
    public function events(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $webhookSecret = config('subscription.stripe_webhook_secret');
 | 
			
		||||
            $signature = $request->header('Stripe-Signature');
 | 
			
		||||
            $event = \Stripe\Webhook::constructEvent(
 | 
			
		||||
                $request->getContent(),
 | 
			
		||||
                $signature,
 | 
			
		||||
                $webhookSecret
 | 
			
		||||
            );
 | 
			
		||||
            if (app()->isDownForMaintenance()) {
 | 
			
		||||
                $epoch = now()->valueOf();
 | 
			
		||||
                $data = [
 | 
			
		||||
@@ -33,241 +38,17 @@ class Stripe extends Controller
 | 
			
		||||
                $json = json_encode($data);
 | 
			
		||||
                Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
                return response('Webhook received. Cool cool cool cool cool.', 200);
 | 
			
		||||
            }
 | 
			
		||||
            $webhookSecret = config('subscription.stripe_webhook_secret');
 | 
			
		||||
            $signature = $request->header('Stripe-Signature');
 | 
			
		||||
            $excludedPlans = config('subscription.stripe_excluded_plans');
 | 
			
		||||
            $event = \Stripe\Webhook::constructEvent(
 | 
			
		||||
                $request->getContent(),
 | 
			
		||||
                $signature,
 | 
			
		||||
                $webhookSecret
 | 
			
		||||
            );
 | 
			
		||||
            $webhook = Webhook::create([
 | 
			
		||||
            $this->webhook = Webhook::create([
 | 
			
		||||
                'type' => 'stripe',
 | 
			
		||||
                'payload' => $request->getContent(),
 | 
			
		||||
            ]);
 | 
			
		||||
            $type = data_get($event, 'type');
 | 
			
		||||
            $data = data_get($event, 'data.object');
 | 
			
		||||
            switch ($type) {
 | 
			
		||||
                case 'radar.early_fraud_warning.created':
 | 
			
		||||
                    $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
 | 
			
		||||
                    $id = data_get($data, 'id');
 | 
			
		||||
                    $charge = data_get($data, 'charge');
 | 
			
		||||
                    if ($charge) {
 | 
			
		||||
                        $stripe->refunds->create(['charge' => $charge]);
 | 
			
		||||
                    }
 | 
			
		||||
                    $pi = data_get($data, 'payment_intent');
 | 
			
		||||
                    $piData = $stripe->paymentIntents->retrieve($pi, []);
 | 
			
		||||
                    $customerId = data_get($piData, 'customer');
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                    if ($subscription) {
 | 
			
		||||
                        $subscriptionId = data_get($subscription, 'stripe_subscription_id');
 | 
			
		||||
                        $stripe->subscriptions->cancel($subscriptionId, []);
 | 
			
		||||
                        $subscription->update([
 | 
			
		||||
                            'stripe_invoice_paid' => false,
 | 
			
		||||
                        ]);
 | 
			
		||||
                        send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
 | 
			
		||||
                    } else {
 | 
			
		||||
                        send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
 | 
			
		||||
            StripeProcessJob::dispatch($event);
 | 
			
		||||
 | 
			
		||||
                        return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'checkout.session.completed':
 | 
			
		||||
                    $clientReferenceId = data_get($data, 'client_reference_id');
 | 
			
		||||
                    if (is_null($clientReferenceId)) {
 | 
			
		||||
                        send_internal_notification('Checkout session completed without client reference id.');
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    $userId = Str::before($clientReferenceId, ':');
 | 
			
		||||
                    $teamId = Str::after($clientReferenceId, ':');
 | 
			
		||||
                    $subscriptionId = data_get($data, 'subscription');
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $team = Team::find($teamId);
 | 
			
		||||
                    $found = $team->members->where('id', $userId)->first();
 | 
			
		||||
                    if (! $found->isAdmin()) {
 | 
			
		||||
                        send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
 | 
			
		||||
 | 
			
		||||
                        return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
 | 
			
		||||
                    }
 | 
			
		||||
                    $subscription = Subscription::where('team_id', $teamId)->first();
 | 
			
		||||
                    if ($subscription) {
 | 
			
		||||
                        // send_internal_notification('Old subscription activated for team: '.$teamId);
 | 
			
		||||
                        $subscription->update([
 | 
			
		||||
                            'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                            'stripe_customer_id' => $customerId,
 | 
			
		||||
                            'stripe_invoice_paid' => true,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // send_internal_notification('New subscription for team: '.$teamId);
 | 
			
		||||
                        Subscription::create([
 | 
			
		||||
                            'team_id' => $teamId,
 | 
			
		||||
                            'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                            'stripe_customer_id' => $customerId,
 | 
			
		||||
                            'stripe_invoice_paid' => true,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'invoice.paid':
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $planId = data_get($data, 'lines.data.0.plan.id');
 | 
			
		||||
                    if (Str::contains($excludedPlans, $planId)) {
 | 
			
		||||
                        // send_internal_notification('Subscription excluded.');
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                    if ($subscription) {
 | 
			
		||||
                        $subscription->update([
 | 
			
		||||
                            'stripe_invoice_paid' => true,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return response("No subscription found for customer: {$customerId}", 400);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'invoice.payment_failed':
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                    if (! $subscription) {
 | 
			
		||||
                        // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
 | 
			
		||||
 | 
			
		||||
                        return response('No subscription found in Coolify.');
 | 
			
		||||
                    }
 | 
			
		||||
                    $team = data_get($subscription, 'team');
 | 
			
		||||
                    if (! $team) {
 | 
			
		||||
                        // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
 | 
			
		||||
 | 
			
		||||
                        return response('No team found in Coolify.');
 | 
			
		||||
                    }
 | 
			
		||||
                    if (! $subscription->stripe_invoice_paid) {
 | 
			
		||||
                        SubscriptionInvoiceFailedJob::dispatch($team);
 | 
			
		||||
                        // send_internal_notification('Invoice payment failed: '.$customerId);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // send_internal_notification('Invoice payment failed but already paid: '.$customerId);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'payment_intent.payment_failed':
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                    if (! $subscription) {
 | 
			
		||||
                        // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
 | 
			
		||||
 | 
			
		||||
                        return response('No subscription found in Coolify.');
 | 
			
		||||
                    }
 | 
			
		||||
                    if ($subscription->stripe_invoice_paid) {
 | 
			
		||||
                        // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
 | 
			
		||||
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    send_internal_notification('Subscription payment failed for customer: '.$customerId);
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'customer.subscription.created':
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $subscriptionId = data_get($data, 'id');
 | 
			
		||||
                    $teamId = data_get($data, 'metadata.team_id');
 | 
			
		||||
                    $userId = data_get($data, 'metadata.user_id');
 | 
			
		||||
                    if (! $teamId || ! $userId) {
 | 
			
		||||
                        $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                        if ($subscription) {
 | 
			
		||||
                            return response("Subscription already exists for customer: {$customerId}", 200);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return response('No team id or user id found', 400);
 | 
			
		||||
                    }
 | 
			
		||||
                    $team = Team::find($teamId);
 | 
			
		||||
                    $found = $team->members->where('id', $userId)->first();
 | 
			
		||||
                    if (! $found->isAdmin()) {
 | 
			
		||||
                        send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
 | 
			
		||||
 | 
			
		||||
                        return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
 | 
			
		||||
                    }
 | 
			
		||||
                    $subscription = Subscription::where('team_id', $teamId)->first();
 | 
			
		||||
                    if ($subscription) {
 | 
			
		||||
                        return response("Subscription already exists for team: {$teamId}", 200);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Subscription::create([
 | 
			
		||||
                            'team_id' => $teamId,
 | 
			
		||||
                            'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                            'stripe_customer_id' => $customerId,
 | 
			
		||||
                            'stripe_invoice_paid' => false,
 | 
			
		||||
                        ]);
 | 
			
		||||
 | 
			
		||||
                        return response('Subscription created');
 | 
			
		||||
                    }
 | 
			
		||||
                case 'customer.subscription.updated':
 | 
			
		||||
                    $teamId = data_get($data, 'metadata.team_id');
 | 
			
		||||
                    $userId = data_get($data, 'metadata.user_id');
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $status = data_get($data, 'status');
 | 
			
		||||
                    $subscriptionId = data_get($data, 'items.data.0.subscription');
 | 
			
		||||
                    $planId = data_get($data, 'items.data.0.plan.id');
 | 
			
		||||
                    if (Str::contains($excludedPlans, $planId)) {
 | 
			
		||||
                        // send_internal_notification('Subscription excluded.');
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                    if (! $subscription) {
 | 
			
		||||
                        if ($status === 'incomplete_expired') {
 | 
			
		||||
                            return response('Subscription incomplete expired', 200);
 | 
			
		||||
                        }
 | 
			
		||||
                        if ($teamId) {
 | 
			
		||||
                            $subscription = Subscription::create([
 | 
			
		||||
                                'team_id' => $teamId,
 | 
			
		||||
                                'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                                'stripe_customer_id' => $customerId,
 | 
			
		||||
                                'stripe_invoice_paid' => false,
 | 
			
		||||
                            ]);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            return response('No subscription and team id found', 400);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
 | 
			
		||||
                    $feedback = data_get($data, 'cancellation_details.feedback');
 | 
			
		||||
                    $comment = data_get($data, 'cancellation_details.comment');
 | 
			
		||||
                    $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
 | 
			
		||||
                    if (str($lookup_key)->contains('dynamic')) {
 | 
			
		||||
                        $quantity = data_get($data, 'items.data.0.quantity', 2);
 | 
			
		||||
                        $team = data_get($subscription, 'team');
 | 
			
		||||
                        if ($team) {
 | 
			
		||||
                            $team->update([
 | 
			
		||||
                                'custom_server_limit' => $quantity,
 | 
			
		||||
                            ]);
 | 
			
		||||
                        }
 | 
			
		||||
                        ServerLimitCheckJob::dispatch($team);
 | 
			
		||||
                    }
 | 
			
		||||
                    $subscription->update([
 | 
			
		||||
                        'stripe_feedback' => $feedback,
 | 
			
		||||
                        'stripe_comment' => $comment,
 | 
			
		||||
                        'stripe_plan_id' => $planId,
 | 
			
		||||
                        'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
 | 
			
		||||
                    ]);
 | 
			
		||||
                    if ($status === 'paused' || $status === 'incomplete_expired') {
 | 
			
		||||
                        $subscription->update([
 | 
			
		||||
                            'stripe_invoice_paid' => false,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    }
 | 
			
		||||
                    if ($feedback) {
 | 
			
		||||
                        $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
 | 
			
		||||
                        if ($comment) {
 | 
			
		||||
                            $reason .= ' with comment: \''.$comment."'";
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'customer.subscription.deleted':
 | 
			
		||||
                    // End subscription
 | 
			
		||||
                    $customerId = data_get($data, 'customer');
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
 | 
			
		||||
                    $team = data_get($subscription, 'team');
 | 
			
		||||
                    $team?->subscriptionEnded();
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    // Unhandled event type
 | 
			
		||||
            }
 | 
			
		||||
            return response('Webhook received. Cool cool cool cool cool.', 200);
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            if ($type !== 'payment_intent.payment_failed') {
 | 
			
		||||
                send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
 | 
			
		||||
            }
 | 
			
		||||
            $webhook->update([
 | 
			
		||||
            $this->webhook->update([
 | 
			
		||||
                'status' => 'failed',
 | 
			
		||||
                'failure_reason' => $e->getMessage(),
 | 
			
		||||
            ]);
 | 
			
		||||
 
 | 
			
		||||
@@ -199,7 +199,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
 | 
			
		||||
                $databasesToBackup = data_get($this->backup, 'databases_to_backup');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (is_null($databasesToBackup)) {
 | 
			
		||||
            if (filled($databasesToBackup)) {
 | 
			
		||||
                if (str($databaseType)->contains('postgres')) {
 | 
			
		||||
                    $databasesToBackup = [$this->database->postgres_db];
 | 
			
		||||
                } elseif (str($databaseType)->contains('mongodb')) {
 | 
			
		||||
@@ -320,12 +320,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
 | 
			
		||||
                            'filename' => null,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    }
 | 
			
		||||
                    send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
 | 
			
		||||
                    $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (\Throwable $e) {
 | 
			
		||||
            send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
 | 
			
		||||
            throw $e;
 | 
			
		||||
        } finally {
 | 
			
		||||
            if ($this->team) {
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
 | 
			
		||||
                $this->resource?->delete_connected_networks($this->resource->uuid);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (\Throwable $e) {
 | 
			
		||||
            send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
 | 
			
		||||
            throw $e;
 | 
			
		||||
        } finally {
 | 
			
		||||
            $this->resource->forceDelete();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										242
									
								
								app/Jobs/StripeProcessJob.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								app/Jobs/StripeProcessJob.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,242 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Jobs;
 | 
			
		||||
 | 
			
		||||
use App\Models\Subscription;
 | 
			
		||||
use App\Models\Team;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Foundation\Queue\Queueable;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
 | 
			
		||||
class StripeProcessJob implements ShouldQueue
 | 
			
		||||
{
 | 
			
		||||
    use Queueable;
 | 
			
		||||
 | 
			
		||||
    public $type;
 | 
			
		||||
 | 
			
		||||
    public $webhook;
 | 
			
		||||
 | 
			
		||||
    public $tries = 3;
 | 
			
		||||
 | 
			
		||||
    public function __construct(public $event)
 | 
			
		||||
    {
 | 
			
		||||
        $this->onQueue('high');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handle(): void
 | 
			
		||||
    {
 | 
			
		||||
        $excludedPlans = config('subscription.stripe_excluded_plans');
 | 
			
		||||
 | 
			
		||||
        $type = data_get($this->event, 'type');
 | 
			
		||||
        $this->type = $type;
 | 
			
		||||
        $data = data_get($this->event, 'data.object');
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'radar.early_fraud_warning.created':
 | 
			
		||||
                $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
 | 
			
		||||
                $id = data_get($data, 'id');
 | 
			
		||||
                $charge = data_get($data, 'charge');
 | 
			
		||||
                if ($charge) {
 | 
			
		||||
                    $stripe->refunds->create(['charge' => $charge]);
 | 
			
		||||
                }
 | 
			
		||||
                $pi = data_get($data, 'payment_intent');
 | 
			
		||||
                $piData = $stripe->paymentIntents->retrieve($pi, []);
 | 
			
		||||
                $customerId = data_get($piData, 'customer');
 | 
			
		||||
                $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                if ($subscription) {
 | 
			
		||||
                    $subscriptionId = data_get($subscription, 'stripe_subscription_id');
 | 
			
		||||
                    $stripe->subscriptions->cancel($subscriptionId, []);
 | 
			
		||||
                    $subscription->update([
 | 
			
		||||
                        'stripe_invoice_paid' => false,
 | 
			
		||||
                    ]);
 | 
			
		||||
                    send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
 | 
			
		||||
                } else {
 | 
			
		||||
                    send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
 | 
			
		||||
                    throw new \Exception("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 'checkout.session.completed':
 | 
			
		||||
                $clientReferenceId = data_get($data, 'client_reference_id');
 | 
			
		||||
                if (is_null($clientReferenceId)) {
 | 
			
		||||
                    send_internal_notification('Checkout session completed without client reference id.');
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                $userId = Str::before($clientReferenceId, ':');
 | 
			
		||||
                $teamId = Str::after($clientReferenceId, ':');
 | 
			
		||||
                $subscriptionId = data_get($data, 'subscription');
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $team = Team::find($teamId);
 | 
			
		||||
                $found = $team->members->where('id', $userId)->first();
 | 
			
		||||
                if (! $found->isAdmin()) {
 | 
			
		||||
                    send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
 | 
			
		||||
                    throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
 | 
			
		||||
                }
 | 
			
		||||
                $subscription = Subscription::where('team_id', $teamId)->first();
 | 
			
		||||
                if ($subscription) {
 | 
			
		||||
                    send_internal_notification('Old subscription activated for team: '.$teamId);
 | 
			
		||||
                    $subscription->update([
 | 
			
		||||
                        'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                        'stripe_customer_id' => $customerId,
 | 
			
		||||
                        'stripe_invoice_paid' => true,
 | 
			
		||||
                    ]);
 | 
			
		||||
                } else {
 | 
			
		||||
                    send_internal_notification('New subscription for team: '.$teamId);
 | 
			
		||||
                    Subscription::create([
 | 
			
		||||
                        'team_id' => $teamId,
 | 
			
		||||
                        'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                        'stripe_customer_id' => $customerId,
 | 
			
		||||
                        'stripe_invoice_paid' => true,
 | 
			
		||||
                    ]);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 'invoice.paid':
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $planId = data_get($data, 'lines.data.0.plan.id');
 | 
			
		||||
                if (Str::contains($excludedPlans, $planId)) {
 | 
			
		||||
                    send_internal_notification('Subscription excluded.');
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                if ($subscription) {
 | 
			
		||||
                    $subscription->update([
 | 
			
		||||
                        'stripe_invoice_paid' => true,
 | 
			
		||||
                    ]);
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw new \Exception("No subscription found for customer: {$customerId}");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 'invoice.payment_failed':
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                if (! $subscription) {
 | 
			
		||||
                    send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
 | 
			
		||||
                    throw new \Exception("No subscription found for customer: {$customerId}");
 | 
			
		||||
                }
 | 
			
		||||
                $team = data_get($subscription, 'team');
 | 
			
		||||
                if (! $team) {
 | 
			
		||||
                    send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
 | 
			
		||||
                    throw new \Exception("No team found in Coolify for customer: {$customerId}");
 | 
			
		||||
                }
 | 
			
		||||
                if (! $subscription->stripe_invoice_paid) {
 | 
			
		||||
                    SubscriptionInvoiceFailedJob::dispatch($team);
 | 
			
		||||
                    send_internal_notification('Invoice payment failed: '.$customerId);
 | 
			
		||||
                } else {
 | 
			
		||||
                    send_internal_notification('Invoice payment failed but already paid: '.$customerId);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 'payment_intent.payment_failed':
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                if (! $subscription) {
 | 
			
		||||
                    send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
 | 
			
		||||
                    throw new \Exception("No subscription found in Coolify for customer: {$customerId}");
 | 
			
		||||
                }
 | 
			
		||||
                if ($subscription->stripe_invoice_paid) {
 | 
			
		||||
                    send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                send_internal_notification('Subscription payment failed for customer: '.$customerId);
 | 
			
		||||
                break;
 | 
			
		||||
            case 'customer.subscription.created':
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $subscriptionId = data_get($data, 'id');
 | 
			
		||||
                $teamId = data_get($data, 'metadata.team_id');
 | 
			
		||||
                $userId = data_get($data, 'metadata.user_id');
 | 
			
		||||
                if (! $teamId || ! $userId) {
 | 
			
		||||
                    $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                    if ($subscription) {
 | 
			
		||||
                        throw new \Exception("Subscription already exists for customer: {$customerId}");
 | 
			
		||||
                    }
 | 
			
		||||
                    throw new \Exception('No team id or user id found');
 | 
			
		||||
                }
 | 
			
		||||
                $team = Team::find($teamId);
 | 
			
		||||
                $found = $team->members->where('id', $userId)->first();
 | 
			
		||||
                if (! $found->isAdmin()) {
 | 
			
		||||
                    send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
 | 
			
		||||
                    throw new \Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
 | 
			
		||||
                }
 | 
			
		||||
                $subscription = Subscription::where('team_id', $teamId)->first();
 | 
			
		||||
                if ($subscription) {
 | 
			
		||||
                    send_internal_notification("Subscription already exists for team: {$teamId}");
 | 
			
		||||
                    throw new \Exception("Subscription already exists for team: {$teamId}");
 | 
			
		||||
                } else {
 | 
			
		||||
                    Subscription::create([
 | 
			
		||||
                        'team_id' => $teamId,
 | 
			
		||||
                        'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                        'stripe_customer_id' => $customerId,
 | 
			
		||||
                        'stripe_invoice_paid' => false,
 | 
			
		||||
                    ]);
 | 
			
		||||
                }
 | 
			
		||||
            case 'customer.subscription.updated':
 | 
			
		||||
                $teamId = data_get($data, 'metadata.team_id');
 | 
			
		||||
                $userId = data_get($data, 'metadata.user_id');
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $status = data_get($data, 'status');
 | 
			
		||||
                $subscriptionId = data_get($data, 'items.data.0.subscription');
 | 
			
		||||
                $planId = data_get($data, 'items.data.0.plan.id');
 | 
			
		||||
                if (Str::contains($excludedPlans, $planId)) {
 | 
			
		||||
                    send_internal_notification('Subscription excluded.');
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
 | 
			
		||||
                if (! $subscription) {
 | 
			
		||||
                    if ($status === 'incomplete_expired') {
 | 
			
		||||
                        send_internal_notification('Subscription incomplete expired');
 | 
			
		||||
                        throw new \Exception('Subscription incomplete expired');
 | 
			
		||||
                    }
 | 
			
		||||
                    if ($teamId) {
 | 
			
		||||
                        $subscription = Subscription::create([
 | 
			
		||||
                            'team_id' => $teamId,
 | 
			
		||||
                            'stripe_subscription_id' => $subscriptionId,
 | 
			
		||||
                            'stripe_customer_id' => $customerId,
 | 
			
		||||
                            'stripe_invoice_paid' => false,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        send_internal_notification('No subscription and team id found');
 | 
			
		||||
                        throw new \Exception('No subscription and team id found');
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
 | 
			
		||||
                $feedback = data_get($data, 'cancellation_details.feedback');
 | 
			
		||||
                $comment = data_get($data, 'cancellation_details.comment');
 | 
			
		||||
                $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
 | 
			
		||||
                if (str($lookup_key)->contains('dynamic')) {
 | 
			
		||||
                    $quantity = data_get($data, 'items.data.0.quantity', 2);
 | 
			
		||||
                    $team = data_get($subscription, 'team');
 | 
			
		||||
                    if ($team) {
 | 
			
		||||
                        $team->update([
 | 
			
		||||
                            'custom_server_limit' => $quantity,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    }
 | 
			
		||||
                    ServerLimitCheckJob::dispatch($team);
 | 
			
		||||
                }
 | 
			
		||||
                $subscription->update([
 | 
			
		||||
                    'stripe_feedback' => $feedback,
 | 
			
		||||
                    'stripe_comment' => $comment,
 | 
			
		||||
                    'stripe_plan_id' => $planId,
 | 
			
		||||
                    'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
 | 
			
		||||
                ]);
 | 
			
		||||
                if ($status === 'paused' || $status === 'incomplete_expired') {
 | 
			
		||||
                    $subscription->update([
 | 
			
		||||
                        'stripe_invoice_paid' => false,
 | 
			
		||||
                    ]);
 | 
			
		||||
                }
 | 
			
		||||
                if ($feedback) {
 | 
			
		||||
                    $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
 | 
			
		||||
                    if ($comment) {
 | 
			
		||||
                        $reason .= ' with comment: \''.$comment."'";
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case 'customer.subscription.deleted':
 | 
			
		||||
                // End subscription
 | 
			
		||||
                $customerId = data_get($data, 'customer');
 | 
			
		||||
                $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
 | 
			
		||||
                $team = data_get($subscription, 'team');
 | 
			
		||||
                $team?->subscriptionEnded();
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                // Unhandled event type
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -73,6 +73,9 @@ class Email extends Component
 | 
			
		||||
    #[Validate(['nullable', 'string'])]
 | 
			
		||||
    public ?string $resendApiKey = null;
 | 
			
		||||
 | 
			
		||||
    #[Validate(['required', 'email'])]
 | 
			
		||||
    public string $testEmailAddress = '';
 | 
			
		||||
 | 
			
		||||
    public function mount()
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
@@ -132,14 +135,21 @@ class Email extends Component
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function sendTestNotification()
 | 
			
		||||
    public function sendTestEmail()
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $this->validate([
 | 
			
		||||
                'testEmailAddress' => 'required|email',
 | 
			
		||||
            ], [
 | 
			
		||||
                'testEmailAddress.required' => 'Test email address is required.',
 | 
			
		||||
                'testEmailAddress.email' => 'Please enter a valid email address.',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $executed = RateLimiter::attempt(
 | 
			
		||||
                'test-email:'.$this->team->id,
 | 
			
		||||
                $perMinute = 0,
 | 
			
		||||
                function () {
 | 
			
		||||
                    $this->team?->notify(new Test($this->emails));
 | 
			
		||||
                    $this->team?->notify(new Test($this->testEmailAddress));
 | 
			
		||||
                    $this->dispatch('success', 'Test Email sent.');
 | 
			
		||||
                },
 | 
			
		||||
                $decaySeconds = 10,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
return [
 | 
			
		||||
    'coolify' => [
 | 
			
		||||
        'version' => '4.0.0-beta.371',
 | 
			
		||||
        'version' => '4.0.0-beta.372',
 | 
			
		||||
        'self_hosted' => env('SELF_HOSTED', true),
 | 
			
		||||
        'autoupdate' => env('AUTOUPDATE'),
 | 
			
		||||
        'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
return '4.0.0-beta.371';
 | 
			
		||||
return '4.0.0-beta.372';
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,9 @@
 | 
			
		||||
            @endif
 | 
			
		||||
            @if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
 | 
			
		||||
                <x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
 | 
			
		||||
                    <form wire:submit='submit' class="flex flex-col w-full gap-2">
 | 
			
		||||
                        <x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
 | 
			
		||||
                        <x-forms.button wire:click="sendTestNotification" @click="modalOpen=false">
 | 
			
		||||
                    <form wire:submit.prevent="sendTestEmail" class="flex flex-col w-full gap-2">
 | 
			
		||||
                        <x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress" label="Recipients" required />
 | 
			
		||||
                        <x-forms.button type="submit" @click="modalOpen=false">
 | 
			
		||||
                            Send Email
 | 
			
		||||
                        </x-forms.button>
 | 
			
		||||
                    </form>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,10 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
    "coolify": {
 | 
			
		||||
        "v4": {
 | 
			
		||||
            "version": "4.0.0-beta.371"
 | 
			
		||||
            "version": "4.0.0-beta.372"
 | 
			
		||||
        },
 | 
			
		||||
        "nightly": {
 | 
			
		||||
            "version": "4.0.0-beta.372"
 | 
			
		||||
            "version": "4.0.0-beta.373"
 | 
			
		||||
        },
 | 
			
		||||
        "helper": {
 | 
			
		||||
            "version": "1.0.4"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user