Merge branch 'next' into fix-environement-route
This commit is contained in:
@@ -3,21 +3,26 @@
|
|||||||
namespace App\Http\Controllers\Webhook;
|
namespace App\Http\Controllers\Webhook;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Jobs\ServerLimitCheckJob;
|
use App\Jobs\StripeProcessJob;
|
||||||
use App\Jobs\SubscriptionInvoiceFailedJob;
|
|
||||||
use App\Models\Subscription;
|
|
||||||
use App\Models\Team;
|
|
||||||
use App\Models\Webhook;
|
use App\Models\Webhook;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class Stripe extends Controller
|
class Stripe extends Controller
|
||||||
{
|
{
|
||||||
|
protected $webhook;
|
||||||
|
|
||||||
public function events(Request $request)
|
public function events(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||||
|
$signature = $request->header('Stripe-Signature');
|
||||||
|
$event = \Stripe\Webhook::constructEvent(
|
||||||
|
$request->getContent(),
|
||||||
|
$signature,
|
||||||
|
$webhookSecret
|
||||||
|
);
|
||||||
if (app()->isDownForMaintenance()) {
|
if (app()->isDownForMaintenance()) {
|
||||||
$epoch = now()->valueOf();
|
$epoch = now()->valueOf();
|
||||||
$data = [
|
$data = [
|
||||||
@@ -33,241 +38,17 @@ class Stripe extends Controller
|
|||||||
$json = json_encode($data);
|
$json = json_encode($data);
|
||||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
|
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');
|
$this->webhook = Webhook::create([
|
||||||
$signature = $request->header('Stripe-Signature');
|
|
||||||
$excludedPlans = config('subscription.stripe_excluded_plans');
|
|
||||||
$event = \Stripe\Webhook::constructEvent(
|
|
||||||
$request->getContent(),
|
|
||||||
$signature,
|
|
||||||
$webhookSecret
|
|
||||||
);
|
|
||||||
$webhook = Webhook::create([
|
|
||||||
'type' => 'stripe',
|
'type' => 'stripe',
|
||||||
'payload' => $request->getContent(),
|
'payload' => $request->getContent(),
|
||||||
]);
|
]);
|
||||||
$type = data_get($event, 'type');
|
StripeProcessJob::dispatch($event);
|
||||||
$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}");
|
|
||||||
|
|
||||||
return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
|
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($type !== 'payment_intent.payment_failed') {
|
$this->webhook->update([
|
||||||
send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
|
|
||||||
}
|
|
||||||
$webhook->update([
|
|
||||||
'status' => 'failed',
|
'status' => 'failed',
|
||||||
'failure_reason' => $e->getMessage(),
|
'failure_reason' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_null($databasesToBackup)) {
|
if (filled($databasesToBackup)) {
|
||||||
if (str($databaseType)->contains('postgres')) {
|
if (str($databaseType)->contains('postgres')) {
|
||||||
$databasesToBackup = [$this->database->postgres_db];
|
$databasesToBackup = [$this->database->postgres_db];
|
||||||
} elseif (str($databaseType)->contains('mongodb')) {
|
} elseif (str($databaseType)->contains('mongodb')) {
|
||||||
@@ -320,12 +320,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
'filename' => null,
|
'filename' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
|
|
||||||
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
|
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
|
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
} finally {
|
||||||
if ($this->team) {
|
if ($this->team) {
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$this->resource?->delete_connected_networks($this->resource->uuid);
|
$this->resource?->delete_connected_networks($this->resource->uuid);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
|
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
} finally {
|
||||||
$this->resource->forceDelete();
|
$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'])]
|
#[Validate(['nullable', 'string'])]
|
||||||
public ?string $resendApiKey = null;
|
public ?string $resendApiKey = null;
|
||||||
|
|
||||||
|
#[Validate(['required', 'email'])]
|
||||||
|
public string $testEmailAddress = '';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -132,14 +135,21 @@ class Email extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sendTestNotification()
|
public function sendTestEmail()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$this->validate([
|
||||||
|
'testEmailAddress' => 'required|email',
|
||||||
|
], [
|
||||||
|
'testEmailAddress.required' => 'Test email address is required.',
|
||||||
|
'testEmailAddress.email' => 'Please enter a valid email address.',
|
||||||
|
]);
|
||||||
|
|
||||||
$executed = RateLimiter::attempt(
|
$executed = RateLimiter::attempt(
|
||||||
'test-email:'.$this->team->id,
|
'test-email:'.$this->team->id,
|
||||||
$perMinute = 0,
|
$perMinute = 0,
|
||||||
function () {
|
function () {
|
||||||
$this->team?->notify(new Test($this->emails));
|
$this->team?->notify(new Test($this->testEmailAddress));
|
||||||
$this->dispatch('success', 'Test Email sent.');
|
$this->dispatch('success', 'Test Email sent.');
|
||||||
},
|
},
|
||||||
$decaySeconds = 10,
|
$decaySeconds = 10,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.371',
|
'version' => '4.0.0-beta.372',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
'autoupdate' => env('AUTOUPDATE'),
|
'autoupdate' => env('AUTOUPDATE'),
|
||||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return '4.0.0-beta.371';
|
return '4.0.0-beta.372';
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
@endif
|
@endif
|
||||||
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
|
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
|
||||||
<x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
|
<x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
|
||||||
<form wire:submit='submit' class="flex flex-col w-full gap-2">
|
<form wire:submit.prevent="sendTestEmail" class="flex flex-col w-full gap-2">
|
||||||
<x-forms.input placeholder="test@example.com" id="emails" label="Recipients" required />
|
<x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress" label="Recipients" required />
|
||||||
<x-forms.button wire:click="sendTestNotification" @click="modalOpen=false">
|
<x-forms.button type="submit" @click="modalOpen=false">
|
||||||
Send Email
|
Send Email
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.371"
|
"version": "4.0.0-beta.372"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.372"
|
"version": "4.0.0-beta.373"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.4"
|
"version": "1.0.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user