fix: subscriptions

This commit is contained in:
Andras Bacsai 2023-08-30 18:23:55 +02:00
parent 5b6667c461
commit 923af88336
20 changed files with 147 additions and 69 deletions

View File

@ -12,7 +12,7 @@ class ServerController extends Controller
public function new_server() public function new_server()
{ {
if (!is_cloud() || isInstanceAdmin()) { if (!is_cloud()) {
return view('server.create', [ return view('server.create', [
'limit_reached' => false, 'limit_reached' => false,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(), 'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),

View File

@ -48,7 +48,7 @@ class PricingPlans extends Component
'enabled' => true, 'enabled' => true,
], ],
'mode' => 'subscription', 'mode' => 'subscription',
'success_url' => route('subscription.success'), 'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]),
]; ];
$customer = currentTeam()->subscription?->stripe_customer_id ?? null; $customer = currentTeam()->subscription?->stripe_customer_id ?? null;

View File

@ -37,7 +37,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
private function cleanup_waitlist() private function cleanup_waitlist()
{ {
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get(); $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
foreach ($waitlist as $item) { foreach ($waitlist as $item) {
$item->delete(); $item->delete();
} }

View File

@ -9,7 +9,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stripe\Stripe;
class SubscriptionInvoiceFailedJob implements ShouldQueue class SubscriptionInvoiceFailedJob implements ShouldQueue
{ {

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model class Subscription extends Model
{ {
@ -14,6 +15,7 @@ class Subscription extends Model
} }
public function type() public function type()
{ {
if (isLemon()) {
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids')); $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
@ -28,6 +30,30 @@ class Subscription extends Model
if (in_array($subscription, $ultimate)) { if (in_array($subscription, $ultimate)) {
return 'ultimate'; return 'ultimate';
} }
}
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'unknown';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {
return null;
}
$subscriptionPlanId = data_get($subscription,'stripe_plan_id');
if (!$subscriptionPlanId) {
return null;
}
$subscriptionConfigs = collect(config('subscription'));
$stripePlanId = null;
$subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) {
if ($value === $subscriptionPlanId){
$stripePlanId = $key;
};
})->first();
if ($stripePlanId) {
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'unknown'; return 'unknown';
} }
} }

View File

@ -32,7 +32,7 @@ class User extends Authenticatable implements SendsEmail
$team = [ $team = [
'name' => $user->name . "'s Team", 'name' => $user->name . "'s Team",
'personal_team' => true, 'personal_team' => true,
'boarding' => true 'show_boarding' => true
]; ];
if ($user->id === 0) { if ($user->id === 0) {
$team['id'] = 0; $team['id'] = 0;
@ -94,7 +94,7 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam() public function currentTeam()
{ {
return session('currentTeam'); return Team::find(session('currentTeam')->id);
} }
public function otherTeams() public function otherTeams()

View File

@ -56,16 +56,16 @@ function isSubscriptionActive()
if (!$subscription) { if (!$subscription) {
return false; return false;
} }
if (config('subscription.provider') === 'lemon') { if (isLemon()) {
return $subscription->lemon_status === 'active'; return $subscription->lemon_status === 'active';
} }
if (config('subscription.provider') === 'stripe') { // if (isPaddle()) {
// return $subscription->paddle_status === 'active';
// }
if (isStripe()) {
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false; return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false;
} }
return false; return false;
// if (config('subscription.provider') === 'paddle') {
// return $subscription->paddle_status === 'active';
// }
} }
function isSubscriptionOnGracePeriod() function isSubscriptionOnGracePeriod()
@ -78,12 +78,12 @@ function isSubscriptionOnGracePeriod()
if (!$subscription) { if (!$subscription) {
return false; return false;
} }
if (config('subscription.provider') === 'lemon') { if (isLemon()) {
$is_still_grace_period = $subscription->lemon_ends_at && $is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period; return $is_still_grace_period;
} }
if (config('subscription.provider') === 'stripe') { if (isStripe()) {
return $subscription->stripe_cancel_at_period_end; return $subscription->stripe_cancel_at_period_end;
} }
return false; return false;
@ -92,6 +92,15 @@ function subscriptionProvider()
{ {
return config('subscription.provider'); return config('subscription.provider');
} }
function isLemon () {
return config('subscription.provider') === 'lemon';
}
function isStripe() {
return config('subscription.provider') === 'stripe';
}
function isPaddle() {
return config('subscription.provider') === 'paddle';
}
function getStripeCustomerPortalSession(Team $team) function getStripeCustomerPortalSession(Team $team)
{ {
Stripe::setApiKey(config('subscription.stripe_api_key')); Stripe::setApiKey(config('subscription.stripe_api_key'));
@ -124,5 +133,6 @@ function allowedPathsForBoardingAccounts()
...allowedPathsForUnsubscribedAccounts(), ...allowedPathsForUnsubscribedAccounts(),
'boarding', 'boarding',
'livewire/message/boarding', 'livewire/message/boarding',
'livewire/message/boarding.index',
]; ];
} }

View File

@ -1,7 +1,7 @@
<?php <?php
return [ return [
'waitlist' => [ 'waitlist' => [
'confirmation_valid_for_minutes' => 10, 'expiration' => 10,
], ],
'invitation' => [ 'invitation' => [
'link' => [ 'link' => [
@ -15,5 +15,10 @@ return [
'pro' => 10, 'pro' => 10,
'ultimate' => 25, 'ultimate' => 25,
], ],
'smtp' => [
'basic' => false,
'pro' => true,
'ultimate' => true,
],
], ],
]; ];

View File

@ -0,0 +1,29 @@
<?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->string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_plan_id');
});
}
};

View File

@ -12,7 +12,9 @@
@if ($attributes->get('type') === 'submit') @if ($attributes->get('type') === 'submit')
<span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span> <span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span>
@else @else
<span wire:target="{{ explode('(', $attributes->whereStartsWith('wire:click')->first())[0] }}" wire:loading.delay @if ($attributes->has('wire:click'))
<span wire:target="{{ $attributes->get('wire:click') }}" wire:loading.delay
class="loading loading-xs loading-spinner"></span> class="loading loading-xs loading-spinner"></span>
@endif @endif
@endif
</button> </button>

View File

@ -4,20 +4,10 @@
<ol class="inline-flex items-center"> <ol class="inline-flex items-center">
<li> <li>
<div class="flex items-center"> <div class="flex items-center">
<span>Currently active team: {{ session('currentTeam.name') }}</span> <span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div> </div>
</li> </li>
@if (session('currentTeam.description'))
<li class="inline-flex items-center">
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<span class="truncate">{{ Str::limit(session('currentTeam.description'), 52) }}</span>
</li>
@endif
</ol> </ol>
</nav> </nav>
<nav class="navbar-main"> <nav class="navbar-main">

View File

@ -1,4 +1,4 @@
Someone added this email to the Coolify Cloud's waitlist. Someone added this email to the Coolify Cloud's waitlist.
<br> <br>
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.confirmation_valid_for_minutes')}} minutes.<br><br> <a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.expiration')}} minutes.<br><br>
You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist. You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist.

View File

@ -4,6 +4,16 @@
@endif @endif
<h1>Dashboard</h1> <h1>Dashboard</h1>
<div class="subtitle">Something <x-highlighted text="(more)" /> useful will be here.</div> <div class="subtitle">Something <x-highlighted text="(more)" /> useful will be here.</div>
@if (request()->query->get('success'))
<div class="rounded alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<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" />
</svg>
<span>Your subscription has been activated! Welcome onboard!</span>
</div>
@endif
<div class="w-full rounded stats stats-vertical lg:stats-horizontal"> <div class="w-full rounded stats stats-vertical lg:stats-horizontal">
<div class="stat"> <div class="stat">
<div class="stat-title">Servers</div> <div class="stat-title">Servers</div>
@ -25,4 +35,5 @@
<div class="stat-value">{{ $s3s }}</div> <div class="stat-value">{{ $s3s }}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,9 @@
<div> <div>
@if (subscriptionProvider() === 'stripe') @if (subscriptionProvider() === 'stripe')
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button>
<div class="pt-4">
<div>Current Plan: <span class="text-warning">{{ data_get(currentTeam(), 'subscription')->type() }}<span>
</div>
@if (currentTeam()->subscription->stripe_cancel_at_period_end) @if (currentTeam()->subscription->stripe_cancel_at_period_end)
<div>Subscription is active but on cancel period.</div> <div>Subscription is active but on cancel period.</div>
@else @else
@ -11,7 +15,9 @@
<a class="hover:no-underline" href="{{ route('subscription.index') }}"><x-forms.button>Subscribe <a class="hover:no-underline" href="{{ route('subscription.index') }}"><x-forms.button>Subscribe
again</x-forms.button></a> again</x-forms.button></a>
@endif @endif
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button> <div>To update your subscription (upgrade / downgrade), please <a class="text-white underline"
href="https://docs.coollabs.io/contact" target="_blank">contact us.</a></div>
</div>
@endif @endif
@if (subscriptionProvider() === 'lemon') @if (subscriptionProvider() === 'lemon')
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div> <div>Status: {{ currentTeam()->subscription->lemon_status }}</div>

View File

@ -13,7 +13,7 @@
<div>You can't delete your last team.</div> <div>You can't delete your last team.</div>
@elseif(currentTeam()->subscription && @elseif(currentTeam()->subscription &&
currentTeam()->subscription?->lemon_status !== 'cancelled') currentTeam()->subscription?->lemon_status !== 'cancelled')
<div>Please cancel your subscription before delete this team (Manage My Subscription button).</div> <div>Please cancel your subscription before delete this team (Manage My Subscription).</div>
@else @else
@if (currentTeam()->isEmpty()) @if (currentTeam()->isEmpty())
<div class="pb-4">This will delete your team. Beware! There is no coming back!</div> <div class="pb-4">This will delete your team. Beware! There is no coming back!</div>

View File

@ -1,3 +0,0 @@
<x-layout-subscription>
Cancel
</x-layout-subscription>

View File

@ -11,8 +11,12 @@
class="text-warning">{{ session('currentTeam.name') }}</span></span> class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div> </div>
@if (request()->query->get('cancelled')) @if (request()->query->get('cancelled'))
<div class="rounded alert alert-error"> <div class="mb-6 rounded alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact support.</span> <span>Something went wrong with your subscription. Please try again or contact support.</span>
</div> </div>
@endif @endif

View File

@ -1,3 +0,0 @@
<x-layout>
Success
</x-layout>

View File

@ -99,8 +99,6 @@ Route::middleware(['auth'])->group(function () {
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset'); Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
}); });
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.index'); Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.index');
Route::get('/subscription/success', fn () => view('subscription.success'))->name('subscription.success');
Route::get('/subscription/cancel', fn () => view('profile'))->name('subscription.cancel');
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration'); Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license'); Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile'); Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');

View File

@ -181,7 +181,7 @@ Route::get('/waitlist/confirm', function () {
ray($email, $confirmation_code); ray($email, $confirmation_code);
try { try {
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); $found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
if ($found && !$found->verified && $found->created_at > now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes'))) { if ($found && !$found->verified && $found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) {
$found->verified = true; $found->verified = true;
$found->save(); $found->save();
send_internal_notification('Waitlist confirmed: ' . $email); send_internal_notification('Waitlist confirmed: ' . $email);
@ -267,9 +267,11 @@ Route::post('/payments/stripe/events', function () {
break; break;
case 'customer.subscription.updated': case 'customer.subscription.updated':
$subscriptionId = data_get($data, 'items.data.0.subscription'); $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'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
$subscription->update([ $subscription->update([
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]); ]);
break; break;
@ -277,6 +279,8 @@ Route::post('/payments/stripe/events', function () {
$subscriptionId = data_get($data, 'items.data.0.subscription'); $subscriptionId = data_get($data, 'items.data.0.subscription');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
$subscription->update([ $subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id'=> null,
'stripe_cancel_at_period_end' => false, 'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
]); ]);