Skip to content

Commit

Permalink
feat: allow to update a subscription frequency (#5436)
Browse files Browse the repository at this point in the history
  • Loading branch information
asbiin authored Aug 26, 2021
1 parent 60e47cf commit e298c63
Show file tree
Hide file tree
Showing 19 changed files with 495 additions and 167 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,13 @@ jobs:
run: phpdbg -dmemory_limit=4G -qrr vendor/bin/phpunit -c phpunit.xml --testsuite ${{ matrix.testsuite }} --log-junit ./results/junit/results${{ matrix.testsuite }}.xml --coverage-clover ./results/coverage/coverage${{ matrix.testsuite }}.xml
env:
DB_CONNECTION: ${{ matrix.connection }}
STRIPE_SECRET: ${{ secrets.STRIPE_SECRET }}
- name: Run Unit test suite
if: matrix.php-version != env.default-php-version
run: vendor/bin/phpunit -c phpunit.xml --testsuite ${{ matrix.testsuite }} --log-junit ./results/junit/results${{ matrix.testsuite }}.xml
env:
DB_CONNECTION: ${{ matrix.connection }}
STRIPE_SECRET: ${{ secrets.STRIPE_SECRET }}

- name: Fix results files
if: matrix.php-version == env.default-php-version
Expand Down
11 changes: 11 additions & 0 deletions app/Helpers/AccountHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ public static function hasReachedContactLimit(Account $account): bool
return $account->allContacts()->real()->active()->count() >= config('monica.number_of_allowed_contacts_free_account');
}

/**
* Indicate whether an account has not reached the contact limit of free accounts.
*
* @param Account $account
* @return bool
*/
public static function isBelowContactLimit(Account $account): bool
{
return $account->allContacts()->real()->active()->count() <= config('monica.number_of_allowed_contacts_free_account');
}

/**
* Check if the account can be downgraded, based on a set of rules.
*
Expand Down
2 changes: 1 addition & 1 deletion app/Helpers/DateHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static function getShortDate($date): string
/**
* Return a date in a full format like "October 29, 1981".
*
* @param string $date
* @param string|int $date
* @return string
*/
public static function getFullDate($date): string
Expand Down
40 changes: 40 additions & 0 deletions app/Helpers/InstanceHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,46 @@ public static function getPlanInformationFromConfig(string $timePeriod): ?array
];
}

/**
* Get the plan information for the given time period.
*
* @param \Laravel\Cashier\Subscription $subscription
* @return array|null
*/
public static function getPlanInformationFromSubscription(\Laravel\Cashier\Subscription $subscription): ?array
{
try {
$stripeSubscription = $subscription->asStripeSubscription();
$plan = $stripeSubscription->plan;
} catch (\Stripe\Exception\ApiErrorException $e) {
$stripeSubscription = null;
$plan = null;
}

if (is_null($stripeSubscription) || is_null($plan)) {
return [
'type' => $subscription->stripe_plan,
'name' => $subscription->name,
'id' => $subscription->stripe_id,
'price' => '?',
'friendlyPrice' => '?',
'nextBillingDate' => '',
];
}

$currency = Currency::where('iso', strtoupper($plan->currency))->first();
$amount = MoneyHelper::format($plan->amount, $currency);

return [
'type' => $plan->interval === 'month' ? 'monthly' : 'annual',
'name' => $subscription->name,
'id' => $plan->id,
'price' => $plan->amount,
'friendlyPrice' => $amount,
'nextBillingDate' => DateHelper::getFullDate($stripeSubscription->current_period_end),
];
}

/**
* Get changelogs entries.
*
Expand Down
109 changes: 91 additions & 18 deletions app/Http/Controllers/Settings/SubscriptionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,40 +31,35 @@ class SubscriptionsController extends Controller
*/
public function index()
{
$account = auth()->user()->account;

if (! config('monica.requires_subscription')) {
return redirect()->route('settings.index');
}

$account = auth()->user()->account;

$subscription = $account->getSubscribedPlan();
if (! $account->isSubscribed() && (! $subscription || $subscription->ended())) {
return view('settings.subscriptions.blank', [
'numberOfCustomers' => InstanceHelper::getNumberOfPaidSubscribers(),
]);
}

try {
$nextBillingDate = $account->getNextBillingDate();
} catch (StripeException $e) {
$nextBillingDate = trans('app.unknown');
}

$hasInvoices = $account->hasStripeId() && $account->hasInvoices();
$invoices = null;
if ($hasInvoices) {
$invoices = $account->invoices();
}

$planInformation = InstanceHelper::getPlanInformationFromConfig($subscription->name);

if ($planInformation === null) {
abort(404);
try {
$planInformation = $this->stripeCall(function () use ($subscription) {
return InstanceHelper::getPlanInformationFromSubscription($subscription);
});
} catch (StripeException $e) {
$planInformation = null;
}

return view('settings.subscriptions.account', [
'planInformation' => $planInformation,
'nextBillingDate' => $nextBillingDate,
'subscription' => $subscription,
'hasInvoices' => $hasInvoices,
'invoices' => $invoices,
Expand All @@ -89,6 +84,10 @@ public function upgrade(Request $request)
}

$plan = $request->query('plan');
if ($plan !== 'monthly' && $plan !== 'annual') {
abort(404);
}

$planInformation = InstanceHelper::getPlanInformationFromConfig($plan);

if ($planInformation === null) {
Expand All @@ -102,6 +101,78 @@ public function upgrade(Request $request)
]);
}

/**
* Display the update view page.
*
* @param Request $request
* @return View|Factory|RedirectResponse
*/
public function update(Request $request)
{
if (! config('monica.requires_subscription')) {
return redirect()->route('settings.index');
}

$account = auth()->user()->account;

$subscription = $account->getSubscribedPlan();
if (! $account->isSubscribed() && (! $subscription || $subscription->ended())) {
return view('settings.subscriptions.blank', [
'numberOfCustomers' => InstanceHelper::getNumberOfPaidSubscribers(),
]);
}

$planInformation = InstanceHelper::getPlanInformationFromSubscription($subscription);

if ($planInformation === null) {
abort(404);
}

$plans = collect();
foreach (['monthly', 'annual'] as $plan) {
$plans->push(InstanceHelper::getPlanInformationFromConfig($plan));
}

$legacyPlan = null;
if (! $plans->contains(function ($value) use ($planInformation) {
return $value['id'] === $planInformation['id'];
})) {
$legacyPlan = $planInformation;
}

return view('settings.subscriptions.update', [
'planInformation' => $planInformation,
'plans' => $plans,
'legacyPlan' => $legacyPlan,
]);
}

/**
* Process the update process.
*
* @param Request $request
* @return View|Factory|RedirectResponse
*/
public function processUpdate(Request $request)
{
$account = auth()->user()->account;

$subscription = $account->getSubscribedPlan();
if (! $account->isSubscribed() && ! $subscription) {
return redirect()->route('settings.index');
}

try {
$account->updateSubscription($request->input('frequency'), $subscription);
} catch (StripeException $e) {
return back()
->withInput()
->withErrors($e->getMessage());
}

return redirect()->route('settings.subscriptions.index');
}

/**
* Display the confirm view page.
*
Expand Down Expand Up @@ -200,7 +271,7 @@ public function downgrade()
->with('numberOfPendingInvitations', $account->invitations()->count())
->with('numberOfUsers', $account->users()->count())
->with('accountHasLimitations', AccountHelper::hasLimitations($account))
->with('hasReachedContactLimit', AccountHelper::hasReachedContactLimit($account))
->with('hasReachedContactLimit', ! AccountHelper::isBelowContactLimit($account))
->with('canDowngrade', AccountHelper::canDowngrade($account));
}

Expand All @@ -211,17 +282,19 @@ public function downgrade()
*/
public function processDowngrade()
{
if (! AccountHelper::canDowngrade(auth()->user()->account)) {
$account = auth()->user()->account;

if (! AccountHelper::canDowngrade($account)) {
return redirect()->route('settings.subscriptions.downgrade');
}

$subscription = auth()->user()->account->getSubscribedPlan();
if (! auth()->user()->account->isSubscribed() && ! $subscription) {
$subscription = $account->getSubscribedPlan();
if (! $account->isSubscribed() && ! $subscription) {
return redirect()->route('settings.index');
}

try {
auth()->user()->account->subscriptionCancel();
$account->subscriptionCancel();
} catch (StripeException $e) {
return back()
->withInput()
Expand Down
6 changes: 1 addition & 5 deletions app/Services/Account/Settings/DestroyAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,7 @@ private function destroyPhotos(Account $account)
private function cancelStripe(Account $account)
{
if ($account->isSubscribed() && ! $account->has_access_to_paid_version_for_free) {
try {
$account->subscriptionCancel();
} catch (StripeException $e) {
throw new StripeException();
}
$account->subscriptionCancel();
}
}
}
2 changes: 2 additions & 0 deletions app/Traits/StripeCall.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ private function stripeCall($callback)
} catch (\Stripe\Exception\ApiErrorException $e) {
$errorMessage = $e->getMessage();
Log::error('Stripe error: '.(string) $e, $e->getJsonBody() ?: []);
} catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) {
throw $e;
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
Log::error('Stripe error: '.(string) $e);
Expand Down
71 changes: 36 additions & 35 deletions app/Traits/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace App\Traits;

use App\Helpers\DateHelper;
use Laravel\Cashier\Billable;
use App\Helpers\InstanceHelper;

Expand Down Expand Up @@ -34,6 +33,39 @@ public function subscribe(string $payment_method, string $planName)
});
}

/**
* Update an existing subscription.
*
* @param string $planName
* @param \Laravel\Cashier\Subscription $subscription
* @return \Laravel\Cashier\Subscription
*/
public function updateSubscription(string $planName, \Laravel\Cashier\Subscription $subscription)
{
$oldPlan = $subscription->stripe_plan;
$plan = InstanceHelper::getPlanInformationFromConfig($planName);
if ($plan === null) {
abort(404);
}

if ($oldPlan === $planName) {
// No change
return $subscription;
}

$subscription = $this->stripeCall(function () use ($subscription, $plan) {
return $subscription->swap($plan['id']);
});

if ($subscription->stripe_plan !== $oldPlan && $subscription->stripe_plan === $plan['id']) {
$subscription->forceFill([
'name' => $plan['name'],
])->save();
}

return $subscription;
}

/**
* Check if the account is currently subscribed to a plan.
*
Expand All @@ -45,8 +77,7 @@ public function isSubscribed()
return true;
}

return $this->subscribed(config('monica.paid_plan_monthly_friendly_name'))
|| $this->subscribed(config('monica.paid_plan_annual_friendly_name'));
return $this->getSubscribedPlan() !== null;
}

/**
Expand All @@ -56,13 +87,7 @@ public function isSubscribed()
*/
public function getSubscribedPlan()
{
$subscription = $this->subscription(config('monica.paid_plan_monthly_friendly_name'));

if (! $subscription) {
$subscription = $this->subscription(config('monica.paid_plan_annual_friendly_name'));
}

return $subscription;
return $this->subscriptions()->recurring()->first();
}

/**
Expand All @@ -74,11 +99,7 @@ public function getSubscribedPlanId()
{
$plan = $this->getSubscribedPlan();

if (! is_null($plan)) {
return $plan->stripe_plan;
}

return '';
return is_null($plan) ? '' : $plan->stripe_plan;
}

/**
Expand Down Expand Up @@ -122,24 +143,4 @@ public function hasInvoices()
{
return $this->subscriptions()->count() > 0;
}

/**
* Get the next billing date for the account.
*
* @return string
*/
public function getNextBillingDate()
{
// Weird method to get the next billing date from Laravel Cashier
// see https://stackoverflow.com/questions/41576568/get-next-billing-date-from-laravel-cashier
return $this->stripeCall(function () {
$subscriptions = $this->asStripeCustomer()['subscriptions'];
if (! $subscriptions || count($subscriptions->data) <= 0) {
return '';
}
$timestamp = $subscriptions->data[0]['current_period_end'];

return DateHelper::getFullDate($timestamp);
});
}
}
Loading

0 comments on commit e298c63

Please sign in to comment.