Skip to content

Commit

Permalink
Merge pull request #1973 from bakaphp/KTC-131
Browse files Browse the repository at this point in the history
[1.6] KTC-131 Subscriptions with Stripe
  • Loading branch information
kaioken authored Sep 23, 2024
2 parents 9fa57ba + 38c2a73 commit e2b3eab
Show file tree
Hide file tree
Showing 49 changed files with 2,376 additions and 30 deletions.
2 changes: 1 addition & 1 deletion 1.x.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/do

# Install php extensions
RUN chmod +x /usr/local/bin/install-php-extensions && sync && \
install-php-extensions mbstring pdo_mysql zip exif pcntl gd memcached redis swoole opcache curl readline sqlite3 msgpack igbinary pcov sockets
install-php-extensions mbstring pdo_mysql zip exif pcntl gd memcached redis swoole opcache curl readline sqlite3 msgpack igbinary pcov sockets bcmath

# Install dependencies
RUN apt-get update && apt-get install -y \
Expand Down
1 change: 1 addition & 0 deletions app/Console/Commands/KanvasSetupCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function handle()
'migrate --path database/migrations/Souk/ --database commerce',
'migrate --path vendor/laravel-workflow/laravel-workflow/src/migrations/ --database workflow',
'migrate --path database/migrations/ActionEngine/ --database action_engine',
'migrate --path database/migrations/Subscription/ --database mysql',
'db:seed',
'db:seed --class=Database\\\Seeders\\\GuildSeeder --database crm',
'kanvas:create-role Admin',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace App\GraphQL\Subscription\Builders\Subscriptions;

use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Builder;
use Kanvas\Apps\Models\Apps;
use Laravel\Cashier\Subscription;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class SubscriptionBuilder
{
public function getSubscriptions(
mixed $root,
array $args,
GraphQLContext $context,
ResolveInfo $resolveInfo
): Builder {
$user = auth()->user();
$company = $user->getCurrentCompany();
$app = app(Apps::class);

return Subscription::query()
->select('subscriptions.*')
->join('apps_stripe_customers', 'apps_stripe_customers.id', '=', 'subscriptions.apps_stripe_customer_id')
->where('apps_stripe_customers.companies_id', $company->id)
->where('apps_stripe_customers.apps_id', $app->getId());
}
}
90 changes: 90 additions & 0 deletions app/GraphQL/Subscription/Mutations/Plans/PlanMutation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace App\GraphQL\Subscriptions\Mutations\Plans;

use Kanvas\Subscription\Plans\Actions\CreatePlan;
use Kanvas\Subscription\Plans\Actions\UpdatePlan;
use Kanvas\Subscription\Plans\Repositories\PlanRepository;
use Kanvas\Subscription\Plans\DataTransferObject\Plan as PlanDto;
use Kanvas\Subscription\Plans\Models\Plan as PlanModel;
use Kanvas\Apps\Models\Apps;
use Illuminate\Support\Facades\Auth;
use Stripe\Stripe;
use Stripe\Product as StripeProduct;

class PlanMutation
{
/**
* create.
*
* @param mixed $root
* @param array $req
*
* @return PlanModel
*/
public function create(mixed $root, array $req): PlanModel
{
$app = app(Apps::class);
$stripeProduct = StripeProduct::create([
'name' => $req['input']['name'],
'description' => $req['input']['description'] ?? '',
]);

$dto = PlanDto::viaRequest(
array_merge($req['input'], ['stripe_id' => $stripeProduct->id]),
Auth::user(),
$app
);

$action = new CreatePlan($dto);

return $action->execute();
}

/**
* update.
*
* @param mixed $root
* @param array $req
*
* @return PlanModel
*/
public function update(array $req): PlanModel
{
$app = app(Apps::class);
$plan = PlanRepository::getById($req['id']);

StripeProduct::update($plan->stripe_id, [
'name' => $req['input']['name'] ?? $plan->name,
'description' => $req['input']['description'] ?? $plan->description,
]);

$dto = PlanDto::viaRequest($req['input'], Auth::user(), $app);

$action = new UpdatePlan($plan, $dto);

return $action->execute();
}

/**
* delete.
*
* @param mixed $root
* @param array $req
*
* @return bool
*/
public function delete(array $req): bool
{
$plan = PlanRepository::getById($req['id']);

$stripeProduct = StripeProduct::retrieve($plan->stripe_id);
$stripeProduct->delete();

$plan->delete();

return true;
}
}
81 changes: 81 additions & 0 deletions app/GraphQL/Subscription/Mutations/Prices/PriceMutation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace App\GraphQL\Prices\Mutations;

use Illuminate\Support\Facades\Auth;
use Kanvas\Apps\Models\Apps;
use Kanvas\Subscription\Prices\Actions\CreatePrice;
use Kanvas\Subscription\Prices\Actions\UpdatePrice;
use Kanvas\Subscription\Prices\DataTransferObject\Price as PriceDto;
use Kanvas\Subscription\Prices\Models\Price as PriceModel;
use Kanvas\Subscription\Prices\Repositories\PriceRepository;
use Stripe\Price as StripePrice;
use Stripe\Product as StripeProduct;

class PriceMutation
{
/**
* create.
*/
public function create(array $req): PriceModel
{
$app = app(Apps::class);
$stripeProduct = StripeProduct::create([
'name' => 'Price for Plan ' . $req['input']['apps_plans_id'],
]);

$stripePrice = StripePrice::create([
'unit_amount' => $req['input']['amount'] * 100,
'currency' => $req['input']['currency'],
'recurring' => ['interval' => $req['input']['interval']],
'product' => $stripeProduct->id,
]);

$dto = PriceDto::viaRequest(
array_merge($req['input'], ['stripe_id' => $stripePrice->id]),
Auth::user(),
$app
);

$action = new CreatePrice($dto, Auth::user());
$priceModel = $action->execute();

return $priceModel;
}

/**
* update.
*/
public function update(array $req): PriceModel
{
$app = app(Apps::class);
$price = PriceRepository::getById($req['id']);

StripePrice::create([
'unit_amount' => $req['input']['amount'] * 100,
'currency' => $price->currency,
'recurring' => ['interval' => $price->interval],
'product' => $price->stripe_id,
]);

$dto = PriceDto::viaRequest($req['input'], Auth::user(), $app);
$action = new UpdatePrice($price, $dto, Auth::user());
$updatedPrice = $action->execute();

return $updatedPrice;
}

/**
* delete.
*/
public function delete(array $req): bool
{
$price = PriceRepository::getById($req['id']);

$price->delete();

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

declare(strict_types=1);

namespace App\GraphQL\Subscription\Mutations\Subscriptions;

use Carbon\Carbon;
use Kanvas\Apps\Models\Apps;
use Kanvas\Connectors\Stripe\Enums\ConfigurationEnum;
use Kanvas\Exceptions\ValidationException;
use Kanvas\Subscription\Prices\Models\Price;
use Kanvas\Subscription\Prices\Repositories\PriceRepository;
use Kanvas\Subscription\Subscriptions\DataTransferObject\SubscriptionInput;
use Laravel\Cashier\Subscription;
use Throwable;

class SubscriptionMutation
{
private Apps $app;
private $user;

public function __construct()
{
$this->app = app(Apps::class);
$this->user = auth()->user();

if (empty($this->app->get(ConfigurationEnum::STRIPE_SECRET_KEY->value))) {
throw new ValidationException('Stripe is not configured for this app');
}
}

public function create($root, array $args, $context): Subscription
{
$data = $args['input'];
$company = $this->user->getCurrentCompany();

$subscriptionInput = SubscriptionInput::viaRequest(
$data,
$this->user,
$company,
$this->app
);

$companyStripeAccount = $company->getStripeAccount($this->app);

if (! $companyStripeAccount->subscriptions()->exists()) {
try {
$subscription = $companyStripeAccount->newSubscription($subscriptionInput->price->plan->stripe_plan, $subscriptionInput->price->stripe_id);
if ($subscriptionInput->price->plan->free_trial_days) {
$subscription->trialDays($subscriptionInput->price->plan->free_trial_days);
}
$subscription->create($subscriptionInput->payment_method_id);
} catch (Throwable $e) {
throw new ValidationException($e->getMessage());
}
}

return $companyStripeAccount->subscriptions()->firstOrFail();
}

public function update($root, array $args, $context, $info): Subscription
{
$data = $args['input'];
$company = $this->user->getCurrentCompany();
$companyStripeAccount = $company->getStripeAccount($this->app);

if (! $companyStripeAccount->subscriptions()->exists()) {
throw new ValidationException('No active subscription found for your company');
}
$newPrice = PriceRepository::getByIdWithApp((int) $data['apps_plans_prices_id'], $this->app);

$upgradeSubscription = $companyStripeAccount
->subscriptions()->where('type', $newPrice->plan->stripe_plan)->first();

if (! $upgradeSubscription) {
throw new ValidationException('Trying to upgrade to of a different type');
}

$upgradeSubscription->swap($newPrice->stripe_id);

return $upgradeSubscription;
}

public function cancel(mixed $root, array $args): bool
{
$id = $args['id'];
$company = $this->user->getCurrentCompany();
$companyStripeAccount = $company->getStripeAccount($this->app);

if (! $companyStripeAccount->subscriptions()->exists()) {
throw new ValidationException('No active subscription found for your company');
}

$upgradeSubscription = $companyStripeAccount
->subscriptions()->where('id', $id)->first();

if (! $upgradeSubscription) {
throw new ValidationException('Trying to cancel a subscription that does not exist');
}

$cancelSubscription = $upgradeSubscription->cancel();

return $cancelSubscription->ends_at !== null;
}

public function reactivate(mixed $root, array $args): Subscription
{
$id = $args['id'];
$company = $this->user->getCurrentCompany();
$companyStripeAccount = $company->getStripeAccount($this->app);

if (! $companyStripeAccount->subscriptions()->exists()) {
throw new ValidationException('No subscriptions found for your company');
}

$subscription = $companyStripeAccount
->subscriptions()->where('id', $id)->first();

if (! $subscription) {
throw new ValidationException('Subscription not found');
}

// Check if the subscription is past the grace period (30 days)
$gracePeriodEnd = Carbon::parse($subscription->ends_at)->addDays(30);
if (Carbon::now()->isAfter($gracePeriodEnd)) {
// Past grace period, create a new subscription
$price = Price::where('stripe_id', $subscription->stripe_price)->firstOrFail();

try {
$newSubscription = $companyStripeAccount->newSubscription($price->plan->stripe_plan, $price->stripe_id);
if ($price->plan->free_trial_days) {
$newSubscription->trialDays($price->plan->free_trial_days);
}

return $newSubscription->create($subscription->latestPayment()->payment_method);
} catch (Throwable $e) {
throw new ValidationException('Failed to create new subscription: ' . $e->getMessage());
}
}

// Within grace period, resume the existing subscription
try {
return $subscription->resume();
} catch (Throwable $e) {
throw new ValidationException('Failed to reactivate subscription: ' . $e->getMessage());
}
}
}
6 changes: 3 additions & 3 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

namespace App\Providers;

use Bouncer;
use Illuminate\Support\ServiceProvider;
use Kanvas\AccessControlList\Enums\RolesEnums;
use Kanvas\Apps\Models\Apps;
use Kanvas\Sessions\Models\Sessions;
use Kanvas\Subscription\Subscriptions\Models\AppsStripeCustomer;
use Laravel\Cashier\Cashier;
use Laravel\Sanctum\Sanctum;

class AppServiceProvider extends ServiceProvider
Expand All @@ -29,5 +28,6 @@ public function register()
public function boot()
{
Sanctum::usePersonalAccessTokenModel(Sessions::class);
Cashier::useCustomerModel(AppsStripeCustomer::class);
}
}
Loading

0 comments on commit e2b3eab

Please sign in to comment.