Skip to content

Commit

Permalink
Merge pull request #2 from laravelcm/add-subscription-test
Browse files Browse the repository at this point in the history
✅ Add subscription unit test
  • Loading branch information
mckenziearts authored Oct 2, 2023
2 parents b36df3b + f52902a commit 36e30e4
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 42 deletions.
62 changes: 41 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,29 @@ That's it, we only have to use that trait in our User model! Now your users may
### Create a Plan

```php
$plan = app('rinvex.subscriptions.plan')->create([
use Laravelcm\Subscriptions\Models\Plan;
use Laravelcm\Subscriptions\Models\Feature;
use Laravelcm\Subscriptions\Interval;
$plan = Plan::create([
'name' => 'Pro',
'description' => 'Pro plan',
'price' => 9.99,
'signup_fee' => 1.99,
'invoice_period' => 1,
'invoice_interval' => 'month',
'invoice_interval' => Interval::MONTH->value,
'trial_period' => 15,
'trial_interval' => 'day',
'trial_interval' => Interval::DAY->value,
'sort_order' => 1,
'currency' => 'USD',
]);
// Create multiple plan features at once
$plan->features()->saveMany([
new PlanFeature(['name' => 'listings', 'value' => 50, 'sort_order' => 1]),
new PlanFeature(['name' => 'pictures_per_listing', 'value' => 10, 'sort_order' => 5]),
new PlanFeature(['name' => 'listing_duration_days', 'value' => 30, 'sort_order' => 10, 'resettable_period' => 1, 'resettable_interval' => 'month']),
new PlanFeature(['name' => 'listing_title_bold', 'value' => 'Y', 'sort_order' => 15])
new Feature(['name' => 'listings', 'value' => 50, 'sort_order' => 1]),
new Feature(['name' => 'pictures_per_listing', 'value' => 10, 'sort_order' => 5]),
new Feature(['name' => 'listing_duration_days', 'value' => 30, 'sort_order' => 10, 'resettable_period' => 1, 'resettable_interval' => 'month']),
new Feature(['name' => 'listing_title_bold', 'value' => 'Y', 'sort_order' => 15])
]);
```

Expand All @@ -84,7 +88,9 @@ $plan->features()->saveMany([
You can query the plan for further details, using the intuitive API as follows:

```php
$plan = app('rinvex.subscriptions.plan')->find(1);
use Laravelcm\Subscriptions\Models\Plan;
$plan = Plan::find(1);
// Get all plan features
$plan->features;
Expand All @@ -109,23 +115,29 @@ Both `$plan->features` and `$plan->planSubscriptions` are collections, driven fr
Say you want to show the value of the feature _pictures_per_listing_ from above. You can do so in many ways:
```php
use Laravelcm\Subscriptions\Models\Feature;
use Laravelcm\Subscriptions\Models\Subscription;
// Use the plan instance to get feature's value
$amountOfPictures = $plan->getFeatureBySlug('pictures_per_listing')->value;
// Query the feature itself directly
$amountOfPictures = app('rinvex.subscriptions.plan_feature')->where('slug', 'pictures_per_listing')->first()->value;
$amountOfPictures = Feature::where('slug', 'pictures_per_listing')->first()->value;
// Get feature value through the subscription instance
$amountOfPictures = app('rinvex.subscriptions.plan_subscription')->find(1)->getFeatureValue('pictures_per_listing');
$amountOfPictures = Subscription::find(1)->getFeatureValue('pictures_per_listing');
```
### Create a Subscription
You can subscribe a user to a plan by using the `newSubscription()` function available in the `HasPlanSubscriptions` 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
use Laravelcm\Subscriptions\Models\Plan;
use App\Models\User;
$user = User::find(1);
$plan = app('rinvex.subscriptions.plan')->find(1);
$plan = Plan::find(1);
$user->newPlanSubscription('main', $plan);
```
Expand All @@ -137,8 +149,11 @@ The first argument passed to `newSubscription` method should be the title of the
You can change subscription plan easily as follows:
```php
$plan = app('rinvex.subscriptions.plan')->find(2);
$subscription = app('rinvex.subscriptions.plan_subscription')->find(1);
use Laravelcm\Subscriptions\Models\Plan;
use Laravelcm\Subscriptions\Models\Subscription;
$plan = Plan::find(2);
$subscription = Subscription::find(1);
// Change subscription plan
$subscription->changePlan($plan);
Expand All @@ -151,8 +166,10 @@ If both plans (current and new plan) have the same billing frequency (e.g., `inv
Plan features are great for fine-tuning subscriptions, you can top-up certain feature for X times of usage, so users may then use it only for that amount. Features also have the ability to be resettable and then it's usage could be expired too. See the following examples:
```php
use Laravelcm\Subscriptions\Models\Feature;
// Find plan feature
$feature = app('rinvex.subscriptions.plan_feature')->where('name', 'listing_duration_days')->first();
$feature = Feature::where('name', 'listing_duration_days')->first();
// Get feature reset date
$feature->getResetDate(new \Carbon\Carbon());
Expand Down Expand Up @@ -263,24 +280,27 @@ $user->planSubscription('main')->cancel(true);
#### Subscription Model
```php
use Laravelcm\Subscriptions\Models\Subscription;
use App\Models\User;
// Get subscriptions by plan
$subscriptions = app('rinvex.subscriptions.plan_subscription')->byPlanId($plan_id)->get();
$subscriptions = Subscription::byPlanId($plan_id)->get();
// Get bookings of the given user
$user = \App\Models\User::find(1);
$bookingsOfSubscriber = app('rinvex.subscriptions.plan_subscription')->ofSubscriber($user)->get();
$user = User::find(1);
$bookingsOfSubscriber = Subscription::ofSubscriber($user)->get();
// Get subscriptions with trial ending in 3 days
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndingTrial(3)->get();
$subscriptions = Subscription::findEndingTrial(3)->get();
// Get subscriptions with ended trial
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndedTrial()->get();
$subscriptions = Subscription::findEndedTrial()->get();
// Get subscriptions with period ending in 3 days
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndingPeriod(3)->get();
$subscriptions = Subscription::findEndingPeriod(3)->get();
// Get subscriptions with ended period
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndedPeriod()->get();
$subscriptions = Subscription::findEndedPeriod()->get();
```
### Models
Expand Down
21 changes: 19 additions & 2 deletions config/laravel-subscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,32 @@

return [

// Subscriptions Database Tables
/*
|--------------------------------------------------------------------------
| Subscription Tables
|--------------------------------------------------------------------------
|
|
*/

'tables' => [
'plans' => 'plans',
'features' => 'features',
'subscriptions' => 'subscriptions',
'subscription_usage' => 'subscription_usage',
],

// Subscriptions Models
/*
|--------------------------------------------------------------------------
| Subscription Models
|--------------------------------------------------------------------------
|
| Models used to manage subscriptions. You can replace to use your own models,
| but make sure that you have the same functionalities or that your models
| extend from each model that you are going to replace.
|
*/

'models' => [
'plan' => Plan::class,
'feature' => Feature::class,
Expand Down
7 changes: 4 additions & 3 deletions database/migrations/2020_01_01_000001_create_plans_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
use Laravelcm\Subscriptions\Interval;

return new class () extends Migration {
public function up(): void
Expand All @@ -20,11 +21,11 @@ public function up(): void
$table->decimal('signup_fee')->default('0.00');
$table->string('currency', 3);
$table->unsignedSmallInteger('trial_period')->default(0);
$table->string('trial_interval')->default('day');
$table->string('trial_interval')->default(Interval::DAY->value);
$table->unsignedSmallInteger('invoice_period')->default(0);
$table->string('invoice_interval')->default('month');
$table->string('invoice_interval')->default(Interval::MONTH->value);
$table->unsignedSmallInteger('grace_period')->default(0);
$table->string('grace_interval')->default('day');
$table->string('grace_interval')->default(Interval::DAY->value);
$table->unsignedTinyInteger('prorate_day')->nullable();
$table->unsignedTinyInteger('prorate_period')->nullable();
$table->unsignedTinyInteger('prorate_extend_due')->nullable();
Expand Down
File renamed without changes.
14 changes: 14 additions & 0 deletions src/Interval.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Laravelcm\Subscriptions;

enum Interval: string
{
case YEAR = 'year';

case MONTH = 'month';

case DAY = 'day';
}
20 changes: 12 additions & 8 deletions src/Models/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public function changePlan(Plan $plan): self
{
// If plans does not have the same billing frequency
// (e.g., invoice_interval and invoice_period) we will update
// the billing dates starting today, and sice we are basically creating
// the billing dates starting today, and since we are basically creating
// a new billing cycle, the usage data will be cleared.
if ($this->plan->invoice_interval !== $plan->invoice_interval || $this->plan->invoice_period !== $plan->invoice_period) {
$this->setNewPeriod($plan->invoice_interval, $plan->invoice_period);
Expand Down Expand Up @@ -321,7 +321,7 @@ public function scopeFindActive(Builder $builder): Builder
*
* @return $this
*/
protected function setNewPeriod(string $invoice_interval = '', ?int $invoice_period = null, Carbon $start = null): self
protected function setNewPeriod(string $invoice_interval = '', int $invoice_period = null, Carbon $start = null): self
{
if (empty($invoice_interval)) {
$invoice_interval = $this->plan->invoice_interval;
Expand All @@ -331,7 +331,11 @@ protected function setNewPeriod(string $invoice_interval = '', ?int $invoice_per
$invoice_period = $this->plan->invoice_period;
}

$period = new Period($invoice_interval, $invoice_period, $start ?? Carbon::now());
$period = new Period(
interval: $invoice_interval,
count: $invoice_period,
start: $start ?? Carbon::now()
);

$this->starts_at = $period->getStartDate();
$this->ends_at = $period->getEndDate();
Expand All @@ -350,7 +354,7 @@ public function recordFeatureUsage(string $featureSlug, int $uses = 1, bool $inc

if ($feature->resettable_period) {
// Set expiration date when the usage record is new or doesn't have one.
if (null === $usage->valid_until) {
if ($usage->valid_until === null) {
// 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->created_at);
Expand All @@ -362,7 +366,7 @@ public function recordFeatureUsage(string $featureSlug, int $uses = 1, bool $inc
}
}

$usage->used = ($incremental ? $usage->used + $uses : $uses);
$usage->used = $incremental ? $usage->used + $uses : $uses;

$usage->save();

Expand All @@ -373,7 +377,7 @@ public function reduceFeatureUsage(string $featureSlug, int $uses = 1): ?Subscri
{
$usage = $this->usage()->byFeatureSlug($featureSlug)->first();

if (null === $usage) {
if ($usage === null) {
return null;
}

Expand All @@ -396,13 +400,13 @@ public function canUseFeature(string $featureSlug): bool
$featureValue = $this->getFeatureValue($featureSlug);
$usage = $this->usage()->byFeatureSlug($featureSlug)->first();

if ('true' === $featureValue) {
if ($featureValue === '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 ( ! $usage || $usage->expired() || null === $featureValue || '0' === $featureValue || 'false' === $featureValue) {
if ( ! $usage || $usage->expired() || $featureValue === null || $featureValue === '0' || $featureValue === 'false') {
return false;
}

Expand Down
14 changes: 11 additions & 3 deletions src/Traits/HasPlanSubscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function subscribedPlans(): Collection
->pluck('plan_id')
->unique();

return app('laravelcm.subscriptions.models.plan')->whereIn('id', $planIds)->get();
return tap(new (config('laravel-subscriptions.models.plan')))->whereIn('id', $planIds)->get();
}

public function subscribedTo(int $planId): bool
Expand All @@ -66,8 +66,16 @@ public function subscribedTo(int $planId): bool

public function newPlanSubscription(string $subscription, Plan $plan, ?Carbon $startDate = null): Subscription
{
$trial = new Period($plan->trial_interval, $plan->trial_period, $startDate ?? Carbon::now());
$period = new Period($plan->invoice_interval, $plan->invoice_period, $trial->getEndDate());
$trial = new Period(
interval: $plan->trial_interval,
count: $plan->trial_period,
start: $startDate ?? Carbon::now()
);
$period = new Period(
interval: $plan->invoice_interval,
count: $plan->invoice_period,
start: $trial->getEndDate()
);

return $this->planSubscriptions()->create([
'name' => $subscription,
Expand Down
58 changes: 55 additions & 3 deletions tests/Feature/SubscribeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

declare(strict_types=1);

use Tests\Models\Plan;
use Tests\Models\User;

it('User model has plan subscription trait or implement subscription methods', function (): void {
expect(User::factory()->create())
beforeEach(function (): void {
$this->user = User::factory()->create();
$this->plan = Plan::factory()->create();
});

it('User model implement subscription methods', function (): void {
expect($this->user)
->toHaveMethods([
'activePlanSubscriptions',
'planSubscription',
Expand All @@ -14,4 +20,50 @@
'subscribedPlans',
'subscribedTo',
]);
});
})->group('subscribe');

it('a user can subscribe to a plan', function (): void {
$this->user->newPlanSubscription('main', $this->plan);

expect($this->user->subscribedTo($this->plan->id))
->toBeTrue()
->and($this->user->subscribedPlans()->count())
->toBe(1);
})->group('subscribe');

it('user can have a monthly active subscription plan', function (): void {
$this->user->newPlanSubscription('main', $this->plan);

expect($this->user->planSubscription('main')->active())
->toBeTrue()
->and($this->user->planSubscription('main')->ends_at->toDateString())
->toBe(\Carbon\Carbon::now()->addMonth()->addDays($this->plan->trial_period)->toDateString());
})->group('subscribe');

it('user can change plan', function (): void {
$plan = Plan::factory()->create([
'name' => 'Premium plan',
'description' => 'Premium plan description',
'price' => 25.50,
'signup_fee' => 10.99,
]);

$this->user->newPlanSubscription('main', $this->plan);

$this->user->planSubscription('main')->changePlan($plan);

expect($this->user->subscribedTo($plan->id))
->toBeTrue();
})->group('subscribe');

it('user can cancel a subscription', function (): void {
$this->user->newPlanSubscription('main', $this->plan);

expect($this->user->subscribedTo($this->plan->id))
->toBeTrue();

$this->user->planSubscription('main')->cancel(true);

expect($this->user->planSubscription('main')->canceled())
->toBeTrue();
})->group('subscribe');
Loading

0 comments on commit 36e30e4

Please sign in to comment.