From 6da4e7837474adc2f7cfe72c2c6ddc1ee831a83a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 12 Sep 2023 11:19:21 +0200 Subject: [PATCH] feat: trial --- app/Console/Commands/WaitlistInvite.php | 1 - app/Http/Livewire/Help.php | 2 +- .../Livewire/Subscription/PricingPlans.php | 14 +++++ app/Jobs/SubscriptionTrialEndedJob.php | 44 ++++++++++++++ app/Jobs/SubscriptionTrialEndsSoonJob.php | 44 ++++++++++++++ app/Models/Subscription.php | 4 +- app/Models/Team.php | 16 ++++++ ...23_08_22_071059_add_stripe_trial_ended.php | 30 ++++++++++ .../views/components/emails/footer.blade.php | 2 +- .../views/components/pricing-plans.blade.php | 1 + resources/views/emails/trial-ended.blade.php | 7 +++ .../views/emails/trial-ends-soon.blade.php | 7 +++ routes/webhooks.php | 57 +++++++++++++++---- 13 files changed, 214 insertions(+), 15 deletions(-) create mode 100755 app/Jobs/SubscriptionTrialEndedJob.php create mode 100755 app/Jobs/SubscriptionTrialEndsSoonJob.php create mode 100644 database/migrations/2023_08_22_071059_add_stripe_trial_ended.php create mode 100644 resources/views/emails/trial-ended.blade.php create mode 100644 resources/views/emails/trial-ends-soon.blade.php diff --git a/app/Console/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php index dd629d706..f3eefbcfa 100644 --- a/app/Console/Commands/WaitlistInvite.php +++ b/app/Console/Commands/WaitlistInvite.php @@ -92,7 +92,6 @@ class WaitlistInvite extends Command } 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(); diff --git a/app/Http/Livewire/Help.php b/app/Http/Livewire/Help.php index 9f3ca434f..505224d2c 100644 --- a/app/Http/Livewire/Help.php +++ b/app/Http/Livewire/Help.php @@ -30,7 +30,7 @@ class Help extends Component 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( diff --git a/app/Http/Livewire/Subscription/PricingPlans.php b/app/Http/Livewire/Subscription/PricingPlans.php index 80cc81dcf..d200ecd15 100644 --- a/app/Http/Livewire/Subscription/PricingPlans.php +++ b/app/Http/Livewire/Subscription/PricingPlans.php @@ -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 @@ class PricingPlans extends Component '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; diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php new file mode 100755 index 000000000..39acd19a2 --- /dev/null +++ b/app/Jobs/SubscriptionTrialEndedJob.php @@ -0,0 +1,44 @@ +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; + } + } +} diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php new file mode 100755 index 000000000..84bd8ee66 --- /dev/null +++ b/app/Jobs/SubscriptionTrialEndsSoonJob.php @@ -0,0 +1,44 @@ +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; + } + } +} diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 4acd7fe8a..d69d95981 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -33,7 +33,7 @@ class Subscription extends Model } if (isStripe()) { if (!$this->stripe_plan_id) { - return 'unknown'; + return 'zero'; } $subscription = Subscription::where('id', $this->id)->first(); if (!$subscription) { @@ -54,6 +54,6 @@ class Subscription extends Model return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower(); } } - return 'unknown'; + return 'zero'; } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 485811a17..558c8dc3b 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -120,4 +120,20 @@ class Team extends Model implements SendsDiscord, SendsEmail { 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, + ]); + } + } } diff --git a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php new file mode 100644 index 000000000..591f8382d --- /dev/null +++ b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php @@ -0,0 +1,30 @@ +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'); + }); + } +}; diff --git a/resources/views/components/emails/footer.blade.php b/resources/views/components/emails/footer.blade.php index 44f47ef93..fba408cae 100644 --- a/resources/views/components/emails/footer.blade.php +++ b/resources/views/components/emails/footer.blade.php @@ -3,4 +3,4 @@ Thank you,
{{ 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)') }} diff --git a/resources/views/components/pricing-plans.blade.php b/resources/views/components/pricing-plans.blade.php index c231f1d21..ce8d81c20 100644 --- a/resources/views/components/pricing-plans.blade.php +++ b/resources/views/components/pricing-plans.blade.php @@ -21,6 +21,7 @@ +
30 days trial included on all plans, without credit card details.
Save 1 month annually with the yearly plans.
diff --git a/resources/views/emails/trial-ended.blade.php b/resources/views/emails/trial-ended.blade.php new file mode 100644 index 000000000..aeb00fa92 --- /dev/null +++ b/resources/views/emails/trial-ended.blade.php @@ -0,0 +1,7 @@ + + +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. + + diff --git a/resources/views/emails/trial-ends-soon.blade.php b/resources/views/emails/trial-ends-soon.blade.php new file mode 100644 index 000000000..2e6909016 --- /dev/null +++ b/resources/views/emails/trial-ends-soon.blade.php @@ -0,0 +1,7 @@ + + +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. + + diff --git a/routes/webhooks.php b/routes/webhooks.php index f05df9c5a..6a9de6403 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -1,6 +1,7 @@ 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 @@ Route::post('/payments/stripe/events', function () { '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 @@ Route::post('/payments/stripe/events', function () { } 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 @@ Route::post('/payments/stripe/events', function () { } 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 }