feat: trial

This commit is contained in:
Andras Bacsai 2023-09-12 11:19:21 +02:00
parent be30651172
commit 6da4e78374
13 changed files with 214 additions and 15 deletions

View File

@ -92,7 +92,6 @@ private function remove_from_waitlist()
}
private function send_email()
{
ray($this->next_patient->email, $this->password);
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage();

View File

@ -30,7 +30,7 @@ public function submit()
try {
$this->rateLimit(1, 60);
$this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'unknown';
$subscriptionType = auth()->user()?->subscription?->type();
$debug = "Route: {$this->path}";
$mail = new MailMessage();
$mail->view(

View File

@ -10,6 +10,7 @@ class PricingPlans extends Component
{
public function subscribeStripe($type)
{
$team = currentTeam();
Stripe::setApiKey(config('subscription.stripe_api_key'));
switch ($type) {
case 'basic-monthly':
@ -50,10 +51,23 @@ public function subscribeStripe($type)
'automatic_tax' => [
'enabled' => true,
],
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
$payload['subscription_data'] = [
'trial_period_days' => 30,
'trial_settings' => [
'end_behavior' => [
'missing_payment_method' => 'cancel',
]
],
];
$payload['payment_method_collection'] = 'if_required';
}
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;

View File

@ -0,0 +1,44 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndedJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {
}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->subject('Action required: You trial in Coolify Cloud ended.');
$mail->view('emails.trial-ended', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
ray('Sending trial ended email to ' . $member->email);
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to ' . $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndsSoonJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {
}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->subject('You trial in Coolify Cloud ends soon.');
$mail->view('emails.trial-ends-soon', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
ray('Sending trial ending email to ' . $member->email);
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to ' . $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@ -33,7 +33,7 @@ public function type()
}
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'unknown';
return 'zero';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {
@ -54,6 +54,6 @@ public function type()
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'unknown';
return 'zero';
}
}

View File

@ -120,4 +120,20 @@ public function s3s()
{
return $this->hasMany(S3Storage::class);
}
public function trialEnded() {
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => false,
'is_reachable' => false,
]);
}
}
public function trialEndedButSubscribed() {
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_trial_already_ended');
});
}
};

View File

@ -3,4 +3,4 @@
Thank you,<br>
{{ config('app.name') ?? 'Coolify' }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io/contact)') }}

View File

@ -21,6 +21,7 @@ class="sr-only">
</label>
</fieldset>
</div>
<div class="py-2 text-center"><span class="font-bold text-warning">30 days trial</span> included on all plans, without credit card details.</div>
<div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 ">
<div>Save <span class="font-bold text-warning">1 month</span> annually with the yearly plans.
</div>

View File

@ -0,0 +1,7 @@
<x-emails.layout>
Your trial ended. All automations and integrations are disabled for all of your servers.
Please update payment details [here]({{$stripeCustomerPortal}}) or in [Coolify Cloud](https://app.coolify.io) to continue using our services.
</x-emails.layout>

View File

@ -0,0 +1,7 @@
<x-emails.layout>
Your trial ends soon. Please update payment details [here]({{$stripeCustomerPortal}}),
Your servers & deployed resources will be untouched, but you won't be able to deploy new resources and lost all automations and integrations.
</x-emails.layout>

View File

@ -1,6 +1,7 @@
<?php
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\GithubApp;
@ -271,20 +272,17 @@
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$planId = data_get($data, 'lines.data.0.plan.id');
$subscription->update([
'stripe_plan_id' => $planId,
'stripe_invoice_paid' => true,
]);
break;
// case 'invoice.payment_failed':
// $customerId = data_get($data, 'customer');
// $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
// if ($subscription) {
// SubscriptionInvoiceFailedJob::dispatch($subscription->team);
// }
// break;
case 'customer.subscription.updated':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
@ -297,7 +295,19 @@
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
ray($feedback, $comment, $alreadyCancelAtPeriodEnd, $cancelAtPeriodEnd);
if ($status === 'paused') {
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification('Subscription paused for team: ' . $subscription->team->id);
}
// Trial ended but subscribed, reactive servers
if ($trialEndedAlready && $status === 'active') {
$team = data_get($subscription, 'team');
$team->trialEndedButSubscribed();
}
if ($feedback) {
$reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback . "'";
if ($comment) {
@ -305,7 +315,6 @@
}
send_internal_notification($reason);
}
ray($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd);
if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
if ($cancelAtPeriodEnd) {
send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
@ -315,16 +324,44 @@
}
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->trialEnded();
$subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => true,
]);
send_internal_notification('Subscription cancelled: ' . $subscription->team->id);
break;
case 'customer.subscription.trial_will_end':
$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);
}
SubscriptionTrialEndsSoonJob::dispatch($team);
break;
case 'customer.subscription.paused':
$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);
}
$team->trialEnded();
$subscription->update([
'stripe_trial_already_ended' => true,
'stripe_invoice_paid' => false,
]);
SubscriptionTrialEndedJob::dispatch($team);
send_internal_notification('Subscription paused for team: ' . $subscription->team->id);
break;
default:
// Unhandled event type
}