cloud: send notification email if payment

This commit is contained in:
Andras Bacsai 2024-01-24 11:28:01 +01:00
parent 01f7b07fa3
commit 6ecb9c21ce
2 changed files with 141 additions and 126 deletions

View File

@ -5,13 +5,13 @@
<h1>Dashboard</h1> <h1>Dashboard</h1>
<div class="subtitle">Your self-hosted environment</div> <div class="subtitle">Your self-hosted environment</div>
@if (request()->query->get('success')) @if (request()->query->get('success'))
<div class="rounded alert alert-success"> <div class="text-white rounded alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<span>Your subscription has been activated! Welcome onboard!</span> <span>Your subscription has been activated! Welcome onboard! <br>It could take a few seconds before your subscription is activated.<br> Please be patient.</span>
</div> </div>
@endif @endif
@if ($projects->count() === 0 && $servers->count() === 0) @if ($projects->count() === 0 && $servers->count() === 0)

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob; use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob; use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Application; use App\Models\Application;
@ -593,6 +594,16 @@ Route::post('/payments/stripe/events', function () {
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
]); ]);
break; break;
case 'invoice.payment_failed':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (!$team) {
throw new Exception('No team found for subscription: ' . $subscription->id);
}
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: ' . $subscription->team->id);
break;
case 'payment_intent.payment_failed': case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
@ -610,7 +621,11 @@ Route::post('/payments/stripe/events', function () {
send_internal_notification('Subscription excluded.'); send_internal_notification('Subscription excluded.');
break; break;
} }
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
}
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended'); $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end'); $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
@ -703,129 +718,129 @@ Route::post('/payments/stripe/events', function () {
return response($e->getMessage(), 400); return response($e->getMessage(), 400);
} }
}); });
Route::post('/payments/paddle/events', function () { // Route::post('/payments/paddle/events', function () {
try { // try {
$payload = request()->all(); // $payload = request()->all();
$signature = request()->header('Paddle-Signature'); // $signature = request()->header('Paddle-Signature');
$ts = Str::of($signature)->after('ts=')->before(';'); // $ts = Str::of($signature)->after('ts=')->before(';');
$h1 = Str::of($signature)->after('h1='); // $h1 = Str::of($signature)->after('h1=');
$signedPayload = $ts->value . ':' . request()->getContent(); // $signedPayload = $ts->value . ':' . request()->getContent();
$verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret')); // $verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret'));
if (!hash_equals($verify, $h1->value)) { // if (!hash_equals($verify, $h1->value)) {
return response('Invalid signature.', 400); // return response('Invalid signature.', 400);
} // }
$eventType = data_get($payload, 'event_type'); // $eventType = data_get($payload, 'event_type');
$webhook = Webhook::create([ // $webhook = Webhook::create([
'type' => 'paddle', // 'type' => 'paddle',
'payload' => $payload, // 'payload' => $payload,
]); // ]);
// TODO - Handle events // // TODO - Handle events
switch ($eventType) { // switch ($eventType) {
case 'subscription.activated': // case 'subscription.activated':
} // }
ray('Subscription event: ' . $eventType); // ray('Subscription event: ' . $eventType);
$webhook->update([ // $webhook->update([
'status' => 'success', // 'status' => 'success',
]); // ]);
} catch (Exception $e) { // } catch (Exception $e) {
ray($e->getMessage()); // ray($e->getMessage());
send_internal_notification('Subscription webhook failed: ' . $e->getMessage()); // send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
$webhook->update([ // $webhook->update([
'status' => 'failed', // 'status' => 'failed',
'failure_reason' => $e->getMessage(), // 'failure_reason' => $e->getMessage(),
]); // ]);
} finally { // } finally {
return response('OK'); // return response('OK');
} // }
}); // });
Route::post('/payments/lemon/events', function () { // Route::post('/payments/lemon/events', function () {
try { // try {
$secret = config('subscription.lemon_squeezy_webhook_secret'); // $secret = config('subscription.lemon_squeezy_webhook_secret');
$payload = request()->collect(); // $payload = request()->collect();
$hash = hash_hmac('sha256', $payload, $secret); // $hash = hash_hmac('sha256', $payload, $secret);
$signature = request()->header('X-Signature'); // $signature = request()->header('X-Signature');
if (!hash_equals($hash, $signature)) { // if (!hash_equals($hash, $signature)) {
return response('Invalid signature.', 400); // return response('Invalid signature.', 400);
} // }
$webhook = Webhook::create([ // $webhook = Webhook::create([
'type' => 'lemonsqueezy', // 'type' => 'lemonsqueezy',
'payload' => $payload, // 'payload' => $payload,
]); // ]);
$event = data_get($payload, 'meta.event_name'); // $event = data_get($payload, 'meta.event_name');
ray('Subscription event: ' . $event); // ray('Subscription event: ' . $event);
$email = data_get($payload, 'data.attributes.user_email'); // $email = data_get($payload, 'data.attributes.user_email');
$team_id = data_get($payload, 'meta.custom_data.team_id'); // $team_id = data_get($payload, 'meta.custom_data.team_id');
if (is_null($team_id) || empty($team_id)) { // if (is_null($team_id) || empty($team_id)) {
throw new Exception('No team_id found in webhook payload.'); // throw new Exception('No team_id found in webhook payload.');
} // }
$subscription_id = data_get($payload, 'data.id'); // $subscription_id = data_get($payload, 'data.id');
$order_id = data_get($payload, 'data.attributes.order_id'); // $order_id = data_get($payload, 'data.attributes.order_id');
$product_id = data_get($payload, 'data.attributes.product_id'); // $product_id = data_get($payload, 'data.attributes.product_id');
$variant_id = data_get($payload, 'data.attributes.variant_id'); // $variant_id = data_get($payload, 'data.attributes.variant_id');
$variant_name = data_get($payload, 'data.attributes.variant_name'); // $variant_name = data_get($payload, 'data.attributes.variant_name');
$customer_id = data_get($payload, 'data.attributes.customer_id'); // $customer_id = data_get($payload, 'data.attributes.customer_id');
$status = data_get($payload, 'data.attributes.status'); // $status = data_get($payload, 'data.attributes.status');
$trial_ends_at = data_get($payload, 'data.attributes.trial_ends_at'); // $trial_ends_at = data_get($payload, 'data.attributes.trial_ends_at');
$renews_at = data_get($payload, 'data.attributes.renews_at'); // $renews_at = data_get($payload, 'data.attributes.renews_at');
$ends_at = data_get($payload, 'data.attributes.ends_at'); // $ends_at = data_get($payload, 'data.attributes.ends_at');
$update_payment_method = data_get($payload, 'data.attributes.urls.update_payment_method'); // $update_payment_method = data_get($payload, 'data.attributes.urls.update_payment_method');
$team = Team::find($team_id); // $team = Team::find($team_id);
$found = $team->members->where('email', $email)->first(); // $found = $team->members->where('email', $email)->first();
if (!$found->isAdmin()) { // if (!$found->isAdmin()) {
throw new Exception("User {$email} is not an admin or owner of team {$team->id}."); // throw new Exception("User {$email} is not an admin or owner of team {$team->id}.");
} // }
switch ($event) { // switch ($event) {
case 'subscription_created': // case 'subscription_created':
case 'subscription_updated': // case 'subscription_updated':
case 'subscription_resumed': // case 'subscription_resumed':
case 'subscription_unpaused': // case 'subscription_unpaused':
send_internal_notification("LemonSqueezy Event (`$event`): `" . $email . '` with status `' . $status . '`, tier: `' . $variant_name . '`'); // send_internal_notification("LemonSqueezy Event (`$event`): `" . $email . '` with status `' . $status . '`, tier: `' . $variant_name . '`');
$subscription = Subscription::updateOrCreate([ // $subscription = Subscription::updateOrCreate([
'team_id' => $team_id, // 'team_id' => $team_id,
], [ // ], [
'lemon_subscription_id' => $subscription_id, // 'lemon_subscription_id' => $subscription_id,
'lemon_customer_id' => $customer_id, // 'lemon_customer_id' => $customer_id,
'lemon_order_id' => $order_id, // 'lemon_order_id' => $order_id,
'lemon_product_id' => $product_id, // 'lemon_product_id' => $product_id,
'lemon_variant_id' => $variant_id, // 'lemon_variant_id' => $variant_id,
'lemon_status' => $status, // 'lemon_status' => $status,
'lemon_variant_name' => $variant_name, // 'lemon_variant_name' => $variant_name,
'lemon_trial_ends_at' => $trial_ends_at, // 'lemon_trial_ends_at' => $trial_ends_at,
'lemon_renews_at' => $renews_at, // 'lemon_renews_at' => $renews_at,
'lemon_ends_at' => $ends_at, // 'lemon_ends_at' => $ends_at,
'lemon_update_payment_menthod_url' => $update_payment_method, // 'lemon_update_payment_menthod_url' => $update_payment_method,
]); // ]);
break; // break;
case 'subscription_cancelled': // case 'subscription_cancelled':
case 'subscription_paused': // case 'subscription_paused':
case 'subscription_expired': // case 'subscription_expired':
$subscription = Subscription::where('team_id', $team_id)->where('lemon_order_id', $order_id)->first(); // $subscription = Subscription::where('team_id', $team_id)->where('lemon_order_id', $order_id)->first();
if ($subscription) { // if ($subscription) {
send_internal_notification("LemonSqueezy Event (`$event`): " . $subscription_id . ' for team ' . $team_id . ' with status ' . $status); // send_internal_notification("LemonSqueezy Event (`$event`): " . $subscription_id . ' for team ' . $team_id . ' with status ' . $status);
$subscription->update([ // $subscription->update([
'lemon_status' => $status, // 'lemon_status' => $status,
'lemon_trial_ends_at' => $trial_ends_at, // 'lemon_trial_ends_at' => $trial_ends_at,
'lemon_renews_at' => $renews_at, // 'lemon_renews_at' => $renews_at,
'lemon_ends_at' => $ends_at, // 'lemon_ends_at' => $ends_at,
'lemon_update_payment_menthod_url' => $update_payment_method, // 'lemon_update_payment_menthod_url' => $update_payment_method,
]); // ]);
} // }
break; // break;
} // }
$webhook->update([ // $webhook->update([
'status' => 'success', // 'status' => 'success',
]); // ]);
} catch (Exception $e) { // } catch (Exception $e) {
ray($e->getMessage()); // ray($e->getMessage());
send_internal_notification('Subscription webhook failed: ' . $e->getMessage()); // send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
$webhook->update([ // $webhook->update([
'status' => 'failed', // 'status' => 'failed',
'failure_reason' => $e->getMessage(), // 'failure_reason' => $e->getMessage(),
]); // ]);
} finally { // } finally {
return response('OK'); // return response('OK');
} // }
}); // });