add stripe subscription
This commit is contained in:
parent
2d8f166e4a
commit
39890b319a
@ -30,7 +30,7 @@ public function subscription()
|
||||
if (!is_cloud()) {
|
||||
abort(404);
|
||||
}
|
||||
return view('subscription', [
|
||||
return view('subscription.show', [
|
||||
'settings' => InstanceSettings::get(),
|
||||
]);
|
||||
}
|
||||
|
@ -69,4 +69,8 @@ public function resume()
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function stripeCustomerPortal() {
|
||||
$session = getStripeCustomerPortalSession(currentTeam());
|
||||
redirect($session->url);
|
||||
}
|
||||
}
|
||||
|
66
app/Http/Livewire/Subscription/PricingPlans.php
Normal file
66
app/Http/Livewire/Subscription/PricingPlans.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Subscription;
|
||||
|
||||
use Livewire\Component;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Checkout\Session;
|
||||
|
||||
class PricingPlans extends Component
|
||||
{
|
||||
public function subscribeStripe($type)
|
||||
{
|
||||
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||
switch ($type) {
|
||||
case 'basic-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_basic_monthly');
|
||||
break;
|
||||
case 'basic-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_basic_yearly');
|
||||
break;
|
||||
case 'ultimate-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
|
||||
break;
|
||||
case 'pro-monthly':
|
||||
$priceId = config('subscription.stripe_price_id_pro_monthly');
|
||||
break;
|
||||
case 'pro-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_pro_yearly');
|
||||
break;
|
||||
case 'ultimate-yearly':
|
||||
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
|
||||
break;
|
||||
default:
|
||||
$priceId = config('subscription.stripe_price_id_basic_monthly');
|
||||
break;
|
||||
}
|
||||
if (!$priceId) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ public function __construct(
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
|
||||
$remote_process = resolve(RunRemoteProcess::class, [
|
||||
'activity' => $this->activity,
|
||||
'ignore_errors' => $this->ignore_errors,
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ public function handle(): void
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ public function handle()
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
ray($th->getMessage());
|
||||
send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage());
|
||||
//throw $th;
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ public function handle(): void
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
}
|
||||
send_internal_notification('ProxyContainerStatusJob failed with: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
app/Jobs/SubscriptionInvoiceFailedJob.php
Executable file
41
app/Jobs/SubscriptionInvoiceFailedJob.php
Executable file
@ -0,0 +1,41 @@
|
||||
<?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;
|
||||
use Stripe\Stripe;
|
||||
|
||||
class SubscriptionInvoiceFailedJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(protected Team $team)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$session = getStripeCustomerPortalSession($this->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());
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ class Webhook extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'type' => 'string',
|
||||
'payload' => 'encrypted',
|
||||
];
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Stripe\Stripe;
|
||||
|
||||
function getSubscriptionLink($type)
|
||||
{
|
||||
@ -43,40 +45,79 @@ function getEndDate()
|
||||
return Carbon::parse(currentTeam()->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'
|
||||
];
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
63
composer.lock
generated
63
composer.lock
generated
@ -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",
|
||||
|
@ -1,6 +1,33 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'provider'=> 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),
|
||||
|
@ -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('webhooks', function (Blueprint $table) {
|
||||
$table->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();
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
<?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_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();
|
||||
});
|
||||
}
|
||||
};
|
@ -9,7 +9,7 @@
|
||||
@env('local')
|
||||
<title>Coolify - localhost</title>
|
||||
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
||||
@else
|
||||
@else
|
||||
<title>{{ $title ?? 'Coolify' }}</title>
|
||||
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
|
||||
@endenv
|
||||
@ -26,7 +26,7 @@
|
||||
<body>
|
||||
@livewireScripts
|
||||
<x-toaster-hub />
|
||||
@if (isInstanceAdmin() || is_subscription_in_grace_period())
|
||||
@if (isSubscriptionOnGracePeriod())
|
||||
<div class="fixed top-3 left-4" id="vue">
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
@ -68,6 +68,18 @@ function changePasswordFieldType(event) {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
Livewire.on('info', (message) => {
|
||||
if (message) Toaster.info(message)
|
||||
})
|
||||
Livewire.on('error', (message) => {
|
||||
if (message) Toaster.error(message)
|
||||
})
|
||||
Livewire.on('warning', (message) => {
|
||||
if (message) Toaster.warning(message)
|
||||
})
|
||||
Livewire.on('success', (message) => {
|
||||
if (message) Toaster.success(message)
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
80
resources/views/components/paddle.blade.php
Normal file
80
resources/views/components/paddle.blade.php
Normal file
@ -0,0 +1,80 @@
|
||||
<x-slot:basic>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="w-full h-10 buyme"
|
||||
x-on:click="subscribe('basic-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="w-full h-10 buyme"
|
||||
x-on:click="subscribe('basic-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:basic>
|
||||
<x-slot:pro>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||
x-on:click="subscribe('pro-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||
x-on:click="subscribe('pro-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:pro>
|
||||
<x-slot:ultimate>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
|
||||
x-on:click="subscribe('ultimate-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="w-full h-10 buyme"
|
||||
x-on:click="subscribe('ultimate-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:ultimate>
|
||||
<x-slot:other>
|
||||
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||
<script type="text/javascript">
|
||||
Paddle.Environment.set("{{ is_dev() ? 'sandbox' : 'production' }}");
|
||||
Paddle.Setup({
|
||||
seller: {{ config('subscription.paddle_vendor_id') }},
|
||||
checkout: {
|
||||
settings: {
|
||||
displayMode: "overlay",
|
||||
theme: "light",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function subscribe(type) {
|
||||
let priceId = null
|
||||
switch (type) {
|
||||
case 'basic-monthly':
|
||||
priceId = "{{ config('subscription.paddle_price_id_basic_monthly') }}"
|
||||
break;
|
||||
case 'basic-yearly':
|
||||
priceId = "{{ config('subscription.paddle_price_id_basic_yearly') }}"
|
||||
break;
|
||||
case 'pro-monthly':
|
||||
priceId = "{{ config('subscription.paddle_price_id_pro_monthly') }}"
|
||||
break;
|
||||
case 'pro-yearly':
|
||||
priceId = "{{ config('subscription.paddle_price_id_pro_yearly') }}"
|
||||
break;
|
||||
case 'ultimate-monthly':
|
||||
priceId = "{{ config('subscription.paddle_price_id_ultimate_monthly') }}"
|
||||
break;
|
||||
case 'ultimate-yearly':
|
||||
priceId = "{{ config('subscription.paddle_price_id_ultimate_yearly') }}"
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
Paddle.Checkout.open({
|
||||
customer: {
|
||||
email: '{{ auth()->user()->email }}',
|
||||
},
|
||||
customData: {
|
||||
"team_id": "{{ currentTeam()->id }}",
|
||||
},
|
||||
items: [{
|
||||
priceId,
|
||||
quantity: 1
|
||||
}],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</x-slot:other>
|
@ -105,10 +105,9 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
|
||||
<span>billed annually</span>
|
||||
</span>
|
||||
@if ($showSubscribeButtons)
|
||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
||||
href="{{ getSubscriptionLink('monthly_basic') }}">Subscribe</a>
|
||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
||||
href="{{ getSubscriptionLink('yearly_basic') }}">Subscribe</a>
|
||||
@isset($basic)
|
||||
{{ $basic }}
|
||||
@endisset
|
||||
@endif
|
||||
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">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
|
||||
<span>billed annually</span>
|
||||
</span>
|
||||
@if ($showSubscribeButtons)
|
||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro" class="buyme"
|
||||
href="{{ getSubscriptionLink('monthly_pro') }}">Subscribe</a>
|
||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="buyme"
|
||||
href="{{ getSubscriptionLink('yearly_pro') }}">Subscribe</a>
|
||||
@isset($pro)
|
||||
{{ $pro }}
|
||||
@endisset
|
||||
@endif
|
||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
|
||||
</p>
|
||||
@ -227,10 +225,9 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
|
||||
<span>billed annually</span>
|
||||
</span>
|
||||
@if ($showSubscribeButtons)
|
||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="buyme"
|
||||
href="{{ getSubscriptionLink('monthly_ultimate') }}">Subscribe</a>
|
||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="buyme"
|
||||
href="{{ getSubscriptionLink('yearly_ultimate') }}">Subscribe</a>
|
||||
@isset($ultimate)
|
||||
{{ $ultimate }}
|
||||
@endisset
|
||||
@endif
|
||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and
|
||||
manage them easily in one place.</p>
|
||||
@ -274,3 +271,6 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-coolgray-500 isolate gap
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@isset($other)
|
||||
{{ $other }}
|
||||
@endisset
|
||||
|
0
resources/views/components/stripe.blade.php
Normal file
0
resources/views/components/stripe.blade.php
Normal file
@ -0,0 +1,4 @@
|
||||
Your last invoice has failed to be paid for Coolify Cloud. Please <a href="{{$stripeCustomerPortal}}">update payment details on your Stripe Customer Portal</a>.
|
||||
<br><br>
|
||||
Thanks,<br>
|
||||
Coolify Cloud
|
@ -1,4 +1,5 @@
|
||||
Congratulations!<br>
|
||||
Congratulations!<br>
|
||||
<br>
|
||||
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a>
|
||||
<br>
|
||||
|
@ -1,31 +1,48 @@
|
||||
<div>
|
||||
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div>
|
||||
<div>Type: {{ currentTeam()->subscription->lemon_variant_name }}</div>
|
||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
|
||||
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
|
||||
class="text-white underline" href="https://docs.coollabs.io/contact" target="_blank">please
|
||||
contact
|
||||
us.</a></div>
|
||||
@else
|
||||
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
|
||||
@if (subscriptionProvider() === 'stripe')
|
||||
@if (currentTeam()->subscription->stripe_cancel_at_period_end)
|
||||
<div>Subscription is active but on cancel period.</div>
|
||||
@else
|
||||
<div>Subscription is active. Last invoice is
|
||||
{{ currentTeam()->subscription->stripe_invoice_paid ? 'paid' : 'not paid' }}.</div>
|
||||
@endif
|
||||
|
||||
@if (currentTeam()->subscription->stripe_cancel_at_period_end)
|
||||
<a class="hover:no-underline" href="{{ route('subscription') }}"><x-forms.button>Subscribe
|
||||
again</x-forms.button></a>
|
||||
@endif
|
||||
<x-forms.button wire:click='stripeCustomerPortal'>Manage My Subscription</x-forms.button>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<x-forms.button class="bg-coollabs-gradient" wire:click='resume'>Resume Subscription
|
||||
@if (subscriptionProvider() === 'lemon')
|
||||
<div>Status: {{ currentTeam()->subscription->lemon_status }}</div>
|
||||
<div>Type: {{ currentTeam()->subscription->lemon_variant_name }}</div>
|
||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
|
||||
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
|
||||
class="text-white underline" href="https://docs.coollabs.io/contact" target="_blank">please
|
||||
contact
|
||||
us.</a></div>
|
||||
@else
|
||||
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
@if (currentTeam()->subscription->lemon_status === 'cancelled')
|
||||
<x-forms.button class="bg-coollabs-gradient" wire:click='resume'>Resume Subscription
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button wire:click='cancel'>Cancel Subscription</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.button><a class="text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment
|
||||
Details</a>
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button wire:click='cancel'>Cancel Subscription</x-forms.button>
|
||||
@endif
|
||||
<a class="text-white hover:no-underline"
|
||||
href="https://app.lemonsqueezy.com/my-orders"><x-forms.button>Manage My
|
||||
Subscription</x-forms.button></a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.button><a class="text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment
|
||||
Details</a>
|
||||
</x-forms.button>
|
||||
<a class="text-white hover:no-underline"
|
||||
href="https://app.lemonsqueezy.com/my-orders"><x-forms.button>Manage My
|
||||
Subscription</x-forms.button></a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
@ -0,0 +1,63 @@
|
||||
<x-pricing-plans>
|
||||
@if (config('subscription.provider') === 'stripe')
|
||||
<x-slot:basic>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
|
||||
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
|
||||
class="w-full h-10 buyme" wire:click="subscribeStripe('basic-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:basic>
|
||||
<x-slot:pro>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro"
|
||||
class="w-full h-10 buyme" wire:click="subscribeStripe('pro-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||
wire:click="subscribeStripe('pro-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:pro>
|
||||
<x-slot:ultimate>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate"
|
||||
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate"
|
||||
class="w-full h-10 buyme" wire:click="subscribeStripe('ultimate-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:ultimate>
|
||||
@endif
|
||||
@if (config('subscription.provider') === 'paddle')
|
||||
<x-paddle />
|
||||
@endif
|
||||
@if (config('subscription.provider') === 'lemon')
|
||||
<x-slot:basic>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic"
|
||||
class="w-full h-10 buyme" wire:click="getSubscriptionLink('basic-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic"
|
||||
class="w-full h-10 buyme" wire:click="getSubscriptionLink('basic-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:basic>
|
||||
<x-slot:pro>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro"
|
||||
class="w-full h-10 buyme" wire:click="getSubscriptionLink('pro-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="w-full h-10 buyme"
|
||||
wire:click="getSubscriptionLink('pro-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:pro>
|
||||
<x-slot:ultimate>
|
||||
<x-forms.button x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate"
|
||||
class="w-full h-10 buyme" wire:click="getSubscriptionLink('ultimate-monthly')"> Subscribe
|
||||
</x-forms.button>
|
||||
|
||||
<x-forms.button x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate"
|
||||
class="w-full h-10 buyme" wire:click="getSubscriptionLink('ultimate-yearly')"> Subscribe
|
||||
</x-forms.button>
|
||||
</x-slot:ultimate>
|
||||
@endif
|
||||
</x-pricing-plans>
|
3
resources/views/subscription/cancel.blade.php
Normal file
3
resources/views/subscription/cancel.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<x-layout-subscription>
|
||||
Cancel
|
||||
</x-layout-subscription>
|
@ -1,7 +1,8 @@
|
||||
<x-layout-subscription>
|
||||
@if ($settings->is_resale_license_active)
|
||||
<div class="flex justify-center mx-10">
|
||||
<div>
|
||||
|
||||
<div x-data>
|
||||
<div class="flex gap-2">
|
||||
<h2>Subscription</h2>
|
||||
<livewire:switch-team />
|
||||
@ -10,7 +11,12 @@
|
||||
<span>Currently active team: <span
|
||||
class="text-warning">{{ session('currentTeam.name') }}</span></span>
|
||||
</div>
|
||||
<x-pricing-plans />
|
||||
@if(request()->query->get('cancelled'))
|
||||
<div class="text-xl text-center text-red-500">Something went wrong. Please try again.</div>
|
||||
@endif
|
||||
@if (config('subscription.provider') !== null)
|
||||
<livewire:subscription.pricing-plans />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
3
resources/views/subscription/success.blade.php
Normal file
3
resources/views/subscription/success.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<x-layout>
|
||||
Success
|
||||
</x-layout>
|
@ -97,7 +97,9 @@
|
||||
Route::middleware(['throttle:force-password-reset'])->group(function() {
|
||||
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
|
||||
});
|
||||
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription');
|
||||
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.show');
|
||||
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/license', [Controller::class, 'license'])->name('settings.license');
|
||||
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\SubscriptionInvoiceFailedJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\GithubApp;
|
||||
@ -206,7 +207,131 @@
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
})->name('webhooks.waitlist.cancel');
|
||||
Route::post('/payments/events', function () {
|
||||
|
||||
|
||||
Route::post('/payments/stripe/events', function () {
|
||||
try {
|
||||
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||
$signature = request()->header('Stripe-Signature');
|
||||
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
request()->getContent(),
|
||||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
$webhook = Webhook::create([
|
||||
'type' => 'stripe',
|
||||
'payload' => request()->getContent()
|
||||
]);
|
||||
$type = data_get($event, 'type');
|
||||
$data = data_get($event, 'data.object');
|
||||
switch ($type) {
|
||||
case 'checkout.session.completed':
|
||||
$clientReferenceId = data_get($data, 'client_reference_id');
|
||||
$userId = Str::before($clientReferenceId, ':');
|
||||
$teamId = Str::after($clientReferenceId, ':');
|
||||
$subscriptionId = data_get($data, 'subscription');
|
||||
$customerId = data_get($data, 'customer');
|
||||
$team = Team::find($teamId);
|
||||
$found = $team->members->where('id', $userId)->first();
|
||||
if (!$found->isAdmin()) {
|
||||
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}.");
|
||||
}
|
||||
$subscription = Subscription::where('team_id', $teamId)->first();
|
||||
if ($subscription) {
|
||||
$subscription->update([
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
} else {
|
||||
Subscription::create([
|
||||
'team_id' => $teamId,
|
||||
'stripe_subscription_id' => $subscriptionId,
|
||||
'stripe_customer_id' => $customerId,
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case 'invoice.paid':
|
||||
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
|
||||
$subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
]);
|
||||
break;
|
||||
case 'invoice.payment_failed':
|
||||
$customerId = data_get($data, 'customer');
|
||||
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
|
||||
SubscriptionInvoiceFailedJob::dispatch($subscription->team);
|
||||
break;
|
||||
case 'customer.subscription.updated':
|
||||
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
|
||||
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
|
||||
$subscription->update([
|
||||
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
|
||||
]);
|
||||
break;
|
||||
case 'customer.subscription.deleted':
|
||||
$subscriptionId = data_get($data, 'items.data.0.subscription');
|
||||
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
|
||||
$subscription->update([
|
||||
'stripe_cancel_at_period_end' => false,
|
||||
'stripe_invoice_paid' => false,
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
// Unhandled event type
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
|
||||
$webhook->update([
|
||||
'status' => 'failed',
|
||||
'failure_reason' => $e->getMessage(),
|
||||
]);
|
||||
return response($e->getMessage(), 400);
|
||||
}
|
||||
});
|
||||
Route::post('/payments/paddle/events', function () {
|
||||
try {
|
||||
$payload = request()->all();
|
||||
$signature = request()->header('Paddle-Signature');
|
||||
$ts = Str::of($signature)->after('ts=')->before(';');
|
||||
$h1 = Str::of($signature)->after('h1=');
|
||||
$signedPayload = $ts->value . ':' . request()->getContent();
|
||||
$verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret'));
|
||||
ray($verify, $h1->value, hash_equals($verify, $h1->value));
|
||||
if (!hash_equals($verify, $h1->value)) {
|
||||
return response('Invalid signature.', 400);
|
||||
}
|
||||
$eventType = data_get($payload, 'event_type');
|
||||
ray($eventType);
|
||||
$webhook = Webhook::create([
|
||||
'type' => 'paddle',
|
||||
'payload' => $payload,
|
||||
]);
|
||||
// TODO - Handle events
|
||||
switch ($eventType) {
|
||||
case 'subscription.activated':
|
||||
}
|
||||
ray('Subscription event: ' . $eventType);
|
||||
$webhook->update([
|
||||
'status' => 'success',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
|
||||
$webhook->update([
|
||||
'status' => 'failed',
|
||||
'failure_reason' => $e->getMessage(),
|
||||
]);
|
||||
} finally {
|
||||
return response('OK');
|
||||
}
|
||||
});
|
||||
Route::post('/payments/lemon/events', function () {
|
||||
try {
|
||||
$secret = config('subscription.lemon_squeezy_webhook_secret');
|
||||
$payload = request()->collect();
|
||||
|
@ -47,9 +47,6 @@ function schedule:run {
|
||||
bash spin exec -u webuser coolify php artisan schedule:run
|
||||
}
|
||||
|
||||
function db:reset {
|
||||
bash spin exec -u webuser coolify php artisan migrate:fresh --seed
|
||||
}
|
||||
|
||||
function db {
|
||||
bash spin exec -u webuser coolify php artisan db
|
||||
@ -59,6 +56,9 @@ function db:migrate {
|
||||
bash spin exec -u webuser coolify php artisan migrate
|
||||
}
|
||||
|
||||
function db:reset {
|
||||
bash spin exec -u webuser coolify php artisan migrate:fresh --seed
|
||||
}
|
||||
function db:reset-prod {
|
||||
bash spin exec -u webuser coolify php artisan migrate:fresh --force --seed --seeder=ProductionSeeder ||
|
||||
php artisan migrate:fresh --force --seed --seeder=ProductionSeeder
|
||||
|
Loading…
Reference in New Issue
Block a user