From a32b8d48a9270c6be57298f96c1d244de719be9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20B=C3=A1ez?= Date: Fri, 2 Sep 2016 16:33:18 -0400 Subject: [PATCH] Refactor - it's big --- README.md | 238 +++++------- .../Contracts/PlanSubscriberInterface.php | 8 +- .../Contracts/PlanSubscriptionInterface.php | 15 +- src/LaraPlans/Models/PlanSubscription.php | 340 ++++++------------ .../Models/PlanSubscriptionUsage.php | 3 +- src/LaraPlans/Period.php | 4 +- src/LaraPlans/SubscriptionAbility.php | 123 +++++++ src/LaraPlans/SubscriptionBuilder.php | 112 ++++++ src/LaraPlans/SubscriptionUsageManager.php | 106 ++++++ src/LaraPlans/Traits/PlanSubscriber.php | 66 ++-- .../factories/PlanSubscriptionFactory.php | 1 + ...190033_create_plan_subscriptions_table.php | 8 +- .../Models/PlanSubscriptionTest.php | 193 ++-------- .../Models/PlanSubscriptionUsageTest.php | 29 +- tests/integration/Models/UserTest.php | 70 ---- tests/integration/SubscriptionAbilityTest.php | 53 +++ tests/integration/SubscriptionBuilderTest.php | 47 +++ .../SubscriptionUsageManagerTest.php | 73 ++++ 18 files changed, 816 insertions(+), 673 deletions(-) create mode 100644 src/LaraPlans/SubscriptionAbility.php create mode 100644 src/LaraPlans/SubscriptionBuilder.php create mode 100644 src/LaraPlans/SubscriptionUsageManager.php delete mode 100644 tests/integration/Models/UserTest.php create mode 100644 tests/integration/SubscriptionAbilityTest.php create mode 100644 tests/integration/SubscriptionBuilderTest.php create mode 100644 tests/integration/SubscriptionUsageManagerTest.php diff --git a/README.md b/README.md index faccc73..5995e4e 100644 --- a/README.md +++ b/README.md @@ -5,31 +5,34 @@ SaaS style recurring plans for Laravel 5.2. > Please note: this package doesn't handle payments. - + +- [Considerations](#considerations) - [Installation](#installation) - - [Composer](#composer) - - [Service Provider](#service-provider) - - [Config file and Migrations](#config-file-and-migrations) - - [Traits and Contracts](#traits-and-contracts) + - [Composer](#composer) + - [Service Provider](#service-provider) + - [Config file and Migrations](#config-file-and-migrations) + - [Traits and Contracts](#traits-and-contracts) - [Usage](#usage) - - [Create a Plan](#create-a-plan) - - [Subscribe User to a Plan](#subscribe-user-to-a-plan) - - [Check plan limitations](#check-plan-limitations) - - [Record Feature Usage](#record-feature-usage) - - [Reduce Feature Usage](#reduce-feature-usage) - - [Clear User Subscription Usage](#clear-user-subscription-usage) - - [Check User Subscription Status](#check-user-subscription-status) - - [Renew User Subscription](#renew-user-subscription) - - [Cancel Subscription](#cancel-subscription) - - [Get User Subscription](#get-user-subscription) - - [Get User Subscription Plan](#get-user-subscription-plan) - - [Subscription Model Scopes](#subscription-model-scopes) + - [Create a Plan](#create-a-plan) + - [Creating subscriptions](#creating-subscriptions) + - [Subscription Ability](#subscription-ability) + - [Record Feature Usage](#record-feature-usage) + - [Reduce Feature Usage](#reduce-feature-usage) + - [Clear The Subscription Usage Data](#clear-the-subscription-usage-data) + - [Check Subscription Status](#check-subscription-status) + - [Renew a Subscription](#renew-a-subscription) + - [Cancel a Subscription](#cancel-a-subscription) + - [Scopes](#scopes) - [Models](#models) - [Config File](#config-file) - + +## Considerations + +- Payments are out of scope for this package. +- You may want to extend all of LaraPlans models since it's likely that you will need to override the logic behind some helper methods like `renew()`, `cancel()` etc. E.g.: when cancelling a subscription you may want to also cancel the recurring payment attached. ## Installation @@ -119,197 +122,134 @@ $freePlan->features()->saveMany([ ]); ``` -### Subscribe User to a Plan - -You can subscribe a user to a plan by using the `subscribeToPlan()` function available in the `PlanSubscriber` trait. This will create a new subscription if the user doesn't have one. +### Creating subscriptions -If both plans (current and new plan) have the same billing frequency (e.g., ` interval` and `interval_count`) the subscription will retain the same billing dates. If the plans don't have the same billing frequency, the subscription will have the new plan billing frequency, starting on the day of the change and _the subscription usage data will be cleared_. - -If the new plan have a trial period and it's a new subscription, the trial period will be applied. +You can subscribe a user to a plan by using the `newSubscription()` function available in the `PlanSubscriber` trait. First, retrieve an instance of your subscriber model, which typically will be your user model and an instance of the plan your user is subscribing to. Once you have retrieved the model instance, you may use the `newSubscription` method to create the model's subscription. ```php subscribeToPlan($plan_id)->save(); -``` +$plan = Plan::find(1); -### Check plan limitations +$user->newSubscription('main', $plan)->create(); +``` -The limit is reached when one of this conditions is _true_: +The first argument passed to `newSubscription` method should be the name of the subscription. If your application offer a single subscription, you might call this `main` or `primary`. The second argument is the plan instance your user is subscribing to. -- Feature's _value is not a positive word_ (positive words are configured in the config file). -- Feature's _value is zero_. -- Feature _doesn't have remaining usages_ (i.e., when the user has used all his available uses). + -```php - -use Auth; +### Subscription Ability -$user = Auth::user(); +There's multiple ways to determine the usage and ability of a particular feature in the user subscription, the most common one is `canUse`: -// Check if user has reached the limit in a particular feature in his subscription: -$user->planSubscription->limitReached('listings_per_month'); +The `canUse` method returns `true` or `false` depending on multiple factors: -// Check if a feature is enabled -$user->planSubscription->featureEnabled('title_in_bold'); +- Feature _is enabled_. +- Feature value isn't `0`. +- Or feature has remaining uses available. -// Get feature's value -$user->planSubscription->getFeatureValue('pictures_per_listing'); +```php +$user->subscription('main')->ability()->canUse('listings'); ``` -### Record Feature Usage -```php - All methods share the same signature: e.g. `$user->subscription('main')->ability()->consumed('listings');`. -// Increment usage by 1 -$user->planSubscription->recordUsage('listings_per_month'); -// Increment usage by custom number (perfect when user perform batch actions) -$user->planSubscription->recordUsage('listings_per_month', 3); +### Record Feature Usage -// Set fixed amount -$user->planSubscription->recordUsage('listings_per_month', 5, false); +In order to efectively use the ability methods you will need to keep track of every usage of each feature (or at least those that require it). You may use the `record` method available through the user `subscriptionUsage()` method: +```php +$user->subscriptionUsage('main')->record('listings'); ``` +The `record` method accept 3 parameters: the first one is the feature's code, the second one is the quantity of uses to add (default is `1`), and the third one indicates if the addition should be incremental (default behavior), when disabled the usage will be override by the quantity provided. -### Reduce Feature Usage +E.g.: ```php -subscriptionUsage('main')->record('listings', 2); -// Reduce usage by 1 -$user->planSubscription->reduceUsage('listings_per_month', 1); - -// Reduce usage by custom number -$user->planSubscription->reduceUsage('listings_per_month', 3); +// Override with 9 +$user->subscriptionUsage('main')->record('listings', 9, false); ``` -### Clear User Subscription Usage +### Reduce Feature Usage -You may want to reset all feature's usage data when user renew his subscription, in this case the `clearUsage()` function will help you: +Reducing the feature usage is _almost_ the same as incrementing it. Here we only _substract_ a given quantity (default is `1`) to the actual usage: ```php -planSubscription->clearUsage(); +$user->subscriptionUsage('main')->reduce('listings', 2); ``` -### Check User Subscription Status - -For a subscription to be considered active one of the following must be _true_: - -- Subscription `canceled_at` is `null` or in the future. -- Subscription `trial_end` is in the future. -- Subscription `current_period_end` is in the future. +### Clear The Subscription Usage Data ```php -planSubscription->isActive(); - -// Alternatively you can use the following: -$user->planSubscription->isCanceled(); -$user->planSubscription->isTrialling(); -$user->planSubscription->periodEnded(); - -// Get the subscription status -$user->planSubscription->status; // (active|canceled|ended) +$user->subscriptionUsage('main')->clear(); ``` -### Renew User Subscription +### Check Subscription Status -```php -planSubscription->setNewPeriod()->save(); - -// You may want to clear the usage data: -$user->planSubscription->setNewPeriod()->clearUsage()->save(); +```php +$user->subscribed('main'); ``` -### Cancel Subscription +Alternatively you can use the following methods available in the subscription model: ```php -subscription('main')->active(); +$user->subscription('main')->canceled(); +$user->subscription('main')->ended(); +$user->subscription('main')->onTrial(); +``` -use Auth; +> Canceled subscriptions with an active trial or `ends_at` in the future are considered active. -$user = Auth::user(); +### Renew a Subscription -// Cancel At Period End -$user->planSubscription->cancel(); +To renew a subscription you may use the `renew` method available in the subscription model. This will set a new `ends_at` date based on the selected plan and _will clear the usage data_ of the subscription. -// Cancel Immediately -$user->planSubscription->cancel(true); +```php +$user->subscription('main')->renew(); ``` -### Get User Subscription +_Canceled subscriptions with an ended period can't be renewed._ -```php -planSubscription; - -// Get Subscription details -$user->planSubscription->plan; -$user->planSubscription->status; // (active|canceled|ended) -$user->planSubscription->trial_end; // null|date -$user->planSubscription->current_period_start; // date -$user->planSubscription->current_period_end; // date -$user->planSubscription->canceled_at; // null|date -$user->planSubscription->isActive(); // true|false +```php +$user->subscription('main')->cancel(); ``` -### Get User Subscription Plan +By default the subscription will remain active until the end of the period, you may pass `true` to end the subscription _immediately_: ```php -plan; -// -or- -$user->planSubscription->plan; - -// Get plan details -$user->plan->name; // Pro -$user->plan->slug; // pro; -$user->plan->description; // Pro Features for 9.99/month. -$user->plan->price; // 9.99 -$user->plan->isFree(); // true|false -$user->plan->interval; // month -$user->plan->interval_count; // 1 -$user->plan->sort_order; -$user->plan->trial_period_days; // 15 +$user->subscription('main')->cancel(true); ``` -### Subscription Model Scopes +### Scopes +#### Subscription Model ```php get(); $subscription = PlanSubscription::byUser($user_id)->first(); // Get subscriptions with trial ending in 3 days: -$subscriptions = PlanSubscription::FindEndingTrial(3)->get(); +$subscriptions = PlanSubscription::findEndingTrial(3)->get(); // Get subscriptions with ended trial: -$subscriptions = PlanSubscription::FindEndedTrial()->get(); +$subscriptions = PlanSubscription::findEndedTrial()->get(); // Get subscriptions with period ending in 3 days: -$subscriptions = PlanSubscription::FindEndingPeriod(3)->get(); +$subscriptions = PlanSubscription::findEndingPeriod(3)->get(); // Get subscriptions with ended period: -$subscriptions = PlanSubscription::FindEndedPeriod()->get(); +$subscriptions = PlanSubscription::findEndedPeriod()->get(); ``` ## Models diff --git a/src/LaraPlans/Contracts/PlanSubscriberInterface.php b/src/LaraPlans/Contracts/PlanSubscriberInterface.php index 40dc266..20d3d6e 100644 --- a/src/LaraPlans/Contracts/PlanSubscriberInterface.php +++ b/src/LaraPlans/Contracts/PlanSubscriberInterface.php @@ -4,7 +4,9 @@ interface PlanSubscriberInterface { - public function getPlanAttribute(); - public function planSubscription(); - public function subscribeToPlan($plan); + public function subscription($name = 'default'); + public function subscriptions(); + public function subscribed($subscription = 'default'); + public function newSubscription($name, $plan); + public function subscriptionUsage($subscription = 'default'); } diff --git a/src/LaraPlans/Contracts/PlanSubscriptionInterface.php b/src/LaraPlans/Contracts/PlanSubscriptionInterface.php index 5fe5f4c..ad61a04 100644 --- a/src/LaraPlans/Contracts/PlanSubscriptionInterface.php +++ b/src/LaraPlans/Contracts/PlanSubscriptionInterface.php @@ -8,16 +8,11 @@ public function user(); public function plan(); public function usage(); public function getStatusAttribute(); - public function isActive(); - public function periodEnded(); - public function isTrialling(); - public function isCanceled(); + public function active(); + public function onTrial(); + public function canceled(); + public function ended(); + public function renew(); public function cancel($immediately); public function changePlan($plan); - public function limitReached($feature_code); - public function featureEnabled($feature_code); - public function recordUsage($feature_code, $uses); - public function getFeatureValue($feature_code, $default); - public function setNewPeriod($interval, $interval_count, $start); - public function clearUsage(); } diff --git a/src/LaraPlans/Models/PlanSubscription.php b/src/LaraPlans/Models/PlanSubscription.php index fad1696..8e001fe 100644 --- a/src/LaraPlans/Models/PlanSubscription.php +++ b/src/LaraPlans/Models/PlanSubscription.php @@ -2,12 +2,15 @@ namespace Gerardojbaez\LaraPlans\Models; +use DB; use App; use Carbon\Carbon; +use LogicException; use Gerardojbaez\LaraPlans\Period; -use Gerardojbaez\LaraPlans\Feature; use Illuminate\Database\Eloquent\Model; use Gerardojbaez\LaraPlans\Models\PlanFeature; +use Gerardojbaez\LaraPlans\SubscriptionAbility; +use Gerardojbaez\LaraPlans\SubscriptionUsageManager; use Gerardojbaez\LaraPlans\Traits\BelongsToPlan; use Gerardojbaez\LaraPlans\Contracts\PlanInterface; use Gerardojbaez\LaraPlans\Contracts\PlanSubscriptionInterface; @@ -23,7 +26,6 @@ class PlanSubscription extends Model implements PlanSubscriptionInterface */ const STATUS_ACTIVE = 'active'; const STATUS_CANCELED = 'canceled'; - const STATUS_ENDED = 'ended'; /** * The attributes that are mass assignable. @@ -33,9 +35,10 @@ class PlanSubscription extends Model implements PlanSubscriptionInterface protected $fillable = [ 'user_id', 'plan_id', - 'trial_end', - 'current_period_end', - 'current_period_start', + 'name', + 'trial_ends_at', + 'starts_at', + 'ends_at', 'canceled_at' ]; @@ -45,10 +48,17 @@ class PlanSubscription extends Model implements PlanSubscriptionInterface * @var array */ protected $dates = [ - 'created_at', 'updated_at', 'canceled_at', 'trial_end', - 'current_period_start', 'current_period_end' + 'created_at', 'updated_at', + 'canceled_at', 'trial_ends_at', 'ends_at', 'starts_at' ]; + /** + * Subscription Ability Manager instance. + * + * @var Gerardojbaez\LaraPlans\SubscriptionAbility + */ + protected $ability; + /** * Boot function for using with User Events. * @@ -60,8 +70,8 @@ protected static function boot() static::saving(function($model) { - // Set period if isn't set - if (!$model->current_period_start OR !$model->current_period_start) + // Set period if it wasn't set + if (! $model->ends_at) $model->setNewPeriod(); }); } @@ -83,7 +93,10 @@ function user() */ public function usage() { - return $this->hasMany(config('laraplans.models.plan_subscription_usage'), 'subscription_id'); + return $this->hasMany( + config('laraplans.models.plan_subscription_usage'), + 'subscription_id' + ); } /** @@ -93,14 +106,11 @@ public function usage() */ public function getStatusAttribute() { - if ($this->isActive()) + if ($this->active()) return self::STATUS_ACTIVE; - if ($this->isCanceled()) + if ($this->canceled()) return self::STATUS_CANCELED; - - if ($this->periodEnded()) - return self::STATUS_ENDED; } /** @@ -108,71 +118,63 @@ public function getStatusAttribute() * * @return bool */ - public function isActive() + public function active() { - if ($this->isCanceled()) - return false; - - if ($this->isTrialling()) + if (! $this->ended() OR $this->onTrial()) return true; - if ($this->periodEnded()) - return false; - - return true; + return false; } /** - * Check if subscription period has ended. + * Check if subscription is trialling. * * @return bool */ - public function periodEnded() + public function onTrial() { - if ($this->current_period_end->isToday() OR $this->current_period_end->isPast()) - return true; + if (! is_null($trialEndsAt = $this->trial_ends_at)) + return Carbon::now()->lt(Carbon::instance($trialEndsAt)); return false; } /** - * Check if subscription is trialling. + * Check if subscription is canceled. * * @return bool */ - public function isTrialling() + public function canceled() { - if (!is_null($this->trial_end) AND $this->trial_end->isFuture()) - return true; - - return false; + return ! is_null($this->canceled_at); } /** - * Check if subscription is canceled. + * Check if subscription period has ended. * * @return bool */ - public function isCanceled() + public function ended() { - if (is_null($this->canceled_at)) - return false; + $endsAt = Carbon::instance($this->ends_at); - return ($this->canceled_at->isToday() OR $this->canceled_at->isPast()); + return Carbon::now()->gt($endsAt) OR Carbon::now()->eq($endsAt); } /** * Cancel subscription. * - * @param bool $at_period_end + * @param bool $immediately * @return $this */ public function cancel($immediately = false) { + $this->canceled_at = Carbon::now(); + if ($immediately) - $this->canceled_at = new Carbon; - else - $this->canceled_at = $this->current_period_end; + $this->ends_at = $this->canceled_at; + + $this->save(); return $this; } @@ -199,216 +201,60 @@ public function changePlan($plan) $this->setNewPeriod($plan->interval, $plan->interval_count); // Clear usage data - $this->clearUsage(); + $usageManager = new SubscriptionUsageManager($this); + $usageManager->clear(); } // Attach new plan to subscription $this->plan_id = $plan->id; - // Refresh relations - $this->load('plan'); - - return $this; - } - - /** - * Check whether limit was reached or not. - * - * @param string $feature_code - * @throws \Gerardojbaez\LaraPlans\Exceptions\InvalidPlanFeatureException - * @throws \Gerardojbaez\LaraPlans\Exceptions\FeatureValueFormatIncompatibleException - * @return boolean - */ - public function limitReached($feature_code) - { - // Get features and usage - $feature = $this->getFeatureByCode($feature_code); - - if (!$feature) - throw new InvalidPlanFeatureException($feature_code); - - // Match "booleans" type value - if ($this->featureEnabled($feature_code) === true) - return false; - - // If the feature is zero, let's return true since there's no uses - // available. (useful to disable countable features) - if ($feature->value === '0') - return true; - - // Get feature usage data to check for expiration and - // remaining uses... - $usage = $this->usage->where('code', $feature->code)->first(); - - // Feature has usage record? - if (!$usage) - return false; - - // Usage has expired? - if ($usage->isExpired() === true) - return false; - - // Feature has remaining uses? - if (($feature->value - $usage->used) > 0) - return false; - - return true; - } - - /** - * Check if subscription plan feature is enabled. - * - * @param string $feature_code - * @throws \Gerardojbaez\LaraPlans\Exceptions\InvalidPlanFeatureException - * @return bool - */ - public function featureEnabled($feature_code) - { - $feature = $this->getFeatureByCode($feature_code); - - if (!$feature) - return false; - - // If value is one of the positive words configured then the - // feature is enabled. - if (in_array(strtoupper($feature->value), config('laraplans.positive_words'))) - return true; - - return false; - } - - /** - * Record usage. - * - * This will create or update a usage record. - * - * @param string $feature_code - * @param int $uses - * @return \Gerardojbaez\LaraPlans\Models\PlanSubscriptionUsage - */ - public function recordUsage($feature_code, $uses = 1, $incremental = true) - { - $feature = new Feature($feature_code); - - $usage = $this->usage()->firstOrNew([ - 'code' => $feature_code, - ]); - - if($feature->isReseteable()) - { - // Is 'valid_until' attribute null? - if (is_null($usage->valid_until)) - { - $usage->valid_until = $feature->getResetDate($this->created_at); - } - - // Has expired? - elseif ($usage->isExpired() === true) - { - $usage->valid_until = $feature->getResetDate($usage->valid_until); - $usage->used = 0; - } - } - - $usage->used = ($incremental ? $usage->used + $uses : $uses); - - $usage->save(); - - // Refresh usage records - $this->load('usage'); - - return $usage; - } - - /** - * Reduce usage. - * - * @param string $feature_code - * @param int $uses - * @return mixed - */ - public function reduceUsage($feature_code, $uses = 1) - { - $feature = new Feature($feature_code); - - $usage = $this->usage()->byFeatureCode($feature_code)->first(); - - if (!$usage) - return false; - - $usage->used = max($usage->used - $uses, 0); - - $usage->save(); - - // Refresh usage records - $this->load('usage'); - - return $usage; - } - - /** - * Get feature's value. - * - * Useful when you need to set model attribute - * based on a plan's feature's value. - */ - public function getFeatureValue($feature_code, $default = null) - { - $feature = $this->plan->features->where('code', $feature_code)->first(); - - if (!$feature) - return $default; - - return $feature->value; - } - - /** - * Clear usage data. - * - * @return $this - */ - public function clearUsage() - { - $this->usage()->delete(); - - if ($this->relationLoaded('usage')) - $this->load('usage'); - return $this; } /** - * Set new subscription period. + * Renew subscription period. * * @param string $interval * @param int $interval_count * @param string $start Start date + * @throws \LogicException * @return $this */ - public function setNewPeriod($interval = '', $interval_count = '', $start = '') + public function renew() { - if (empty($interval)) - $interval = $this->plan->interval; + if ($this->ended() AND $this->canceled()) { + throw new LogicException( + 'Unable to renew canceled ended subscription.' + ); + } - if (empty($interval_count)) - $interval_count = $this->plan->interval_count; + $subscription = $this; - $period = new Period($interval, $interval_count, $start); + DB::transaction(function() use ($subscription) { + // Clear usage data + $usageManager = new SubscriptionUsageManager($subscription); + $usageManager->clear(); - $this->current_period_start = $period->getStartDate(); - $this->current_period_end = $period->getEndDate(); + // Renew period + $subscription->setNewPeriod(); + $subscription->canceled_at = null; + $subscription->save(); + }); return $this; } /** - * Get feature from subscription plan. + * Get Subscription Ability instance. * - * @return \Gerardojbaez\LaraPlans\Models\PlanFeature|null + * @return \Gerardojbaez\LaraPlans\SubscriptionAbility */ - protected function getFeatureByCode($code) + public function ability() { - return $this->plan->features->where('code', $code)->first(); + if (is_null($this->ability)) + return new SubscriptionAbility($this); + + return $this->ability; } /** @@ -418,7 +264,7 @@ protected function getFeatureByCode($code) * @param int $user_id * @return \Illuminate\Database\Eloquent\Builder */ - function scopeByUser($query, $user_id) + public function scopeByUser($query, $user_id) { return $query->where('user_id', $user_id); } @@ -430,10 +276,10 @@ function scopeByUser($query, $user_id) */ public function scopeFindEndingTrial($query, $dayRange = 3) { - $from = new Carbon; - $to = (new Carbon)->addDays($dayRange); + $from = Carbon::now(); + $to = Carbon::now()->addDays($dayRange); - $query->whereBetween('trial_end', [$from, $to]); + $query->whereBetween('trial_ends_at', [$from, $to]); } /** @@ -443,7 +289,7 @@ public function scopeFindEndingTrial($query, $dayRange = 3) */ public function scopeFindEndedTrial($query) { - $query->where('trial_end', '<=', date('Y-m-d H:i:s')); + $query->where('trial_ends_at', '<=', date('Y-m-d H:i:s')); } /** @@ -453,10 +299,10 @@ public function scopeFindEndedTrial($query) */ public function scopeFindEndingPeriod($query, $dayRange = 3) { - $from = new Carbon; - $to = (new Carbon)->addDays($dayRange); + $from = Carbon::now(); + $to = Carbon::now()->addDays($dayRange); - $query->whereBetween('current_period_end', [$from, $to]); + $query->whereBetween('ends_at', [$from, $to]); } /** @@ -466,6 +312,30 @@ public function scopeFindEndingPeriod($query, $dayRange = 3) */ public function scopeFindEndedPeriod($query) { - $query->where('current_period_end', '<=', date('Y-m-d H:i:s')); + $query->where('ends_at', '<=', date('Y-m-d H:i:s')); + } + + /** + * Set subscription period. + * + * @param string $interval + * @param int $interval_count + * @param string $start Start date + * @return $this + */ + protected function setNewPeriod($interval = '', $interval_count = '', $start = '') + { + if (empty($interval)) + $interval = $this->plan->interval; + + if (empty($interval_count)) + $interval_count = $this->plan->interval_count; + + $period = new Period($interval, $interval_count, $start); + + $this->starts_at = $period->getStartDate(); + $this->ends_at = $period->getEndDate(); + + return $this; } } diff --git a/src/LaraPlans/Models/PlanSubscriptionUsage.php b/src/LaraPlans/Models/PlanSubscriptionUsage.php index aeae488..2c94c32 100644 --- a/src/LaraPlans/Models/PlanSubscriptionUsage.php +++ b/src/LaraPlans/Models/PlanSubscriptionUsage.php @@ -2,6 +2,7 @@ namespace Gerardojbaez\LaraPlans\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Gerardojbaez\LaraPlans\Contracts\PlanSubscriptionUsageInterface; @@ -69,6 +70,6 @@ public function isExpired() if (is_null($this->valid_until)) return false; - return ($this->valid_until->isFuture() === false); + return Carbon::now()->gt($this->valid_until) OR Carbon::now()->eq($this->valid_until); } } diff --git a/src/LaraPlans/Period.php b/src/LaraPlans/Period.php index 86b9bef..8a9e527 100644 --- a/src/LaraPlans/Period.php +++ b/src/LaraPlans/Period.php @@ -68,12 +68,12 @@ public function __construct($interval = 'month', $count = 1, $start = '') { if (empty($start)) $this->start = new Carbon; - elseif (!$start instanceOf Carbon) + elseif (! $start instanceOf Carbon) $this->start = new Carbon($start); else $this->start = $start; - if (!$this::isValidInterval($interval)) + if (! $this::isValidInterval($interval)) throw new InvalidIntervalException($interval); $this->interval = $interval; diff --git a/src/LaraPlans/SubscriptionAbility.php b/src/LaraPlans/SubscriptionAbility.php new file mode 100644 index 0000000..42e319a --- /dev/null +++ b/src/LaraPlans/SubscriptionAbility.php @@ -0,0 +1,123 @@ +subscription = $subscription; + } + + /** + * Determine if the feature is enabled and has + * available uses. + * + * @param string $feature + * @return boolean + */ + public function canUse($feature) + { + // Get features and usage + $feature_value = $this->value($feature); + + if (is_null($feature_value)) + return false; + + // Match "booleans" type value + if ($this->enabled($feature) === true) + return true; + + // If the feature value is zero, let's return false + // since there's no uses available. (useful to disable + // countable features) + if ($feature_value === '0') + return false; + + // Check for available uses + return $this->remainings($feature) > 0; + } + + /** + * Get how many times the feature has been used. + * + * @param string $feature + * @return int + */ + public function consumed($feature) + { + $usage = $this->subscription->usage->first(function ($key, $value) use ($feature) { + return $value->code === $feature; + }); + + if (is_null($usage) OR $usage->isExpired()) + return 0; + + return $usage->used; + } + + /** + * Get the available uses. + * + * @param string $feature + * @return int + */ + public function remainings($feature) + { + return ($this->value($feature) - $this->consumed($feature)); + } + + /** + * Check if subscription plan feature is enabled. + * + * @param string $feature + * @return bool + */ + public function enabled($feature) + { + $feature_value = $this->value($feature); + + if (is_null($feature_value)) + return false; + + // If value is one of the positive words configured then the + // feature is enabled. + if (in_array(strtoupper($feature_value), config('laraplans.positive_words'))) + return true; + + return false; + } + + /** + * Get feature value. + * + * @param string $feature + * @param mixed $default + * @return mixed + */ + public function value($feature, $default = null) + { + $feature = $this->subscription->plan->features->first(function ($key, $value) use ($feature) { + return $value->code === $feature; + }); + + if (is_null($feature)) + return $default; + + return $feature->value; + } +} \ No newline at end of file diff --git a/src/LaraPlans/SubscriptionBuilder.php b/src/LaraPlans/SubscriptionBuilder.php new file mode 100644 index 0000000..3cfa3a4 --- /dev/null +++ b/src/LaraPlans/SubscriptionBuilder.php @@ -0,0 +1,112 @@ +user = $user; + $this->name = $name; + $this->plan = $plan; + } + + /** + * Specify the trial duration period in days. + * + * @param int $trialDays + * @return $this + */ + public function trialDays($trialDays) + { + $this->trialDays = $trialDays; + + return $this; + } + + /** + * Do not apply trial to the subscription. + * + * @return $this + */ + public function skipTrial() + { + $this->skipTrial = true; + + return $this; + } + + /** + * Create a new subscription. + * + * @param array $options + * @return \Gerardojbaez\LaraPlans\Models\PlanSubscription + */ + public function create(array $attributes = []) + { + $now = Carbon::now(); + + if ($this->skipTrial) { + $trialEndsAt = null; + } elseif ($this->trialDays) { + $trialEndsAt = ($this->trialDays ? $now->addDays($this->trialDays) : null); + } elseif ($this->plan->hasTrial()) { + $trialEndsAt = $now->addDays($this->plan->trial_period_days); + } else { + $trialEndsAt = null; + } + + return $this->user->subscriptions()->create(array_replace([ + 'plan_id' => $this->plan->id, + 'trial_ends_at' => $trialEndsAt, + 'name' => $this->name + ], $attributes)); + } +} \ No newline at end of file diff --git a/src/LaraPlans/SubscriptionUsageManager.php b/src/LaraPlans/SubscriptionUsageManager.php new file mode 100644 index 0000000..e23ecd9 --- /dev/null +++ b/src/LaraPlans/SubscriptionUsageManager.php @@ -0,0 +1,106 @@ +subscription = $subscription; + } + + /** + * Record usage. + * + * This will create or update a usage record. + * + * @param string $feature + * @param int $uses + * @return \Gerardojbaez\LaraPlans\Models\PlanSubscriptionUsage + */ + public function record($feature, $uses = 1, $incremental = true) + { + $feature = new Feature($feature); + + $usage = $this->subscription->usage()->firstOrNew([ + 'code' => $feature->getFeatureCode(), + ]); + + if($feature->isReseteable()) + { + // Set expiration date when the usage record is new + // or doesn't have one. + if (is_null($usage->valid_until)) + { + // Set date from subscription creation date so + // the reset period match the period specified + // by the subscription's plan. + $usage->valid_until = $feature->getResetDate($this->subscription->created_at); + } + + // If the usage record has been expired, let's assign + // a new expiration date and reset the uses to zero. + elseif ($usage->isExpired() === true) + { + $usage->valid_until = $feature->getResetDate($usage->valid_until); + $usage->used = 0; + } + } + + $usage->used = ($incremental ? $usage->used + $uses : $uses); + + $usage->save(); + + return $usage; + } + + /** + * Reduce usage. + * + * @param string $feature + * @param int $uses + * @return \Gerardojbaez\LaraPlans\Models\PlanSubscriptionUsage + */ + public function reduce($feature, $uses = 1) + { + $feature = new Feature($feature); + + $usage = $this->subscription + ->usage() + ->byFeatureCode($feature->getFeatureCode()) + ->first(); + + if (is_null($usage)) + return false; + + $usage->used = max($usage->used - $uses, 0); + + $usage->save(); + + return $usage; + } + + /** + * Clear usage data. + * + * @return $this + */ + public function clear() + { + $this->subscription->usage()->delete(); + + return $this; + } +} \ No newline at end of file diff --git a/src/LaraPlans/Traits/PlanSubscriber.php b/src/LaraPlans/Traits/PlanSubscriber.php index 3a45258..b316aaf 100644 --- a/src/LaraPlans/Traits/PlanSubscriber.php +++ b/src/LaraPlans/Traits/PlanSubscriber.php @@ -4,22 +4,27 @@ use App; use Carbon\Carbon; +use Gerardojbaez\LaraPlans\SubscriptionBuilder; +use Gerardojbaez\LaraPlans\SubscriptionUsageManager; use Gerardojbaez\LaraPlans\Contracts\PlanInterface; use Gerardojbaez\LaraPlans\Contracts\PlanSubscriptionInterface; trait PlanSubscriber { /** - * Get user plan. + * Get a subscription by name. * - * @return \Gerardojbaez\LaraPlans\Models\Plan|null + * @param string $name + * @return \Gerardojbaez\LaraPlans\Models\Subscription|null */ - function getPlanAttribute() + public function subscription($name = 'default') { - if (!$this->planSubscription) - return null; - - return $this->planSubscription->plan; + return $this->subscriptions->sortByDesc(function ($value) { + return $value->created_at->getTimestamp(); + }) + ->first(function ($key, $value) use ($name) { + return $value->name === $name; + }); } /** @@ -27,32 +32,47 @@ function getPlanAttribute() * * @return \Illuminate\Database\Eloquent\Relations\HasOne */ - function planSubscription() + public function subscriptions() { - return $this->hasOne(config('laraplans.models.plan_subscription')); + return $this->hasMany(config('laraplans.models.plan_subscription')); } /** - * Subscribe user to a new plan. + * Check if the user has a given subscription. * - * @var mixed $plan Plan Id or Plan Model Instance - * @var array $extra - * @return \Gerardojbaez\LaraPlans\Models\PlanSubscription + * @param string $subscription + * @return bool */ - function subscribeToPlan($plan) + public function subscribed($subscription = 'default') { - $subscription = App::make(PlanSubscriptionInterface::class) - ->firstOrNew(['user_id' => $this->id]); + $subscription = $this->subscription($subscription); - if (is_numeric($plan)) - $plan = App::make(PlanInterface::class)->find($plan); + if (is_null($subscription)) + return false; - $subscription->changePlan($plan); + return $subscription->active(); + } - // Add trial period if this is a new subscription - if (is_null($subscription->id) AND ($trialDays = $plan->trial_period_days)) - $subscription->trial_end = (new Carbon)->addDays($trialDays); + /** + * Subscribe user to a new plan. + * + * @param string $subscription + * @param mixed $plan + * @return \Gerardojbaez\LaraPlans\Models\PlanSubscription + */ + public function newSubscription($subscription = 'default', $plan) + { + return new SubscriptionBuilder($this, $subscription, $plan); + } - return $subscription; + /** + * Get subscription usage manager instance. + * + * @param string $subscription + * @return \Gerardojbaez\LaraPlans\SubscriptionUsageManager + */ + public function subscriptionUsage($subscription = 'default') + { + return new SubscriptionUsageManager($this->subscription($subscription)); } } diff --git a/src/database/factories/PlanSubscriptionFactory.php b/src/database/factories/PlanSubscriptionFactory.php index ec63b72..c3dbc3d 100644 --- a/src/database/factories/PlanSubscriptionFactory.php +++ b/src/database/factories/PlanSubscriptionFactory.php @@ -8,5 +8,6 @@ return [ 'user_id' => factory(User::class)->create()->id, 'plan_id' => factory(Plan::class)->create()->id, + 'name' => $faker->word ]; }); \ No newline at end of file diff --git a/src/database/migrations/2016_08_15_190033_create_plan_subscriptions_table.php b/src/database/migrations/2016_08_15_190033_create_plan_subscriptions_table.php index 5e82b6a..2999109 100644 --- a/src/database/migrations/2016_08_15_190033_create_plan_subscriptions_table.php +++ b/src/database/migrations/2016_08_15_190033_create_plan_subscriptions_table.php @@ -17,13 +17,13 @@ public function up() $table->increments('id'); $table->integer('user_id')->unsigned(); $table->integer('plan_id')->unsigned(); - $table->timestamp('trial_end')->nullable(); - $table->timestamp('current_period_end')->nullable(); - $table->timestamp('current_period_start')->nullable(); + $table->string('name'); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('starts_at')->nullable(); + $table->timestamp('ends_at')->nullable(); $table->timestamp('canceled_at')->nullable(); $table->timestamps(); - $table->unique(['user_id', 'plan_id']); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('plan_id')->references('id')->on('plans')->onDelete('cascade'); }); diff --git a/tests/integration/Models/PlanSubscriptionTest.php b/tests/integration/Models/PlanSubscriptionTest.php index e1ac525..430227e 100644 --- a/tests/integration/Models/PlanSubscriptionTest.php +++ b/tests/integration/Models/PlanSubscriptionTest.php @@ -60,9 +60,9 @@ public function setUp() 'password' => '123' ]); - $this->user->subscribeToPlan($this->plan)->save(); + $this->user->newSubscription('main', $this->plan)->create(); - $this->subscription = $this->user->planSubscription; + $this->subscription = $this->user->subscription('main'); } /** @@ -84,7 +84,7 @@ public function it_can_get_subscription_user() */ public function it_is_active() { - $this->assertTrue($this->subscription->isActive()); + $this->assertTrue($this->subscription->active()); $this->assertEquals(PlanSubscription::STATUS_ACTIVE, $this->subscription->status); } @@ -98,14 +98,17 @@ public function it_is_canceled() { // Cancel subscription at period end... $this->subscription->cancel(); + $this->subscription->trial_ends_at = null; - $this->assertFalse($this->subscription->isCanceled()); + $this->assertTrue($this->subscription->canceled()); + $this->assertTrue($this->subscription->active()); $this->assertEquals(PlanSubscription::STATUS_ACTIVE, $this->subscription->status); // Cancel subscription immediately... $this->subscription->cancel(true); - $this->assertTrue($this->subscription->isCanceled()); + $this->assertTrue($this->subscription->canceled()); + $this->assertFalse($this->subscription->active()); $this->assertEquals(PlanSubscription::STATUS_CANCELED, $this->subscription->status); } @@ -117,158 +120,40 @@ public function it_is_canceled() */ public function it_is_trialling() { - $this->subscription->trial_end = (new Carbon)->subDay(); - $this->subscription->setNewPeriod('month', 1, (new Carbon)->subMonths(2)); - $this->assertFalse($this->subscription->isActive()); - $this->assertEquals(PlanSubscription::STATUS_ENDED, $this->subscription->status); - - $this->subscription->trial_end = $this->subscription->trial_end->addDays(2); - $this->assertTrue($this->subscription->isActive()); + // Test if subscription is active after applying a trial. + $this->subscription->trial_ends_at = $this->subscription->trial_ends_at->addDays(2); + $this->assertTrue($this->subscription->active()); $this->assertEquals(PlanSubscription::STATUS_ACTIVE, $this->subscription->status); - } - - /** - * Can check if subscription has ended. - * - * @test - * @return void - */ - public function period_ended() - { - $this->subscription->trial_end = null; - $this->subscription->setNewPeriod('month', 1, (new Carbon)->subMonths(2)); - - $this->assertFalse($this->subscription->isActive()); - $this->assertEquals(PlanSubscription::STATUS_ENDED, $this->subscription->status); - } - - /** - * Can record feature usage. - * - * @test - * @return void - */ - public function it_can_record_feature_usage() - { - $usage = $this->subscription->recordUsage('listings_per_month'); - $this->assertInstanceOf(PlanSubscriptionUsage::class, $usage); - $this->assertEquals(1, $usage->used); - - // Record fixed amount (not incremental) - $usage = $this->subscription->recordUsage('listings_per_month', 2, false); - $this->assertInstanceOf(PlanSubscriptionUsage::class, $usage); - $this->assertEquals(2, $usage->used); - } - - /** - * Can record reduce usage. - * - * @test - * @return void - */ - public function it_can_reduce_feature_usage() - { - // Can't reduce unrecorded usage... - $usage = $this->subscription->reduceUsage('listings_per_month'); - $this->assertFalse($usage); - - // Record usage - $usage = $this->subscription->recordUsage('listings_per_month', 5); - - // Reduce - $usage = $this->subscription->reduceUsage('listings_per_month', 3); - - $this->assertInstanceOf(PlanSubscriptionUsage::class, $usage); - $this->assertEquals(2, $usage->used); - } - - /** - * Can get feature value. - * - * @test - * @return void - */ - public function it_can_get_feature_value() - { - $this->assertEquals('N', $this->subscription->getFeatureValue('listing_title_bold')); - $this->assertEquals(30, $this->subscription->getFeatureValue('listing_duration_days')); - } - - /** - * Can check if a particular feature is enabled. - * - * @test - * @return void - */ - public function it_can_check_if_a_feature_is_enabled() - { - $this->assertFalse($this->subscription->featureEnabled('listing_title_bold')); - } - - /** - * Can check if feature limit was reached. - * - * @test - * @return void - */ - public function it_can_check_if_feature_limit_was_reached() - { - // First, let's test non reached limits... - $this->assertFalse($this->subscription->limitReached('listings_per_month')); - $this->assertFalse($this->subscription->limitReached('listing_duration_days')); - $this->assertFalse($this->subscription->limitReached('listing_title_bold')); - - // Now let's update the usage records to reflect "limit reached" beheavor. - $this->subscription->recordUsage('listings_per_month', 50); - - $this->assertTrue($this->subscription->limitReached('listings_per_month')); - } - - /** - * Can clear usage data. - * - * @test - * @return void - */ - public function it_can_clear_usage_data() - { - $this->subscription->recordUsage('listings_per_month', 2); - - $this->assertEquals(1, $this->subscription->usage->count()); - - $this->subscription->clearUsage(); - - $this->assertEquals(0, $this->subscription->usage->count()); + // Test if subscription is inactive after removing the trial. + $this->subscription->trial_ends_at = Carbon::now()->subDay(); + $this->subscription->cancel(true); + $this->assertFalse($this->subscription->active()); } /** - * Can set new period. + * Can be renewed. * * @test * @return void */ - public function it_can_set_new_period() + public function it_can_be_renewed() { - // Create a subscription that with an ended period... + // Create a subscription with an ended period... $subscription = factory(PlanSubscription::class)->create([ 'plan_id' => factory(Plan::class)->create([ 'interval' => 'month' ])->id, - 'trial_end' => (new Carbon)->subMonth(), - 'current_period_start' => (new Carbon)->subMonths(2), - 'current_period_end' => (new Carbon)->subMonth(), + 'trial_ends_at' => Carbon::now()->subMonth(), + 'ends_at' => Carbon::now()->subMonth(), ]); - $this->assertFalse($subscription->isActive()); - - $subscription->setNewPeriod(); + $this->assertFalse($subscription->active()); - $expected = new Period('month', 1, $subscription->current_period_start); + $subscription->renew(); - $this->assertTrue($subscription->isActive()); - $this->assertEquals($expected->getStartDate(), $subscription->current_period_start); - $this->assertEquals($expected->getEndDate(), $subscription->current_period_end); + $this->assertTrue($subscription->active()); + $this->assertEquals(Carbon::now()->addMonth(), $subscription->ends_at); } /** @@ -282,12 +167,12 @@ public function it_can_find_subscriptions_with_ending_trial() // For "control", these subscription shouldn't be // included in the result... factory(PlanSubscription::class, 10)->create([ - 'trial_end' => (new Carbon)->addDays(10) // End in ten days... + 'trial_ends_at' => Carbon::now()->addDays(10) // End in ten days... ]); // These are the results that should be returned... factory(PlanSubscription::class, 5)->create([ - 'trial_end' => (new Carbon)->addDays(3), // Ended a day ago... + 'trial_ends_at' => Carbon::now()->addDays(3), // Ended a day ago... ]); $result = PlanSubscription::FindEndingTrial(3)->get(); @@ -306,12 +191,12 @@ public function it_can_find_subscriptions_with_ended_trial() // For "control", these subscription shouldn't be // included in the result... factory(PlanSubscription::class, 10)->create([ - 'trial_end' => (new Carbon)->addDays(2) // End in two days... + 'trial_ends_at' => Carbon::now()->addDays(2) // End in two days... ]); // These are the results that should be returned... factory(PlanSubscription::class, 5)->create([ - 'trial_end' => (new Carbon)->subDay(), // Ended a day ago... + 'trial_ends_at' => Carbon::now()->subDay(), // Ended a day ago... ]); $result = PlanSubscription::FindEndedTrial()->get(); @@ -330,14 +215,12 @@ public function it_can_find_subscriptions_with_ending_period() // For "control", these subscription shouldn't be // included in the result... factory(PlanSubscription::class, 10)->create([ - 'current_period_start' => (new Carbon)->subMonth()->addDays(10), - 'current_period_end' => (new Carbon)->addDays(10) // End in ten days... + 'ends_at' => Carbon::now()->addDays(10) // End in ten days... ]); // These are the results that should be returned... factory(PlanSubscription::class, 5)->create([ - 'current_period_start' => (new Carbon)->subMonth()->addDays(3), - 'current_period_end' => (new Carbon)->addDays(3), // Ended a day ago... + 'ends_at' => Carbon::now()->addDays(3), // Ended a day ago... ]); $result = PlanSubscription::FindEndingPeriod(3)->get(); @@ -356,14 +239,12 @@ public function it_can_find_subscriptions_with_ended_period() // For "control", these subscription shouldn't be // included in the result... factory(PlanSubscription::class, 10)->create([ - 'current_period_start' => (new Carbon)->subMonth()->addDays(2), - 'current_period_end' => (new Carbon)->addDays(2) // End in two days... + 'ends_at' => Carbon::now()->addDays(2) // End in two days... ]); // These are the results that should be returned... factory(PlanSubscription::class, 5)->create([ - 'current_period_start' => (new Carbon)->subMonth()->addDay(), - 'current_period_end' => (new Carbon)->subDay(), // Ended a day ago... + 'ends_at' => Carbon::now()->subDay(), // Ended a day ago... ]); $result = PlanSubscription::FindEndedPeriod()->get(); @@ -397,10 +278,9 @@ public function it_can_change_plan() $this->subscription->changePlan($newPlan)->save(); // Plan was changed? - $this->assertEquals('Business', $this->subscription->plan->name); + $this->assertEquals('Business', $this->subscription->fresh()->plan->name); - // Let's check if the subscription period was set (i.e., current_period_start - // and current_period_end) + // Let's check if the subscription period was set $period = new Period($newPlan->interval, $newPlan->interval_count); // Expected dates @@ -408,11 +288,10 @@ public function it_can_change_plan() $expectedPeriodEndDate = $period->getEndDate(); // Finaly test period - $this->assertEquals($expectedPeriodStartDate, $this->subscription->current_period_start); - $this->assertEquals($expectedPeriodEndDate, $this->subscription->current_period_end); + $this->assertEquals($expectedPeriodEndDate, $this->subscription->ends_at); // This assertion will make sure that the subscription is now using // the new plan features... - $this->assertEquals('Y', $this->subscription->getFeatureValue('listing_title_bold')); + $this->assertEquals('Y', $this->subscription->fresh()->ability()->value('listing_title_bold')); } } diff --git a/tests/integration/Models/PlanSubscriptionUsageTest.php b/tests/integration/Models/PlanSubscriptionUsageTest.php index 4a3ff5a..135b785 100644 --- a/tests/integration/Models/PlanSubscriptionUsageTest.php +++ b/tests/integration/Models/PlanSubscriptionUsageTest.php @@ -11,12 +11,14 @@ class PlanSubscriptionUsageTest extends TestCase { - protected $subscription; - - public function setUP() + /** + * Check if usage has expired. + * + * @test + * @return void + */ + public function it_can_check_if_usage_has_expired() { - parent::setUp(); - Config::set('laraplans.features', [ 'listings_per_month' => [ 'reseteable_interval' => 'month', @@ -40,24 +42,13 @@ public function setUP() 'password' => '123' ]); - $user->subscribeToPlan($plan)->save(); + $user->newSubscription('main', $plan)->create(); - $this->subscription = $user->planSubscription; - } - - /** - * Check if usage has expired. - * - * @test - * @return void - */ - public function it_can_check_if_usage_has_expired() - { - $usage = $this->subscription->recordUsage('listings_per_month'); + $usage = $user->subscriptionUsage('main')->record('listings_per_month'); $this->assertFalse($usage->isExpired()); - $usage->valid_until = (new Carbon)->subDay(); // date is in the past by 1 day... + $usage->valid_until = Carbon::now()->subDay(); // date is in the past by 1 day... $usage->save(); diff --git a/tests/integration/Models/UserTest.php b/tests/integration/Models/UserTest.php deleted file mode 100644 index bc47ffe..0000000 --- a/tests/integration/Models/UserTest.php +++ /dev/null @@ -1,70 +0,0 @@ -plan = Plan::create([ - 'name' => 'Pro', - 'description' => 'Pro plan', - 'price' => 9.99, - 'interval' => 'month', - 'interval_count' => 1, - 'trial_period_days' => 15, - 'sort_order' => 1, - ]); - - $this->plan->features()->saveMany([ - new PlanFeature(['code' => 'listings_per_month', 'value' => 50, 'sort_order' => 1]), - new PlanFeature(['code' => 'pictures_per_listing', 'value' => 10, 'sort_order' => 5]), - new PlanFeature(['code' => 'listing_duration_days', 'value' => 30, 'sort_order' => 10]), - ]); - - $this->user = User::create([ - 'email' => 'test@example.org', - 'name' => 'Test user', - 'password' => '123' - ]); - } - - /** - * Can subscribe user to a plan. - * - * @test - * @return void - */ - public function it_can_subscribe_user_to_a_plan() - { - // Subscribe user to plan - $saved = $this->user->subscribeToPlan($this->plan->id)->save(); - - $this->assertTrue($saved); - $this->assertEquals('Pro', $this->user->plan->name); - - return $this->user; - } - - /** - * New subscription must have a trial end date when plan has trial defined. - * - * @depends it_can_subscribe_user_to_a_plan - * @test - * @return void - */ - public function new_subscription_has_trial_when_trial_is_defined($user) - { - $this->assertTrue(is_null($user->planSubscription->trial_end) === false); - } -} diff --git a/tests/integration/SubscriptionAbilityTest.php b/tests/integration/SubscriptionAbilityTest.php new file mode 100644 index 0000000..4314c13 --- /dev/null +++ b/tests/integration/SubscriptionAbilityTest.php @@ -0,0 +1,53 @@ + 'gerardo@email.dev', + 'name' => 'Gerardo', + 'password' => 'password' + ]); + + $plan = Plan::create([ + 'name' => 'Pro', + 'description' => 'Pro plan', + 'price' => 9.99, + 'interval' => 'month', + 'interval_count' => 1, + 'trial_period_days' => 15, + ]); + + $plan->features()->saveMany([ + new PlanFeature(['code' => 'listings', 'value' => 50]), + new PlanFeature(['code' => 'pictures_per_listing', 'value' => 10]), + new PlanFeature(['code' => 'listing_title_bold', 'value' => 'N']), + new PlanFeature(['code' => 'listing_video', 'value' => 'Y']), + ]); + + // Create Subscription + $user->newSubscription('main', $plan)->create(); + + $this->assertTrue($user->subscription('main')->ability()->canUse('listings')); + $this->assertEquals(50, $user->subscription('main')->ability()->remainings('listings')); + $this->assertEquals(0, $user->subscription('main')->ability()->consumed('listings')); + $this->assertEquals(10, $user->subscription('main')->ability()->value('pictures_per_listing')); + $this->assertEquals('N', $user->subscription('main')->ability()->value('listing_title_bold')); + $this->assertFalse($user->subscription('main')->ability()->enabled('listing_title_bold')); + $this->assertTrue($user->subscription('main')->ability()->enabled('listing_video')); + } +} diff --git a/tests/integration/SubscriptionBuilderTest.php b/tests/integration/SubscriptionBuilderTest.php new file mode 100644 index 0000000..4ac96d6 --- /dev/null +++ b/tests/integration/SubscriptionBuilderTest.php @@ -0,0 +1,47 @@ + 'gerardo@email.dev', + 'name' => 'Gerardo', + 'password' => 'password' + ]); + + $plan = Plan::create([ + 'name' => 'Pro', + 'description' => 'Pro plan', + 'price' => 9.99, + 'interval' => 'month', + 'interval_count' => 1, + 'trial_period_days' => 15, + ]); + + // Create Subscription + $user->newSubscription('main', $plan)->create(); + $user->newSubscription('second', $plan)->create([ + 'name' => 'override' // test if data can be override + ]); + + $this->assertEquals(2, $user->subscriptions->count()); + $this->assertEquals('main', $user->subscription('main')->name); + $this->assertEquals('override', $user->subscription('override')->name); + $this->assertTrue($user->subscribed('main')); + $this->assertTrue($user->subscribed('override')); + } +} diff --git a/tests/integration/SubscriptionUsageManagerTest.php b/tests/integration/SubscriptionUsageManagerTest.php new file mode 100644 index 0000000..0b84f8c --- /dev/null +++ b/tests/integration/SubscriptionUsageManagerTest.php @@ -0,0 +1,73 @@ + 'gerardo@email.dev', + 'name' => 'Gerardo', + 'password' => 'password' + ]); + + $plan = Plan::create([ + 'name' => 'Pro', + 'description' => 'Pro plan', + 'price' => 9.99, + 'interval' => 'month', + 'interval_count' => 1, + 'trial_period_days' => 15, + ]); + + $plan->features()->saveMany([ + new PlanFeature(['code' => 'SAMPLE_SIMPLE_FEATURE', 'value' => 5]) + ]); + + $user->newSubscription('main', $plan)->create(); + + // Record usage + $usage = $user->subscriptionUsage('main')->record('SAMPLE_SIMPLE_FEATURE')->fresh(); + + $this->assertEquals(1, $user->subscriptions->count()); + $this->assertInstanceOf(PlanSubscriptionUsage::class, $usage); + $this->assertEquals(1, $usage->used); + $this->assertEquals(4, $user->fresh()->subscription('main')->ability()->remainings('SAMPLE_SIMPLE_FEATURE')); + + // Record usage by custom incremental amount + $usage = $user->subscriptionUsage('main')->record('SAMPLE_SIMPLE_FEATURE', 2)->fresh(); + $this->assertInstanceOf(PlanSubscriptionUsage::class, $usage); + $this->assertEquals(3, $usage->used); + $this->assertEquals(2, $user->fresh()->subscription('main')->ability()->remainings('SAMPLE_SIMPLE_FEATURE')); + + // Record usage by fixed amount + $usage = $user->subscriptionUsage('main')->record('SAMPLE_SIMPLE_FEATURE', 2, false)->fresh(); + $this->assertInstanceOf(PlanSubscriptionUsage::class, $usage); + $this->assertEquals(2, $usage->used); + $this->assertEquals(3, $user->fresh()->subscription('main')->ability()->remainings('SAMPLE_SIMPLE_FEATURE')); + + // Reduce uses + $usage = $user->subscriptionUsage('main')->reduce('SAMPLE_SIMPLE_FEATURE')->fresh(); + $this->assertEquals(1, $usage->used); + $this->assertEquals(4, $user->fresh()->subscription('main')->ability()->remainings('SAMPLE_SIMPLE_FEATURE')); + + // Clear usage + $user->subscriptionUsage('main')->clear(); + $this->assertEquals(0, $user->subscription('main')->usage()->count()); + $this->assertEquals(5, $user->fresh()->subscription('main')->ability()->remainings('SAMPLE_SIMPLE_FEATURE')); + } +}