diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index d78e67505..a194c3ad9 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -30,7 +30,7 @@ public function subscription() if (!is_cloud()) { abort(404); } - return view('subscription', [ + return view('subscription.show', [ 'settings' => InstanceSettings::get(), ]); } diff --git a/app/Http/Livewire/Subscription/Actions.php b/app/Http/Livewire/Subscription/Actions.php index 7830a935b..7de0ec728 100644 --- a/app/Http/Livewire/Subscription/Actions.php +++ b/app/Http/Livewire/Subscription/Actions.php @@ -69,4 +69,8 @@ public function resume() return general_error_handler($e, $this); } } + public function stripeCustomerPortal() { + $session = getStripeCustomerPortalSession(currentTeam()); + redirect($session->url); + } } diff --git a/app/Http/Livewire/Subscription/PricingPlans.php b/app/Http/Livewire/Subscription/PricingPlans.php new file mode 100644 index 000000000..bfdf48683 --- /dev/null +++ b/app/Http/Livewire/Subscription/PricingPlans.php @@ -0,0 +1,66 @@ +emit('error', 'Price ID not found! Please contact the administrator.'); + return; + } + $payload = [ + 'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id, + 'line_items' => [[ + 'price' => $priceId, + 'quantity' => 1, + ]], + 'customer_update' =>[ + 'name' => 'auto' + ], + 'tax_id_collection' => [ + 'enabled' => true, + ], + 'mode' => 'subscription', + 'success_url' => route('subscription.success'), + 'cancel_url' => route('subscription.show',['cancelled' => true]), + ]; + $customer = currentTeam()->subscription?->stripe_customer_id ?? null; + if ($customer) { + $payload['customer'] = $customer; + } else { + $payload['customer_email'] = auth()->user()->email; + } + $session = Session::create($payload); + return redirect($session->url, 303); + } +} diff --git a/app/Http/Middleware/IsBoardingFlow.php b/app/Http/Middleware/IsBoardingFlow.php index e13e71f31..74a5bba97 100644 --- a/app/Http/Middleware/IsBoardingFlow.php +++ b/app/Http/Middleware/IsBoardingFlow.php @@ -15,12 +15,8 @@ class IsBoardingFlow */ public function handle(Request $request, Closure $next): Response { - $allowed_paths = [ - 'subscription', - 'boarding', - 'livewire/message/boarding' - ]; - if (showBoarding() && !in_array($request->path(), $allowed_paths)) { + ray('IsBoardingFlow Middleware'); + if (showBoarding() && !in_array($request->path(), allowedPaths())) { return redirect('boarding'); } return $next($request); diff --git a/app/Http/Middleware/SubscriptionValid.php b/app/Http/Middleware/SubscriptionValid.php index 01bd7a0e5..d6d98a8cc 100644 --- a/app/Http/Middleware/SubscriptionValid.php +++ b/app/Http/Middleware/SubscriptionValid.php @@ -17,31 +17,17 @@ public function handle(Request $request, Closure $next): Response return $next($request); } } - if (isInstanceAdmin()) { - return $next($request); - } - - if (is_subscription_active() && $request->path() === 'subscription') { + if (isSubscriptionActive() && $request->path() === 'subscription') { + // ray('active subscription Middleware'); return redirect('/'); } - if (is_subscription_in_grace_period()) { + if (isSubscriptionOnGracePeriod()) { + // ray('is_subscription_in_grace_period Middleware'); return $next($request); } - if (!is_subscription_active() && !is_subscription_in_grace_period()) { - ray('SubscriptionValid Middleware'); - - $allowed_paths = [ - 'subscription', - 'login', - 'register', - 'waitlist', - 'force-password-reset', - 'logout', - 'livewire/message/force-password-reset', - 'livewire/message/check-license', - 'livewire/message/switch-team', - ]; - if (!in_array($request->path(), $allowed_paths)) { + if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) { + // ray('SubscriptionValid Middleware'); + if (!in_array($request->path(), allowedPaths())) { return redirect('subscription'); } else { return $next($request); diff --git a/app/Jobs/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index f14214733..96211451b 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -22,6 +22,7 @@ public function handle(): void try { resolve(CheckResaleLicense::class)(); } catch (\Throwable $th) { + send_internal_notification('CheckResaleLicenseJob failed with: ' . $th->getMessage()); ray($th); } } diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index e3dd9640f..176841b20 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -28,7 +28,6 @@ public function __construct( */ public function handle(): void { - $remote_process = resolve(RunRemoteProcess::class, [ 'activity' => $this->activity, 'ignore_errors' => $this->ignore_errors, diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index eed6c5de8..44e5601ba 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -64,35 +64,42 @@ public function uniqueId(): int public function handle(): void { - if ($this->database_status !== 'running') { - ray('database not running'); - return; - } - $this->container_name = $this->database->uuid; - $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; + try { + if ($this->database_status !== 'running') { + ray('database not running'); + return; + } + $this->container_name = $this->database->uuid; + $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; - if ($this->database->name === 'coolify-db') { - $this->container_name = "coolify-db"; - $ip = Str::slug($this->server->ip); - $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; - } - $this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql"; - $this->backup_location = $this->backup_dir . $this->backup_file; + if ($this->database->name === 'coolify-db') { + $this->container_name = "coolify-db"; + $ip = Str::slug($this->server->ip); + $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; + } + $this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql"; + $this->backup_location = $this->backup_dir . $this->backup_file; - $this->backup_log = ScheduledDatabaseBackupExecution::create([ - 'filename' => $this->backup_location, - 'scheduled_database_backup_id' => $this->backup->id, - ]); - if ($this->database_type === 'standalone-postgresql') { - $this->backup_standalone_postgresql(); + $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'filename' => $this->backup_location, + 'scheduled_database_backup_id' => $this->backup->id, + ]); + if ($this->database_type === 'standalone-postgresql') { + $this->backup_standalone_postgresql(); + } + $this->calculate_size(); + $this->remove_old_backups(); + if ($this->backup->save_s3) { + $this->upload_to_s3(); + } + $this->save_backup_logs(); + // TODO: Notify user + } catch (\Throwable $th) { + ray($th->getMessage()); + send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage()); + //throw $th; } - $this->calculate_size(); - $this->remove_old_backups(); - if ($this->backup->save_s3) { - $this->upload_to_s3(); - } - $this->save_backup_logs(); - // TODO: Notify user + } private function backup_standalone_postgresql(): void diff --git a/app/Jobs/DatabaseContainerStatusJob.php b/app/Jobs/DatabaseContainerStatusJob.php index f2fc7053c..3993367fc 100644 --- a/app/Jobs/DatabaseContainerStatusJob.php +++ b/app/Jobs/DatabaseContainerStatusJob.php @@ -46,6 +46,7 @@ public function handle(): void $this->database->save(); } } catch (\Exception $e) { + send_internal_notification('DatabaseContainerStatusJob failed with: ' . $e->getMessage()); ray($e->getMessage()); } } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 52279c241..821688754 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -49,6 +49,7 @@ public function handle(): void } } } catch (\Exception $e) { + send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); ray($e->getMessage()); } } diff --git a/app/Jobs/ProxyCheckJob.php b/app/Jobs/ProxyCheckJob.php index bcaf8c8bd..6316c204a 100755 --- a/app/Jobs/ProxyCheckJob.php +++ b/app/Jobs/ProxyCheckJob.php @@ -33,6 +33,7 @@ public function handle() } } catch (\Throwable $th) { ray($th->getMessage()); + send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage()); //throw $th; } } diff --git a/app/Jobs/ProxyContainerStatusJob.php b/app/Jobs/ProxyContainerStatusJob.php index 3f8c21a3e..ddd6e5b8d 100644 --- a/app/Jobs/ProxyContainerStatusJob.php +++ b/app/Jobs/ProxyContainerStatusJob.php @@ -57,6 +57,7 @@ public function handle(): void $this->server->proxy->status = 'exited'; $this->server->save(); } + send_internal_notification('ProxyContainerStatusJob failed with: ' . $e->getMessage()); } } } diff --git a/app/Jobs/ProxyStartJob.php b/app/Jobs/ProxyStartJob.php index 90811d836..7f1df6377 100755 --- a/app/Jobs/ProxyStartJob.php +++ b/app/Jobs/ProxyStartJob.php @@ -29,8 +29,8 @@ public function handle() } resolve(StartProxy::class)($this->server); } catch (\Throwable $th) { + send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage()); ray($th->getMessage()); - //throw $th; } } } diff --git a/app/Jobs/ResourceStatusJob.php b/app/Jobs/ResourceStatusJob.php index 122423f0a..4ff4063d4 100644 --- a/app/Jobs/ResourceStatusJob.php +++ b/app/Jobs/ResourceStatusJob.php @@ -37,8 +37,8 @@ public function handle(): void database: $postgresql, )); } - } catch (\Exception $e) { - ray($e->getMessage()); + } catch (\Exception $th) { + send_internal_notification('ResourceStatusJob failed with: ' . $th->getMessage()); } } } diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php new file mode 100755 index 000000000..3d0e1bb8d --- /dev/null +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -0,0 +1,41 @@ +team); + $mail = new MailMessage(); + $mail->view('emails.subscription-invoice-failed', [ + 'stripeCustomerPortal' => $session->url, + ]); + $mail->subject('Your last payment was failed for Coolify Cloud.'); + $this->team->members()->each(function ($member) use ($mail) { + ray($member); + if ($member->isAdmin()) { + send_user_an_email($mail, $member->email); + } + }); + } catch (\Throwable $th) { + send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $th->getMessage()); + } + } +} diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php index 308f17370..e259d16c1 100644 --- a/app/Models/Webhook.php +++ b/app/Models/Webhook.php @@ -8,6 +8,7 @@ class Webhook extends Model { protected $guarded = []; protected $casts = [ + 'type' => 'string', 'payload' => 'encrypted', ]; } diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index b514fb5af..fa16a1997 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -1,6 +1,8 @@ subscription->lemon_renews_at)->format('Y-M-d H:i:s'); } -function is_subscription_active() +function isSubscriptionActive() { $team = currentTeam(); - if (!$team) { return false; } - if (isInstanceAdmin()) { - return true; - } $subscription = $team?->subscription; if (!$subscription) { return false; } - $is_active = $subscription->lemon_status === 'active'; + if (config('subscription.provider') === 'lemon') { + return $subscription->lemon_status === 'active'; + } + if (config('subscription.provider') === 'stripe') { + return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false; + } + return false; + // if (config('subscription.provider') === 'paddle') { + // return $subscription->paddle_status === 'active'; + // } - return $is_active; } -function is_subscription_in_grace_period() +function isSubscriptionOnGracePeriod() { + $team = currentTeam(); if (!$team) { return false; } - if (isInstanceAdmin()) { - return true; - } $subscription = $team?->subscription; if (!$subscription) { return false; } - $is_still_grace_period = $subscription->lemon_ends_at && - Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); - - return $is_still_grace_period; + if (config('subscription.provider') === 'lemon') { + $is_still_grace_period = $subscription->lemon_ends_at && + Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); + return $is_still_grace_period; + } + if (config('subscription.provider') === 'stripe') { + return $subscription->stripe_cancel_at_period_end; + } + return false; +} +function subscriptionProvider() +{ + return config('subscription.provider'); +} +function getStripeCustomerPortalSession(Team $team) +{ + Stripe::setApiKey(config('subscription.stripe_api_key')); + $return_url = route('team.show'); + $stripe_customer_id = $team->subscription->stripe_customer_id; + $session = \Stripe\BillingPortal\Session::create([ + 'customer' => $stripe_customer_id, + 'return_url' => $return_url, + ]); + return $session; +} +function allowedPaths() +{ + return [ + 'subscription', + 'login', + 'register', + 'waitlist', + 'force-password-reset', + 'logout', + 'boarding', + 'livewire/message/boarding', + 'livewire/message/force-password-reset', + 'livewire/message/check-license', + 'livewire/message/switch-team', + 'livewire/message/subscription.pricing-plans' + ]; } diff --git a/composer.json b/composer.json index fd8a5eb01..bcb74242b 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "spatie/laravel-ray": "^1.32.4", "spatie/laravel-schemaless-attributes": "^2.4", "spatie/url": "^2.2", + "stripe/stripe-php": "^12.0", "symfony/yaml": "^6.2", "visus/cuid2": "^2.0.0" }, diff --git a/composer.lock b/composer.lock index cf0183f12..0dcd5cd72 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a4143cdb58c02a0490f9aa03d05b8e9a", + "content-hash": "da14dce99d76abcaaa6393166eda049a", "packages": [ { "name": "aws/aws-crt-php", @@ -6582,6 +6582,67 @@ ], "time": "2023-04-27T11:07:22+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v12.0.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "732996be0714154716f19f73f956d77bafc99334" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/732996be0714154716f19f73f956d77bafc99334", + "reference": "732996be0714154716f19f73f956d77bafc99334", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "php-coveralls/php-coveralls": "^2.5", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v12.0.0" + }, + "time": "2023-08-18T18:55:28+00:00" + }, { "name": "symfony/console", "version": "v6.3.2", diff --git a/config/subscription.php b/config/subscription.php index d4e94c7d8..de2f6d568 100644 --- a/config/subscription.php +++ b/config/subscription.php @@ -1,6 +1,33 @@ env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon + // Stripe + 'stripe_api_key' => env('STRIPE_API_KEY', null), + 'stripe_secret' => env('STRIPE_SECRET', null), + 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null), + 'stripe_price_id_basic_monthly' => env('STRIPE_PRICE_ID_BASIC_MONTHLY', null), + 'stripe_price_id_basic_yearly' => env('STRIPE_PRICE_ID_BASIC_YEARLY', null), + 'stripe_price_id_pro_monthly' => env('STRIPE_PRICE_ID_PRO_MONTHLY', null), + 'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null), + 'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null), + 'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null), + + + // Paddle + 'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null), + 'paddle_vendor_auth_code' => env('PADDLE_VENDOR_AUTH_CODE', null), + 'paddle_public_key' => env('PADDLE_PUBLIC_KEY', null), + 'paddle_price_id_basic_monthly' => env('PADDLE_PRICE_ID_BASIC_MONTHLY', null), + 'paddle_price_id_basic_yearly' => env('PADDLE_PRICE_ID_BASIC_YEARLY', null), + 'paddle_price_id_pro_monthly' => env('PADDLE_PRICE_ID_PRO_MONTHLY', null), + 'paddle_price_id_pro_yearly' => env('PADDLE_PRICE_ID_PRO_YEARLY', null), + 'paddle_price_id_ultimate_monthly' => env('PADDLE_PRICE_ID_ULTIMATE_MONTHLY', null), + 'paddle_price_id_ultimate_yearly' => env('PADDLE_PRICE_ID_ULTIMATE_YEARLY', null), + 'paddle_webhook_secret' => env('PADDLE_WEBHOOK_SECRET', null), + + + // Lemon 'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null), 'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null), 'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null), diff --git a/database/migrations/2023_08_22_071049_update_webhooks_type.php b/database/migrations/2023_08_22_071049_update_webhooks_type.php new file mode 100644 index 000000000..7f60ca973 --- /dev/null +++ b/database/migrations/2023_08_22_071049_update_webhooks_type.php @@ -0,0 +1,29 @@ +string('type')->change(); + }); + DB::statement("ALTER TABLE webhooks DROP CONSTRAINT webhooks_type_check"); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('webhooks', function (Blueprint $table) { + $table->string('type')->change(); + }); + } +}; diff --git a/database/migrations/2023_08_22_071050_update_subscriptions_stripe.php b/database/migrations/2023_08_22_071050_update_subscriptions_stripe.php new file mode 100644 index 000000000..70462fe20 --- /dev/null +++ b/database/migrations/2023_08_22_071050_update_subscriptions_stripe.php @@ -0,0 +1,52 @@ +boolean('stripe_invoice_paid')->default(false); + $table->string('stripe_subscription_id')->nullable(); + $table->string('stripe_customer_id')->nullable(); + $table->boolean('stripe_cancel_at_period_end')->default(false); + $table->string('lemon_subscription_id')->nullable()->change(); + $table->string('lemon_order_id')->nullable()->change(); + $table->string('lemon_product_id')->nullable()->change(); + $table->string('lemon_variant_id')->nullable()->change(); + $table->string('lemon_variant_name')->nullable()->change(); + $table->string('lemon_customer_id')->nullable()->change(); + $table->string('lemon_status')->nullable()->change(); + $table->string('lemon_renews_at')->nullable()->change(); + $table->string('lemon_update_payment_menthod_url')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('stripe_invoice_paid'); + $table->dropColumn('stripe_subscription_id'); + $table->dropColumn('stripe_customer_id'); + $table->dropColumn('stripe_cancel_at_period_end'); + $table->string('lemon_subscription_id')->change(); + $table->string('lemon_order_id')->change(); + $table->string('lemon_product_id')->change(); + $table->string('lemon_variant_id')->change(); + $table->string('lemon_variant_name')->change(); + $table->string('lemon_customer_id')->change(); + $table->string('lemon_status')->change(); + $table->string('lemon_renews_at')->change(); + $table->string('lemon_update_payment_menthod_url')->change(); + }); + } +}; diff --git a/resources/views/components/layout-subscription.blade.php b/resources/views/components/layout-subscription.blade.php index d450c22bd..a3b0bf806 100644 --- a/resources/views/components/layout-subscription.blade.php +++ b/resources/views/components/layout-subscription.blade.php @@ -9,7 +9,7 @@ @env('local')
Start self-hosting in the cloud @@ -168,10 +167,9 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap billed annually @if ($showSubscribeButtons) - Subscribe - Subscribe + @isset($pro) + {{ $pro }} + @endisset @endif
Scale your business or self-hosting environment.
@@ -227,10 +225,9 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap billed annually @if ($showSubscribeButtons) - Subscribe - Subscribe + @isset($ultimate) + {{ $ultimate }} + @endisset @endifDeploy complex infrastuctures and manage them easily in one place.
@@ -274,3 +271,6 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap +@isset($other) + {{ $other }} +@endisset diff --git a/resources/views/components/stripe.blade.php b/resources/views/components/stripe.blade.php new file mode 100644 index 000000000..e69de29bb diff --git a/resources/views/emails/subscription-invoice-failed.blade.php b/resources/views/emails/subscription-invoice-failed.blade.php new file mode 100644 index 000000000..9d04eebd4 --- /dev/null +++ b/resources/views/emails/subscription-invoice-failed.blade.php @@ -0,0 +1,4 @@ +Your last invoice has failed to be paid for Coolify Cloud. Please update payment details on your Stripe Customer Portal. +