-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
adeb4dc
Initial commit subscriptions
Keysie27 c7db49b
Refactor: Subscriptions
Keysie27 700daed
Refactor: Add subscriptionItems and prices
Keysie27 90693d2
Merge branch 'development' into KTC-131
Keysie27 9c85883
Apply fixes from StyleCI
StyleCIBot 79f15e8
Merge pull request #1976 from bakaphp/analysis-16M2Lw
kaioken 232afe1
Refactor
Keysie27 61615cb
Merge branch 'development' of https://github.com/bakaphp/kanvas-ecosy…
Keysie27 3b2674a
Merge branch 'KTC-131' of https://github.com/bakaphp/kanvas-ecosystem…
Keysie27 d3e2baf
Refactor: Stylo
Keysie27 db7040e
Refactor: Stylo
Keysie27 eb6f032
Refactor: PriceMutation
Keysie27 5dd364e
Add: apps_plans_prices table
Keysie27 bf50bfd
Update composer.lock
Keysie27 c51927c
Add: Queries for company_subscriptions and app_plans
Keysie27 c225fff
Apply fixes from StyleCI
StyleCIBot 64129de
Merge pull request #1994 from bakaphp/analysis-prGJ2Z
kaioken 5459588
Refactor: Test
Keysie27 3de1b1c
Refactor: Test
Keysie27 bbd380d
Merge branch 'KTC-131' of https://github.com/bakaphp/kanvas-ecosystem…
Keysie27 0bcdb67
Stylo
Keysie27 61e0b7f
Merge remote-tracking branch 'origin/development' into KTC-131
Keysie27 0529510
Apply fixes from StyleCI
StyleCIBot 7dcb93f
Merge pull request #2006 from bakaphp/analysis-a6KO9m
kaioken bbeb799
Refactor
Keysie27 7129f46
Refactor delete switchplan
Keysie27 17f6e80
Refactor: database
Keysie27 0eec894
Refactor: Tests
Keysie27 f05fcb0
Refactor: Feedback from PR
Keysie27 bfee7db
Merge branch 'KTC-131' of https://github.com/bakaphp/kanvas-ecosystem…
Keysie27 cdef89b
Stylo
Keysie27 04785dd
Add: Subscription migrations Command setup
Keysie27 36b2bd8
Solve seed conflict
Keysie27 633c0ce
Fix: undefined array key in SubscriptionMutation
Keysie27 e13c54e
fix: issue with plan dto
kaioken 5927a45
fix: issue with plan dto
kaioken b2dbdbf
fix: issue with plan dto
kaioken 5bfbec8
refact: missing library
kaioken 076cbda
fix: test
kaioken 927efb2
Merge branch 'development' into KTC-131
kaioken 7f47252
refact: move all logic to use cashier
kaioken 6e57ac7
refact: move all logic to use cashier
kaioken e28a78e
refact: add get subscriiptions
kaioken 1cff49d
refact: remove thing not needed
kaioken e8a94e3
refact: remove thing not needed
kaioken 454d88e
refact: test
kaioken 80bd6de
refact: test
kaioken 630a9ca
refact: test
kaioken a1f1553
refact: fix
kaioken 57db365
refact: fix
kaioken 7e446a4
fix: test
kaioken 2927d1b
fix: test
kaioken 5d8960c
fix: test
kaioken a14dcfe
fix: test
kaioken 1bb68bf
fix: test
kaioken 624f8ff
fix: test
kaioken 491a474
fix: test
kaioken b251c0f
refact: test ids
kaioken 542a753
refact: test ids
kaioken 6194055
refact: joins
kaioken 1a04a6e
refact: join
kaioken 5912cad
refact: join
kaioken 7dd9d8a
refact: join
kaioken 194a683
refact: get stripe by app
kaioken 6116689
refact: get stripe by app
kaioken c42ffe4
Merge pull request #2022 from bakaphp/refact-subscription-hdnel
kaioken 4728392
refact: bukder
kaioken 923a73f
feat: add missiing test
kaioken 70e8e29
refact: remove function
kaioken 334042e
refact: merge with dev
kaioken 38c2a73
Merge pull request #2027 from bakaphp/KTC-131-update-dev
kaioken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
app/GraphQL/Subscription/Builders/Subscriptions/SubscriptionBuilder.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
81
app/GraphQL/Subscription/Mutations/Prices/PriceMutation.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
148 changes: 148 additions & 0 deletions
148
app/GraphQL/Subscription/Mutations/Subscriptions/SubscriptionMutation.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.