fix: fix subscription cancel (#2886)

This commit is contained in:
Alexis Saettler
2019-08-11 18:35:30 +02:00
committed by GitHub
parent dfb7b07ff4
commit 557f34bf55
17 changed files with 445 additions and 117 deletions
BIN
View File
Binary file not shown.
+2
View File
@@ -13,6 +13,8 @@ steps:
ulimit -S unlimited
phpdbg -dmemory_limit=4G -qrr vendor/bin/phpunit -c phpunit.xml --log-junit ./results/junit/unit/results${SYSTEM_JOBPOSITIONINPHASE}.xml --coverage-clover ./results/coverage${SYSTEM_JOBPOSITIONINPHASE}.xml --testsuite $TESTSUITE
displayName: 'Run unit tests'
env:
STRIPE_SECRET: $(STRIPE_SECRET)
- task: PublishTestResults@2
displayName: 'Publish Test Results'
+1
View File
@@ -13,6 +13,7 @@
### Fixes:
* Fix subscription cancel
* Fix last consulted contact list
* Fix exception in case a user register twice
* Fix vcard export with empty gender
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Exceptions;
use RuntimeException;
/**
* Exception thrown by Stripe.
*/
class StripeException extends RuntimeException
{
}
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Settings;
use App\Helpers\DateHelper;
use Illuminate\Http\Request;
use App\Helpers\InstanceHelper;
use App\Exceptions\StripeException;
use App\Http\Controllers\Controller;
class SubscriptionsController extends Controller
@@ -27,10 +28,15 @@ class SubscriptionsController extends Controller
}
$planId = auth()->user()->account->getSubscribedPlanId();
try {
$nextBillingDate = auth()->user()->account->getNextBillingDate();
} catch (StripeException $e) {
$nextBillingDate = trans('app.unknown');
}
return view('settings.subscriptions.account', [
'planInformation' => InstanceHelper::getPlanInformationFromConfig($planId),
'nextBillingDate' => auth()->user()->account->getNextBillingDate(),
'nextBillingDate' => $nextBillingDate,
]);
}
@@ -118,7 +124,7 @@ class SubscriptionsController extends Controller
return redirect()->route('settings.index');
}
auth()->user()->account->subscription(auth()->user()->account->getSubscribedPlanName())->cancelNow();
auth()->user()->account->subscriptionCancel();
return redirect()->route('settings.subscriptions.downgrade.success');
}
@@ -134,40 +140,16 @@ class SubscriptionsController extends Controller
return redirect()->route('settings.index');
}
$stripeToken = $request->input('stripeToken');
$plan = InstanceHelper::getPlanInformationFromConfig($request->input('plan'));
$errorMessage = '';
try {
auth()->user()->account->newSubscription($plan['name'], $plan['id'])
->create($stripeToken, [
'email' => auth()->user()->email,
]);
return redirect()->route('settings.subscriptions.upgrade.success');
} catch (\Stripe\Error\Card $e) {
// Since it's a decline, \Stripe\Error\Card will be caught
$body = $e->getJsonBody();
$err = $body['error'];
$errorMessage = trans('settings.stripe_error_card', ['message' => $err['message']]);
} catch (\Stripe\Error\RateLimit $e) {
// Too many requests made to the API too quickly
$errorMessage = trans('settings.stripe_error_rate_limit');
} catch (\Stripe\Error\Authentication $e) {
// Authentication with Stripe's API failed
// (maybe you changed API keys recently)
$errorMessage = trans('settings.stripe_error_authentication');
} catch (\Stripe\Error\ApiConnection $e) {
// Network communication with Stripe failed
$errorMessage = trans('settings.stripe_error_api_connection_error');
} catch (\Stripe\Error\Base $e) {
$errorMessage = $e->getMessage();
auth()->user()->account
->subscribe($request->input('stripeToken'), $request->input('plan'));
} catch (StripeException $e) {
return back()
->withInput()
->withErrors($e->getMessage());
}
return back()
->withInput()
->withErrors($errorMessage);
return redirect()->route('settings.subscriptions.upgrade.success');
}
/**
+7 -1
View File
@@ -19,6 +19,7 @@ use App\Models\Account\ImportJob;
use App\Models\Account\Invitation;
use App\Services\User\EmailChange;
use Illuminate\Support\Facades\DB;
use App\Exceptions\StripeException;
use Lahaxearnaud\U2f\Models\U2fKey;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\ImportsRequest;
@@ -178,7 +179,12 @@ class SettingsController
$account = auth()->user()->account;
if ($account->isSubscribed() && ! $account->has_access_to_paid_version_for_free) {
$account->subscriptionCancel();
try {
$account->subscriptionCancel();
} catch (StripeException $e) {
return redirect()->route('settings.index')
->withErrors($e->getMessage());
}
}
DB::table('accounts')->where('id', $account->id)->delete();
+2 -76
View File
@@ -13,8 +13,8 @@ use App\Models\Contact\Debt;
use App\Models\Contact\Gift;
use App\Models\Contact\Note;
use App\Models\Contact\Task;
use App\Traits\Subscription;
use App\Models\Journal\Entry;
use Laravel\Cashier\Billable;
use App\Models\Contact\Gender;
use App\Models\Contact\Address;
use App\Models\Contact\Contact;
@@ -45,7 +45,7 @@ use App\Services\Auth\Population\PopulateContactFieldTypesTable;
class Account extends Model
{
use Billable;
use Subscription;
/**
* The attributes that are mass assignable.
@@ -532,38 +532,6 @@ class Account extends Model
return $isSubscribed;
}
/**
* Check if the account has invoices linked to this account.
* This was created because Laravel Cashier doesn't know how to properly
* handled the case when a user doesn't have invoices yet. This sucks balls.
*
* @return bool
*/
public function hasInvoices()
{
$query = DB::table('subscriptions')->where('account_id', $this->id)->count();
return $query > 0;
}
/**
* Get the next billing date for the account.
*
* @return string $timestamp
*/
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
$subscriptions = $this->asStripeCustomer()['subscriptions'];
if (count($subscriptions->data) <= 0) {
return;
}
$timestamp = $subscriptions->data[0]['current_period_end'];
return DateHelper::getShortDate($timestamp);
}
/**
* Indicates whether the current account has limitations with her current
* plan.
@@ -740,48 +708,6 @@ class Account extends Model
->get();
}
/**
* Get the id of the plan the account is subscribed to.
*
* @return string
*/
public function getSubscribedPlanId()
{
$plan = $this->subscriptions()->first();
if (! is_null($plan)) {
return $plan->stripe_plan;
}
return '';
}
/**
* Get the friendly name of the plan the account is subscribed to.
*
* @return string
*/
public function getSubscribedPlanName()
{
$plan = $this->subscriptions()->first();
if (! is_null($plan)) {
return $plan->name;
}
}
/**
* Cancel the plan the account is subscribed to.
*/
public function subscriptionCancel()
{
$plan = $this->subscriptions()->first();
if (! is_null($plan)) {
return $plan->cancelNow();
}
}
/**
* Replaces a specific gender of all the contacts in the account with another
* gender.
+161
View File
@@ -0,0 +1,161 @@
<?php
namespace App\Traits;
use App\Helpers\DateHelper;
use Laravel\Cashier\Billable;
use App\Helpers\InstanceHelper;
use Illuminate\Support\Facades\DB;
use App\Exceptions\StripeException;
use Illuminate\Support\Facades\Log;
trait Subscription
{
use Billable;
/**
* Process the upgrade payment.
*
* @param string $stripeToken
* @param string $planName
* @return bool|string
*/
public function subscribe(string $stripeToken, string $planName)
{
$plan = InstanceHelper::getPlanInformationFromConfig($planName);
return $this->stripeCall(function () use ($stripeToken, $plan) {
$this->newSubscription($plan['name'], $plan['id'])
->create($stripeToken, [
'email' => auth()->user()->email,
]);
return true;
});
}
/**
* Get the id of the plan the account is subscribed to.
*
* @return string
*/
public function getSubscribedPlanId()
{
$plan = $this->subscriptions()->first();
if (! is_null($plan)) {
return $plan->stripe_plan;
}
return '';
}
/**
* Get the friendly name of the plan the account is subscribed to.
*
* @return string|null
*/
public function getSubscribedPlanName()
{
$plan = $this->subscriptions()->first();
if (! is_null($plan)) {
return $plan->name;
}
}
/**
* Cancel the plan the account is subscribed to.
*
* @return bool|string
*/
public function subscriptionCancel()
{
$plan = $this->subscriptions()->first();
if (! is_null($plan)) {
return $this->stripeCall(function () use ($plan) {
$plan->cancelNow();
return true;
});
}
return false;
}
/**
* Check if the account has invoices linked to this account.
* This was created because Laravel Cashier doesn't know how to properly
* handled the case when a user doesn't have invoices yet. This sucks balls.
*
* @return bool
*/
public function hasInvoices()
{
$query = DB::table('subscriptions')->where('account_id', $this->id)->count();
return $query > 0;
}
/**
* Get the next billing date for the account.
*
* @return string $timestamp
*/
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 (count($subscriptions->data) <= 0) {
return '';
}
$timestamp = $subscriptions->data[0]['current_period_end'];
return DateHelper::getShortDate($timestamp);
});
}
/**
* Call stripe.
*
* @param callable $callback
* @return mixed
*/
private function stripeCall($callback)
{
try {
return $callback();
} catch (\Stripe\Error\Card $e) {
// Since it's a decline, \Stripe\Error\Card will be caught
$body = $e->getJsonBody();
$err = $body['error'];
$errorMessage = trans('settings.stripe_error_card', ['message' => $err['message']]);
Log::error('Stripe card decline error: '.(string) $e, $e->getJsonBody() ?: []);
} catch (\Stripe\Error\RateLimit $e) {
// Too many requests made to the API too quickly
$errorMessage = trans('settings.stripe_error_rate_limit');
Log::error('Stripe RateLimit error: '.(string) $e, $e->getJsonBody() ?: []);
} catch (\Stripe\Error\InvalidRequest $e) {
// Invalid parameters were supplied to Stripe's API
$errorMessage = trans('settings.stripe_error_invalid_request');
Log::error('Stripe InvalidRequest error: '.(string) $e, $e->getJsonBody() ?: []);
} catch (\Stripe\Error\Authentication $e) {
// Authentication with Stripe's API failed
// (maybe you changed API keys recently)
$errorMessage = trans('settings.stripe_error_authentication');
Log::error('Stripe Authentication error: '.(string) $e, $e->getJsonBody() ?: []);
} catch (\Stripe\Error\ApiConnection $e) {
// Network communication with Stripe failed
$errorMessage = trans('settings.stripe_error_api_connection_error');
Log::error('Stripe ApiConnection error: '.(string) $e, $e->getJsonBody() ?: []);
} catch (\Stripe\Error\Base $e) {
$errorMessage = $e->getMessage();
Log::error('Stripe error: '.(string) $e, $e->getJsonBody() ?: []);
}
throw new StripeException($errorMessage);
}
}
+1
View File
@@ -36,6 +36,7 @@ return [
'secret' => env('STRIPE_SECRET', null),
'webhook' => [
'secret' => env('STRIPE_WEBHOOK_SECRET', null),
'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300),
],
],
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
[]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"/js/manifest.js": "/js/manifest.js?id=01c8731923a46c30aaed",
"/js/app.js": "/js/app.js?id=dd8c96b817caf556399d",
"/js/app.js": "/js/app.js?id=3d68d8e1afeb4fc7a5a0",
"/css/app-ltr.css": "/css/app-ltr.css?id=365aae28d589d0d77ad5",
"/css/app-rtl.css": "/css/app-rtl.css?id=a43b10e7013145aaff1e",
"/css/stripe.css": "/css/stripe.css?id=746c8aaac01c56d3cee1",
+1
View File
@@ -227,6 +227,7 @@ return [
'stripe_error_card' => 'Your card was declined. Decline message is: :message',
'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.',
'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.',
'stripe_error_invalid_request' => 'Invalid parameters. Try again later.',
'stripe_error_authentication' => 'Wrong authentication with Stripe',
'import_title' => 'Import contacts in your account',
@@ -37,11 +37,11 @@
<h3>{{ trans('settings.subscriptions_account_current_plan') }}</h3>
<p>{!! trans('settings.subscriptions_account_current_paid_plan', ['name' => $planInformation['name']]) !!}</p>
<p>{!! trans('settings.subscriptions_account_next_billing', ['date' => $nextBillingDate, 'url' => 'settings/subscriptions/downgrade']) !!}</p>
<p>{{ trans('settings.subscriptions_account_current_paid_plan', ['name' => $planInformation['name']]) }}</p>
<p>{!! trans('settings.subscriptions_account_next_billing', ['date' => $nextBillingDate, 'url' => route('settings.subscriptions.downgrade')]) !!}</p>
{{-- Only display invoices if the subscription exists or existed --}}
@if (auth()->user()->account->hasInvoices())
@if (auth()->user()->account->hasStripeId() && auth()->user()->account->hasInvoices())
<div class="invoices">
<h3>{{ trans('settings.subscriptions_account_invoices') }}</h3>
<ul class="table">
+236
View File
@@ -0,0 +1,236 @@
<?php
namespace Tests\Feature;
use Stripe\Plan;
use Stripe\Token;
use Stripe\Stripe;
use Stripe\Product;
use Stripe\ApiResource;
use Tests\FeatureTestCase;
use Illuminate\Support\Str;
use Laravel\Cashier\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class AccountSubscriptionTest extends FeatureTestCase
{
use DatabaseTransactions;
/**
* @var string
*/
protected static $stripePrefix = 'cashier-test-';
/**
* @var string
*/
protected static $productId;
/**
* @var string
*/
protected static $planId;
public function setUp(): void
{
parent::setUp();
if (! static::$productId) {
$this->markTestSkipped('Set STRIPE_SECRET to run this test.');
} else {
config([
'services.stripe.secret' => env('STRIPE_SECRET'),
'monica.requires_subscription' => true,
'monica.paid_plan_annual_friendly_name' => 'Annual',
'monica.paid_plan_annual_id' => 'annual',
'monica.paid_plan_annual_price' => 4500,
]);
}
}
public static function setUpBeforeClass(): void
{
if (empty(env('STRIPE_SECRET'))) {
return;
}
Stripe::setApiVersion('2019-03-14');
Stripe::setApiKey(getenv('STRIPE_SECRET'));
static::$productId = static::$stripePrefix.'product-1'.Str::random(10);
static::$planId = static::$stripePrefix.'monthly-10-'.Str::random(10);
Product::create([
'id' => static::$productId,
'name' => 'Monica Test Product',
'type' => 'service',
]);
Plan::create([
'id' => static::$planId,
'nickname' => 'Annual',
'currency' => 'USD',
'interval' => 'year',
'billing_scheme' => 'per_unit',
'amount' => 4500,
'product' => static::$productId,
]);
}
public static function tearDownAfterClass(): void
{
parent::tearDownAfterClass();
if (static::$planId) {
static::deleteStripeResource(new Plan(static::$planId));
}
if (static::$productId) {
static::deleteStripeResource(new Product(static::$productId));
}
}
protected static function deleteStripeResource(ApiResource $resource)
{
try {
$resource->delete();
} catch (InvalidRequest $e) {
//
}
}
public function test_it_throw_an_error_on_subscribe()
{
$user = $this->signin();
$this->expectException(\App\Exceptions\StripeException::class);
$user->account->subscribe('xxx', 'annual');
}
public function test_it_get_the_plan_name()
{
$user = $this->signin();
factory(Subscription::class)->create([
'account_id' => $user->account_id,
'name' => 'Annual',
'stripe_plan' => 'annual',
'stripe_id' => 'test',
'quantity' => 1,
]);
$this->assertEquals('Annual', $user->account->getSubscribedPlanName());
}
public function test_it_get_next_billing_date()
{
$user = $this->signin();
factory(Subscription::class)->create([
'account_id' => $user->account_id,
'name' => 'Annual',
'stripe_plan' => 'annual',
'stripe_id' => 'test',
'quantity' => 1,
]);
$this->expectException(\App\Exceptions\StripeException::class);
$user->account->getNextBillingDate();
}
public function test_it_throw_an_error_on_cancel()
{
$user = $this->signin();
factory(Subscription::class)->create([
'account_id' => $user->account_id,
'name' => 'Annual',
'stripe_plan' => 'annual',
'stripe_id' => 'test',
'quantity' => 1,
]);
$this->expectException(\App\Exceptions\StripeException::class);
$user->account->subscriptionCancel();
}
public function test_it_get_subscription_page()
{
$user = $this->signin();
factory(Subscription::class)->create([
'account_id' => $user->account_id,
'name' => 'Annual',
'stripe_plan' => 'annual',
'stripe_id' => 'sub_X',
'quantity' => 1,
]);
$response = $this->get('/settings/subscriptions');
$response->assertSee('You are on the Annual plan. Thanks so much for being a subscriber.');
}
public function test_it_subscribe()
{
$user = $this->signin();
$response = $this->post('/settings/subscriptions/processPayment', [
'stripeToken' => $this->getTestToken(),
'plan' => 'annual',
]);
$response->assertRedirect('/settings/subscriptions/upgrade/success');
}
public function test_it_subscribe_with_error()
{
$user = $this->signin();
$response = $this->post('/settings/subscriptions/processPayment', [
'stripeToken' => 'bad',
'plan' => 'annual',
], [
'HTTP_REFERER' => 'back',
]);
$response->assertRedirect('/back');
}
public function test_it_does_not_subscribe()
{
$user = $this->signin();
try {
$user->account->subscribe($this->getTestTokenError(), 'annual');
} catch (\App\Exceptions\StripeException $e) {
$this->assertEquals('Your card was declined. Decline message is: Your card was declined.', $e->getMessage());
return;
}
$this->fails();
}
protected function getTestToken()
{
return Token::create([
'card' => [
'number' => '4242424242424242',
'exp_month' => 5,
'exp_year' => date('Y') + 1,
'cvc' => '123',
],
])->id;
}
protected function getTestTokenError()
{
return Token::create([
'card' => [
'number' => '4000000000009979',
'exp_month' => 5,
'exp_year' => date('Y') + 1,
'cvc' => '123',
],
])->id;
}
}
-1
View File
@@ -27,7 +27,6 @@ class RegisterTest extends FeatureTestCase
$response = $this->post('/register', $params);
$response->dump();
$response->assertStatus(302);
$response->assertRedirect('/dashboard');