Skip to content

Commit 36e30e4

Browse files
authored
Merge pull request #2 from laravelcm/add-subscription-test
✅ Add subscription unit test
2 parents b36df3b + f52902a commit 36e30e4

File tree

9 files changed

+159
-42
lines changed

9 files changed

+159
-42
lines changed

README.md

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,29 @@ That's it, we only have to use that trait in our User model! Now your users may
5757
### Create a Plan
5858

5959
```php
60-
$plan = app('rinvex.subscriptions.plan')->create([
60+
use Laravelcm\Subscriptions\Models\Plan;
61+
use Laravelcm\Subscriptions\Models\Feature;
62+
use Laravelcm\Subscriptions\Interval;
63+
64+
$plan = Plan::create([
6165
'name' => 'Pro',
6266
'description' => 'Pro plan',
6367
'price' => 9.99,
6468
'signup_fee' => 1.99,
6569
'invoice_period' => 1,
66-
'invoice_interval' => 'month',
70+
'invoice_interval' => Interval::MONTH->value,
6771
'trial_period' => 15,
68-
'trial_interval' => 'day',
72+
'trial_interval' => Interval::DAY->value,
6973
'sort_order' => 1,
7074
'currency' => 'USD',
7175
]);
7276
7377
// Create multiple plan features at once
7478
$plan->features()->saveMany([
75-
new PlanFeature(['name' => 'listings', 'value' => 50, 'sort_order' => 1]),
76-
new PlanFeature(['name' => 'pictures_per_listing', 'value' => 10, 'sort_order' => 5]),
77-
new PlanFeature(['name' => 'listing_duration_days', 'value' => 30, 'sort_order' => 10, 'resettable_period' => 1, 'resettable_interval' => 'month']),
78-
new PlanFeature(['name' => 'listing_title_bold', 'value' => 'Y', 'sort_order' => 15])
79+
new Feature(['name' => 'listings', 'value' => 50, 'sort_order' => 1]),
80+
new Feature(['name' => 'pictures_per_listing', 'value' => 10, 'sort_order' => 5]),
81+
new Feature(['name' => 'listing_duration_days', 'value' => 30, 'sort_order' => 10, 'resettable_period' => 1, 'resettable_interval' => 'month']),
82+
new Feature(['name' => 'listing_title_bold', 'value' => 'Y', 'sort_order' => 15])
7983
]);
8084
```
8185

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

8690
```php
87-
$plan = app('rinvex.subscriptions.plan')->find(1);
91+
use Laravelcm\Subscriptions\Models\Plan;
92+
93+
$plan = Plan::find(1);
8894
8995
// Get all plan features
9096
$plan->features;
@@ -109,23 +115,29 @@ Both `$plan->features` and `$plan->planSubscriptions` are collections, driven fr
109115
Say you want to show the value of the feature _pictures_per_listing_ from above. You can do so in many ways:
110116
111117
```php
118+
use Laravelcm\Subscriptions\Models\Feature;
119+
use Laravelcm\Subscriptions\Models\Subscription;
120+
112121
// Use the plan instance to get feature's value
113122
$amountOfPictures = $plan->getFeatureBySlug('pictures_per_listing')->value;
114123
115124
// Query the feature itself directly
116-
$amountOfPictures = app('rinvex.subscriptions.plan_feature')->where('slug', 'pictures_per_listing')->first()->value;
125+
$amountOfPictures = Feature::where('slug', 'pictures_per_listing')->first()->value;
117126
118127
// Get feature value through the subscription instance
119-
$amountOfPictures = app('rinvex.subscriptions.plan_subscription')->find(1)->getFeatureValue('pictures_per_listing');
128+
$amountOfPictures = Subscription::find(1)->getFeatureValue('pictures_per_listing');
120129
```
121130
122131
### Create a Subscription
123132
124133
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.
125134
126135
```php
136+
use Laravelcm\Subscriptions\Models\Plan;
137+
use App\Models\User;
138+
127139
$user = User::find(1);
128-
$plan = app('rinvex.subscriptions.plan')->find(1);
140+
$plan = Plan::find(1);
129141
130142
$user->newPlanSubscription('main', $plan);
131143
```
@@ -137,8 +149,11 @@ The first argument passed to `newSubscription` method should be the title of the
137149
You can change subscription plan easily as follows:
138150
139151
```php
140-
$plan = app('rinvex.subscriptions.plan')->find(2);
141-
$subscription = app('rinvex.subscriptions.plan_subscription')->find(1);
152+
use Laravelcm\Subscriptions\Models\Plan;
153+
use Laravelcm\Subscriptions\Models\Subscription;
154+
155+
$plan = Plan::find(2);
156+
$subscription = Subscription::find(1);
142157
143158
// Change subscription plan
144159
$subscription->changePlan($plan);
@@ -151,8 +166,10 @@ If both plans (current and new plan) have the same billing frequency (e.g., `inv
151166
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:
152167
153168
```php
169+
use Laravelcm\Subscriptions\Models\Feature;
170+
154171
// Find plan feature
155-
$feature = app('rinvex.subscriptions.plan_feature')->where('name', 'listing_duration_days')->first();
172+
$feature = Feature::where('name', 'listing_duration_days')->first();
156173
157174
// Get feature reset date
158175
$feature->getResetDate(new \Carbon\Carbon());
@@ -263,24 +280,27 @@ $user->planSubscription('main')->cancel(true);
263280
#### Subscription Model
264281
265282
```php
283+
use Laravelcm\Subscriptions\Models\Subscription;
284+
use App\Models\User;
285+
266286
// Get subscriptions by plan
267-
$subscriptions = app('rinvex.subscriptions.plan_subscription')->byPlanId($plan_id)->get();
287+
$subscriptions = Subscription::byPlanId($plan_id)->get();
268288
269289
// Get bookings of the given user
270-
$user = \App\Models\User::find(1);
271-
$bookingsOfSubscriber = app('rinvex.subscriptions.plan_subscription')->ofSubscriber($user)->get();
290+
$user = User::find(1);
291+
$bookingsOfSubscriber = Subscription::ofSubscriber($user)->get();
272292
273293
// Get subscriptions with trial ending in 3 days
274-
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndingTrial(3)->get();
294+
$subscriptions = Subscription::findEndingTrial(3)->get();
275295
276296
// Get subscriptions with ended trial
277-
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndedTrial()->get();
297+
$subscriptions = Subscription::findEndedTrial()->get();
278298
279299
// Get subscriptions with period ending in 3 days
280-
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndingPeriod(3)->get();
300+
$subscriptions = Subscription::findEndingPeriod(3)->get();
281301
282302
// Get subscriptions with ended period
283-
$subscriptions = app('rinvex.subscriptions.plan_subscription')->findEndedPeriod()->get();
303+
$subscriptions = Subscription::findEndedPeriod()->get();
284304
```
285305
286306
### Models

config/laravel-subscriptions.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,32 @@
99

1010
return [
1111

12-
// Subscriptions Database Tables
12+
/*
13+
|--------------------------------------------------------------------------
14+
| Subscription Tables
15+
|--------------------------------------------------------------------------
16+
|
17+
|
18+
*/
19+
1320
'tables' => [
1421
'plans' => 'plans',
1522
'features' => 'features',
1623
'subscriptions' => 'subscriptions',
1724
'subscription_usage' => 'subscription_usage',
1825
],
1926

20-
// Subscriptions Models
27+
/*
28+
|--------------------------------------------------------------------------
29+
| Subscription Models
30+
|--------------------------------------------------------------------------
31+
|
32+
| Models used to manage subscriptions. You can replace to use your own models,
33+
| but make sure that you have the same functionalities or that your models
34+
| extend from each model that you are going to replace.
35+
|
36+
*/
37+
2138
'models' => [
2239
'plan' => Plan::class,
2340
'feature' => Feature::class,

database/migrations/2020_01_01_000001_create_plans_table.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Database\Schema\Blueprint;
66
use Illuminate\Database\Migrations\Migration;
77
use Illuminate\Support\Facades\Schema;
8+
use Laravelcm\Subscriptions\Interval;
89

910
return new class () extends Migration {
1011
public function up(): void
@@ -20,11 +21,11 @@ public function up(): void
2021
$table->decimal('signup_fee')->default('0.00');
2122
$table->string('currency', 3);
2223
$table->unsignedSmallInteger('trial_period')->default(0);
23-
$table->string('trial_interval')->default('day');
24+
$table->string('trial_interval')->default(Interval::DAY->value);
2425
$table->unsignedSmallInteger('invoice_period')->default(0);
25-
$table->string('invoice_interval')->default('month');
26+
$table->string('invoice_interval')->default(Interval::MONTH->value);
2627
$table->unsignedSmallInteger('grace_period')->default(0);
27-
$table->string('grace_interval')->default('day');
28+
$table->string('grace_interval')->default(Interval::DAY->value);
2829
$table->unsignedTinyInteger('prorate_day')->nullable();
2930
$table->unsignedTinyInteger('prorate_period')->nullable();
3031
$table->unsignedTinyInteger('prorate_extend_due')->nullable();
File renamed without changes.

src/Interval.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravelcm\Subscriptions;
6+
7+
enum Interval: string
8+
{
9+
case YEAR = 'year';
10+
11+
case MONTH = 'month';
12+
13+
case DAY = 'day';
14+
}

src/Models/Subscription.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public function changePlan(Plan $plan): self
188188
{
189189
// If plans does not have the same billing frequency
190190
// (e.g., invoice_interval and invoice_period) we will update
191-
// the billing dates starting today, and sice we are basically creating
191+
// the billing dates starting today, and since we are basically creating
192192
// a new billing cycle, the usage data will be cleared.
193193
if ($this->plan->invoice_interval !== $plan->invoice_interval || $this->plan->invoice_period !== $plan->invoice_period) {
194194
$this->setNewPeriod($plan->invoice_interval, $plan->invoice_period);
@@ -321,7 +321,7 @@ public function scopeFindActive(Builder $builder): Builder
321321
*
322322
* @return $this
323323
*/
324-
protected function setNewPeriod(string $invoice_interval = '', ?int $invoice_period = null, Carbon $start = null): self
324+
protected function setNewPeriod(string $invoice_interval = '', int $invoice_period = null, Carbon $start = null): self
325325
{
326326
if (empty($invoice_interval)) {
327327
$invoice_interval = $this->plan->invoice_interval;
@@ -331,7 +331,11 @@ protected function setNewPeriod(string $invoice_interval = '', ?int $invoice_per
331331
$invoice_period = $this->plan->invoice_period;
332332
}
333333

334-
$period = new Period($invoice_interval, $invoice_period, $start ?? Carbon::now());
334+
$period = new Period(
335+
interval: $invoice_interval,
336+
count: $invoice_period,
337+
start: $start ?? Carbon::now()
338+
);
335339

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

351355
if ($feature->resettable_period) {
352356
// Set expiration date when the usage record is new or doesn't have one.
353-
if (null === $usage->valid_until) {
357+
if ($usage->valid_until === null) {
354358
// Set date from subscription creation date so the reset
355359
// period match the period specified by the subscription's plan.
356360
$usage->valid_until = $feature->getResetDate($this->created_at);
@@ -362,7 +366,7 @@ public function recordFeatureUsage(string $featureSlug, int $uses = 1, bool $inc
362366
}
363367
}
364368

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

367371
$usage->save();
368372

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

376-
if (null === $usage) {
380+
if ($usage === null) {
377381
return null;
378382
}
379383

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

399-
if ('true' === $featureValue) {
403+
if ($featureValue === 'true') {
400404
return true;
401405
}
402406

403407
// If the feature value is zero, let's return false since
404408
// there's no uses available. (useful to disable countable features)
405-
if ( ! $usage || $usage->expired() || null === $featureValue || '0' === $featureValue || 'false' === $featureValue) {
409+
if ( ! $usage || $usage->expired() || $featureValue === null || $featureValue === '0' || $featureValue === 'false') {
406410
return false;
407411
}
408412

src/Traits/HasPlanSubscriptions.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function subscribedPlans(): Collection
5252
->pluck('plan_id')
5353
->unique();
5454

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

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

6767
public function newPlanSubscription(string $subscription, Plan $plan, ?Carbon $startDate = null): Subscription
6868
{
69-
$trial = new Period($plan->trial_interval, $plan->trial_period, $startDate ?? Carbon::now());
70-
$period = new Period($plan->invoice_interval, $plan->invoice_period, $trial->getEndDate());
69+
$trial = new Period(
70+
interval: $plan->trial_interval,
71+
count: $plan->trial_period,
72+
start: $startDate ?? Carbon::now()
73+
);
74+
$period = new Period(
75+
interval: $plan->invoice_interval,
76+
count: $plan->invoice_period,
77+
start: $trial->getEndDate()
78+
);
7179

7280
return $this->planSubscriptions()->create([
7381
'name' => $subscription,

tests/Feature/SubscribeTest.php

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
declare(strict_types=1);
44

5+
use Tests\Models\Plan;
56
use Tests\Models\User;
67

7-
it('User model has plan subscription trait or implement subscription methods', function (): void {
8-
expect(User::factory()->create())
8+
beforeEach(function (): void {
9+
$this->user = User::factory()->create();
10+
$this->plan = Plan::factory()->create();
11+
});
12+
13+
it('User model implement subscription methods', function (): void {
14+
expect($this->user)
915
->toHaveMethods([
1016
'activePlanSubscriptions',
1117
'planSubscription',
@@ -14,4 +20,50 @@
1420
'subscribedPlans',
1521
'subscribedTo',
1622
]);
17-
});
23+
})->group('subscribe');
24+
25+
it('a user can subscribe to a plan', function (): void {
26+
$this->user->newPlanSubscription('main', $this->plan);
27+
28+
expect($this->user->subscribedTo($this->plan->id))
29+
->toBeTrue()
30+
->and($this->user->subscribedPlans()->count())
31+
->toBe(1);
32+
})->group('subscribe');
33+
34+
it('user can have a monthly active subscription plan', function (): void {
35+
$this->user->newPlanSubscription('main', $this->plan);
36+
37+
expect($this->user->planSubscription('main')->active())
38+
->toBeTrue()
39+
->and($this->user->planSubscription('main')->ends_at->toDateString())
40+
->toBe(\Carbon\Carbon::now()->addMonth()->addDays($this->plan->trial_period)->toDateString());
41+
})->group('subscribe');
42+
43+
it('user can change plan', function (): void {
44+
$plan = Plan::factory()->create([
45+
'name' => 'Premium plan',
46+
'description' => 'Premium plan description',
47+
'price' => 25.50,
48+
'signup_fee' => 10.99,
49+
]);
50+
51+
$this->user->newPlanSubscription('main', $this->plan);
52+
53+
$this->user->planSubscription('main')->changePlan($plan);
54+
55+
expect($this->user->subscribedTo($plan->id))
56+
->toBeTrue();
57+
})->group('subscribe');
58+
59+
it('user can cancel a subscription', function (): void {
60+
$this->user->newPlanSubscription('main', $this->plan);
61+
62+
expect($this->user->subscribedTo($this->plan->id))
63+
->toBeTrue();
64+
65+
$this->user->planSubscription('main')->cancel(true);
66+
67+
expect($this->user->planSubscription('main')->canceled())
68+
->toBeTrue();
69+
})->group('subscribe');

0 commit comments

Comments
 (0)