-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1973 from bakaphp/KTC-131
[1.6] KTC-131 Subscriptions with Stripe
- Loading branch information
Showing
49 changed files
with
2,376 additions
and
30 deletions.
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.