Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.6] KTC-131 Subscriptions with Stripe #1973

Merged
merged 71 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
adeb4dc
Initial commit subscriptions
Keysie27 Sep 6, 2024
c7db49b
Refactor: Subscriptions
Keysie27 Sep 10, 2024
700daed
Refactor: Add subscriptionItems and prices
Keysie27 Sep 12, 2024
90693d2
Merge branch 'development' into KTC-131
Keysie27 Sep 12, 2024
9c85883
Apply fixes from StyleCI
StyleCIBot Sep 12, 2024
79f15e8
Merge pull request #1976 from bakaphp/analysis-16M2Lw
kaioken Sep 12, 2024
232afe1
Refactor
Keysie27 Sep 17, 2024
61615cb
Merge branch 'development' of https://github.com/bakaphp/kanvas-ecosy…
Keysie27 Sep 17, 2024
3b2674a
Merge branch 'KTC-131' of https://github.com/bakaphp/kanvas-ecosystem…
Keysie27 Sep 17, 2024
d3e2baf
Refactor: Stylo
Keysie27 Sep 17, 2024
db7040e
Refactor: Stylo
Keysie27 Sep 17, 2024
eb6f032
Refactor: PriceMutation
Keysie27 Sep 17, 2024
5dd364e
Add: apps_plans_prices table
Keysie27 Sep 17, 2024
bf50bfd
Update composer.lock
Keysie27 Sep 17, 2024
c51927c
Add: Queries for company_subscriptions and app_plans
Keysie27 Sep 17, 2024
c225fff
Apply fixes from StyleCI
StyleCIBot Sep 18, 2024
64129de
Merge pull request #1994 from bakaphp/analysis-prGJ2Z
kaioken Sep 18, 2024
5459588
Refactor: Test
Keysie27 Sep 19, 2024
3de1b1c
Refactor: Test
Keysie27 Sep 19, 2024
bbd380d
Merge branch 'KTC-131' of https://github.com/bakaphp/kanvas-ecosystem…
Keysie27 Sep 19, 2024
0bcdb67
Stylo
Keysie27 Sep 19, 2024
61e0b7f
Merge remote-tracking branch 'origin/development' into KTC-131
Keysie27 Sep 19, 2024
0529510
Apply fixes from StyleCI
StyleCIBot Sep 19, 2024
7dcb93f
Merge pull request #2006 from bakaphp/analysis-a6KO9m
kaioken Sep 19, 2024
bbeb799
Refactor
Keysie27 Sep 20, 2024
7129f46
Refactor delete switchplan
Keysie27 Sep 20, 2024
17f6e80
Refactor: database
Keysie27 Sep 20, 2024
0eec894
Refactor: Tests
Keysie27 Sep 20, 2024
f05fcb0
Refactor: Feedback from PR
Keysie27 Sep 20, 2024
bfee7db
Merge branch 'KTC-131' of https://github.com/bakaphp/kanvas-ecosystem…
Keysie27 Sep 20, 2024
cdef89b
Stylo
Keysie27 Sep 20, 2024
04785dd
Add: Subscription migrations Command setup
Keysie27 Sep 20, 2024
36b2bd8
Solve seed conflict
Keysie27 Sep 20, 2024
633c0ce
Fix: undefined array key in SubscriptionMutation
Keysie27 Sep 20, 2024
e13c54e
fix: issue with plan dto
kaioken Sep 20, 2024
5927a45
fix: issue with plan dto
kaioken Sep 20, 2024
b2dbdbf
fix: issue with plan dto
kaioken Sep 20, 2024
5bfbec8
refact: missing library
kaioken Sep 20, 2024
076cbda
fix: test
kaioken Sep 20, 2024
927efb2
Merge branch 'development' into KTC-131
kaioken Sep 20, 2024
7f47252
refact: move all logic to use cashier
kaioken Sep 21, 2024
6e57ac7
refact: move all logic to use cashier
kaioken Sep 21, 2024
e28a78e
refact: add get subscriiptions
kaioken Sep 21, 2024
1cff49d
refact: remove thing not needed
kaioken Sep 21, 2024
e8a94e3
refact: remove thing not needed
kaioken Sep 21, 2024
454d88e
refact: test
kaioken Sep 21, 2024
80bd6de
refact: test
kaioken Sep 21, 2024
630a9ca
refact: test
kaioken Sep 21, 2024
a1f1553
refact: fix
kaioken Sep 21, 2024
57db365
refact: fix
kaioken Sep 21, 2024
7e446a4
fix: test
kaioken Sep 21, 2024
2927d1b
fix: test
kaioken Sep 21, 2024
5d8960c
fix: test
kaioken Sep 21, 2024
a14dcfe
fix: test
kaioken Sep 21, 2024
1bb68bf
fix: test
kaioken Sep 21, 2024
624f8ff
fix: test
kaioken Sep 21, 2024
491a474
fix: test
kaioken Sep 21, 2024
b251c0f
refact: test ids
kaioken Sep 21, 2024
542a753
refact: test ids
kaioken Sep 21, 2024
6194055
refact: joins
kaioken Sep 21, 2024
1a04a6e
refact: join
kaioken Sep 21, 2024
5912cad
refact: join
kaioken Sep 21, 2024
7dd9d8a
refact: join
kaioken Sep 21, 2024
194a683
refact: get stripe by app
kaioken Sep 21, 2024
6116689
refact: get stripe by app
kaioken Sep 21, 2024
c42ffe4
Merge pull request #2022 from bakaphp/refact-subscription-hdnel
kaioken Sep 21, 2024
4728392
refact: bukder
kaioken Sep 21, 2024
923a73f
feat: add missiing test
kaioken Sep 21, 2024
70e8e29
refact: remove function
kaioken Sep 21, 2024
334042e
refact: merge with dev
kaioken Sep 23, 2024
38c2a73
Merge pull request #2027 from bakaphp/KTC-131-update-dev
kaioken Sep 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if Stripe has validation for not create duplicate records?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each product or price has a different ID in Stripe even if it has the same name and detail. That's why en is linked by the Stripe price_id.

'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
Loading