├── LICENSE.md ├── README.md ├── composer.json ├── resources ├── config │ └── plans.php ├── factories │ ├── FeatureFactory.php │ ├── PlanFactory.php │ ├── PlanFeatureFactory.php │ ├── PlanSubscriptionFactory.php │ ├── PlanSubscriptionUsageFactory.php │ └── UserFactory.php ├── lang │ ├── en │ │ └── messages.php │ └── vi │ │ └── messages.php └── migrations │ └── 2018_01_01_000000_create_plans_tables.php └── src ├── Contracts └── Subscriber.php ├── Events ├── SubscriptionCanceled.php ├── SubscriptionPlanChanged.php └── SubscriptionRenewed.php ├── Models ├── Concerns │ ├── BelongsToPlanModel.php │ ├── HasCode.php │ ├── Resettable.php │ └── Subscribable.php ├── Feature.php ├── Plan.php ├── PlanFeature.php ├── PlanSubscription.php └── PlanSubscriptionUsage.php ├── Period.php ├── PricingPlansServiceProvider.php ├── SubscriptionAbility.php ├── SubscriptionBuilder.php └── SubscriptionUsageManager.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016-2017 Gerardo Baez 4 | Copyright © 2017-2018 Oanh Nguyen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Pricing Plans 2 | 3 | [![Build Status](https://travis-ci.org/oanhnn/laravel-pricing-plans.svg?branch=master)](https://travis-ci.org/oanhnn/laravel-pricing-plans) 4 | [![Coverage Status](https://coveralls.io/repos/github/oanhnn/laravel-pricing-plans/badge.svg?branch=master)](https://coveralls.io/github/oanhnn/laravel-pricing-plans?branch=master) 5 | [![Latest Version](https://img.shields.io/github/release/oanhnn/laravel-pricing-plans.svg?style=flat-square)](https://github.com/oanhnn/laravel-pricing-plans/releases) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 7 | 8 | Easy provide pricing plans for Your Laravel 5.4+ Application. 9 | 10 | 11 | 12 | - [Main features](#main-features) 13 | - [TODO](#todo) 14 | - [Requirements](#requirements) 15 | - [Installation](#installation) 16 | - [Composer](#composer) 17 | - [Service Provider](#service-provider) 18 | - [Config file and Migrations](#config-file-and-migrations) 19 | - [Contract and Traits](#contract-and-traits) 20 | - [Config File](#config-file) 21 | - [Models](#models) 22 | - [Feature model](#feature-model) 23 | - [Plan model](#plan-model) 24 | - [PlanFeature model](#planfeature-model) 25 | - [PlanSubscription model](#plansubscription-model) 26 | - [PlanSubscriptionUsage model](#plansubscriptionusage-model) 27 | - [Events](#events) 28 | - [SubscriptionRenewed event](#subscriptionrenewed-event) 29 | - [SubscriptionCanceled event](#subscriptioncanceled-event) 30 | - [SubscriptionPlanChanged event](#subscriptionplanchanged-event) 31 | - [Usage](#usage) 32 | - [Create features and plan](#create-features-and-plan) 33 | - [Creating subscriptions](#creating-subscriptions) 34 | - [Subscription Ability](#subscription-ability) 35 | - [Record Feature Usage](#record-feature-usage) 36 | - [Reduce Feature Usage](#reduce-feature-usage) 37 | - [Clear The Subscription Usage Data](#clear-the-subscription-usage-data) 38 | - [Check Subscription Status](#check-subscription-status) 39 | - [Renew a Subscription](#renew-a-subscription) 40 | - [Cancel a Subscription](#cancel-a-subscription) 41 | - [Scopes](#scopes) 42 | - [Changelog](#changelog) 43 | - [Testing](#testing) 44 | - [Contributing](#contributing) 45 | - [Security](#security) 46 | - [Credits](#credits) 47 | - [License](#license) 48 | 49 | 50 | 51 | ## Main features 52 | 53 | Easy provide pricing plans for Your Laravel 5.4+ Application. 54 | 55 | ## TODO 56 | 57 | - [ ] Caching some select query 58 | - [ ] Add unit test scripts 59 | - [ ] Make better documents 60 | 61 | ## Requirements 62 | 63 | * php >=7.0 64 | * Laravel 5.4+ 65 | 66 | ## Installation 67 | 68 | ### Composer 69 | 70 | Begin by pulling in the package through Composer. 71 | 72 | ```bash 73 | $ composer require oanhnn/laravel-pricing-plans 74 | ``` 75 | 76 | ### Service Provider 77 | 78 | Next, if using Laravel 5.5+, you done. If using Laravel 5.4, you must include the service provider within your `config/app.php` file. 79 | 80 | ```php 81 | // config/app.php 82 | 83 | 'providers' => [ 84 | // Other service providers... 85 | 86 | Laravel\PricingPlans\PricingPlansServiceProvider::class, 87 | ], 88 | ``` 89 | 90 | ### Config file and Migrations 91 | 92 | Publish package config file and migrations with the command: 93 | 94 | ```bash 95 | $ php artisan vendor:publish --provider="Laravel\PricingPlans\PricingPlansServiceProvider" 96 | ``` 97 | 98 | Then run migrations: 99 | 100 | ```bash 101 | $ php artisan migrate 102 | ``` 103 | 104 | ### Contract and Traits 105 | 106 | Add `Laravel\PricingPlans\Contacts\Subscriber` contract and `Laravel\PricingPlans\Models\Concerns\Subscribable` trait 107 | to your subscriber model (Eg. `User`). 108 | 109 | See the following example: 110 | 111 | ```php 112 | 'Upload images', 220 | 'code' => 'upload-images', 221 | 'description' => null, 222 | 'interval_unit' => 'day', 223 | 'interval_count' => 1, 224 | 'sort_order' => 1, 225 | ]); 226 | 227 | $feature2 = Feature::create([ 228 | 'name' => 'upload video', 229 | 'code' => 'upload-video', 230 | 'description' => null, 231 | 'interval_unit' => 'day', 232 | 'interval_count' => 1, 233 | 'sort_order' => 2, 234 | ]); 235 | 236 | $plan = Plan::create([ 237 | 'name' => 'Pro', 238 | 'code' => 'pro', 239 | 'description' => 'Pro plan', 240 | 'price' => 9.99, 241 | 'interval_unit' => 'month', 242 | 'interval_count' => 1, 243 | 'trial_period_days' => 5, 244 | 'sort_order' => 1, 245 | ]); 246 | 247 | $plan->features()->attach([ 248 | $feature1->id => ['value' => 5, 'note' => 'Can upload maximum 5 images daily'], 249 | $feature2->id => ['value' => 1, 'note' => 'Can upload maximum 1 video daily'], 250 | ]); 251 | ``` 252 | 253 | ### Creating subscriptions 254 | 255 | You can subscribe a user to a plan by using the `newSubscription()` function available in the `Subscribable` trait. 256 | First, retrieve an instance of your subscriber model, which typically will be your user model and an instance of the plan 257 | your user is subscribing to. Once you have retrieved the model instance, you may use the `newSubscription` method 258 | to create the model's subscription. 259 | 260 | ```php 261 | firstOrFail(); 268 | 269 | $user->newSubscription('main', $plan)->create(); 270 | ``` 271 | 272 | The first argument passed to `newSubscription` method should be the name of the subscription. If your application offer 273 | a single subscription, you might call this `main` or `primary`. The second argument is the plan instance your user is subscribing to. 274 | 275 | 278 | 279 | 280 | 281 | ### Subscription Ability 282 | 283 | There's multiple ways to determine the usage and ability of a particular feature in the user subscription, the most common one is `canUse`: 284 | 285 | The `canUse` method returns `true` or `false` depending on multiple factors: 286 | 287 | - Feature _is enabled_. 288 | - Feature value isn't `0`. 289 | - Or feature has remaining uses available. 290 | 291 | ```php 292 | $user->subscription('main')->ability()->canUse(Feature::FEATURE_UPLOAD_IMAGES); 293 | ``` 294 | 295 | Other methods are: 296 | 297 | - `enabled`: returns `true` when the value of the feature is a _positive word_ listed in the config file. 298 | - `consumed`: returns how many times the user has used a particular feature. 299 | - `remainings`: returns available uses for a particular feature. 300 | - `value`: returns the feature value. 301 | 302 | > All methods share the same signature: e.g. 303 | >`$user->subscription('main')->ability()->consumed(Feature::FEATURE_UPLOAD_IMAGES);`. 304 | 305 | 306 | ### Record Feature Usage 307 | 308 | In order to efectively use the ability methods you will need to keep track of every usage of each feature 309 | (or at least those that require it). You may use the `record` method available through the user `subscriptionUsage()` 310 | method: 311 | 312 | ```php 313 | $user->subscriptionUsage('main')->record(Feature::FEATURE_UPLOAD_IMAGES); 314 | ``` 315 | 316 | The `record` method accept 3 parameters: the first one is the feature's code, the second one is the quantity of 317 | uses to add (default is `1`), and the third one indicates if the addition should be incremental (default behavior), 318 | when disabled the usage will be override by the quantity provided. E.g.: 319 | 320 | ```php 321 | // Increment by 2 322 | $user->subscriptionUsage('main')->record(Feature::FEATURE_UPLOAD_IMAGES, 2); 323 | 324 | // Override with 9 325 | $user->subscriptionUsage('main')->record(Feature::FEATURE_UPLOAD_IMAGES, 9, false); 326 | ``` 327 | 328 | ### Reduce Feature Usage 329 | 330 | Reducing the feature usage is _almost_ the same as incrementing it. Here we only _substract_ a given quantity (default is `1`) 331 | to the actual usage: 332 | 333 | ```php 334 | $user->subscriptionUsage('main')->reduce(Feature::FEATURE_UPLOAD_IMAGES, 2); 335 | ``` 336 | 337 | ### Clear The Subscription Usage Data 338 | 339 | ```php 340 | $user->subscriptionUsage('main')->clear(); 341 | ``` 342 | 343 | ### Check Subscription Status 344 | 345 | For a subscription to be considered active _one of the following must be `true`_: 346 | 347 | - Subscription has an active trial. 348 | - Subscription `ends_at` is in the future. 349 | 350 | ```php 351 | $user->subscribed('main'); 352 | $user->subscribed('main', $planId); // Check if user is using a particular plan 353 | ``` 354 | 355 | Alternatively you can use the following methods available in the subscription model: 356 | 357 | ```php 358 | $user->subscription('main')->isActive(); 359 | $user->subscription('main')->isCanceled(); 360 | $user->subscription('main')->isCanceledImmediately(); 361 | $user->subscription('main')->isEnded(); 362 | $user->subscription('main')->onTrial(); 363 | ``` 364 | 365 | > Canceled subscriptions with an active trial or `ends_at` in the future are considered active. 366 | 367 | ### Renew a Subscription 368 | 369 | To renew a subscription you may use the `renew` method available in the subscription model. 370 | This will set a new `ends_at` date based on the selected plan and _will clear the usage data_ of the subscription. 371 | 372 | ```php 373 | $user->subscription('main')->renew(); 374 | ``` 375 | 376 | _Canceled subscriptions with an ended period can't be renewed._ 377 | 378 | ### Cancel a Subscription 379 | 380 | To cancel a subscription, simply use the `cancel` method on the user's subscription: 381 | 382 | ```php 383 | $user->subscription('main')->cancel(); 384 | ``` 385 | 386 | By default the subscription will remain active until the end of the period, you may pass `true` to end the subscription _immediately_: 387 | 388 | ```php 389 | $user->subscription('main')->cancel(true); 390 | ``` 391 | 392 | ### Scopes 393 | 394 | #### Subscription Model 395 | ```php 396 | get(); 402 | 403 | // Get subscription by subscriber: 404 | $subscription = PlanSubscription::bySubscriber($user)->first(); 405 | 406 | // Get subscriptions with trial ending in 3 days: 407 | $subscriptions = PlanSubscription::findEndingTrial(3)->get(); 408 | 409 | // Get subscriptions with ended trial: 410 | $subscriptions = PlanSubscription::findEndedTrial()->get(); 411 | 412 | // Get subscriptions with period ending in 3 days: 413 | $subscriptions = PlanSubscription::findEndingPeriod(3)->get(); 414 | 415 | // Get subscriptions with ended period: 416 | $subscriptions = PlanSubscription::findEndedPeriod()->get(); 417 | ``` 418 | 419 | ## Changelog 420 | 421 | See all change logs in [CHANGELOG](CHANGELOG.md) 422 | 423 | ## Testing 424 | 425 | ```bash 426 | $ git clone git@github.com/oanhnn/laravel-pricing-plans.git /path 427 | $ cd /path 428 | $ composer install 429 | $ composer phpunit 430 | ``` 431 | 432 | ## Contributing 433 | 434 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 435 | 436 | ## Security 437 | 438 | If you discover any security related issues, please email to [Oanh Nguyen](mailto:oanhnn.bk@gmail.com) instead of 439 | using the issue tracker. 440 | 441 | ## Credits 442 | 443 | I forked and recreated this project from [gerardojbaez/laraplans](https://github.com/gerardojbaez/laraplans) project in mid-2017. 444 | Thank [Gerardo Baez](https://github.com/gerardojbaez) 445 | 446 | - [Oanh Nguyen](https://github.com/oanhnn) 447 | - [Gerardo Baez](https://github.com/gerardojbaez) 448 | - [All Contributors](../../contributors) 449 | 450 | ## License 451 | 452 | This project is released under the MIT License. 453 | Copyright © 2017-2018 [Oanh Nguyen](https://oanhnn.github.io/). 454 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oanhnn/laravel-pricing-plans", 3 | "type": "library", 4 | "description": "A package provide pricing plans for Laravel.", 5 | "keywords": [ 6 | "plans", 7 | "laravel", 8 | "subscriptions", 9 | "pricing" 10 | ], 11 | "license": "MIT", 12 | "homepage": "https://github.com/oanhnn/laravel-pricing-plans", 13 | "support": { 14 | "issues": "https://github.com/oanhnn/laravel-pricing-plans/issues", 15 | "source": "https://github.com/oanhnn/laravel-pricing-plans" 16 | }, 17 | "authors": [ 18 | { 19 | "name": "Oanh Nguyen", 20 | "email": "oanhnn.bk@gmail.com" 21 | } 22 | ], 23 | "autoload": { 24 | "psr-4": { 25 | "Laravel\\PricingPlans\\": "src/" 26 | } 27 | }, 28 | "require": { 29 | "php": ">=7.0", 30 | "illuminate/database": "~5.4", 31 | "illuminate/support": "~5.4", 32 | "nesbot/carbon": "~1.21" 33 | }, 34 | "require-dev": { 35 | "fzaninotto/faker": "^1.4", 36 | "mockery/mockery": "^0.9", 37 | "orchestra/database": "^3.4", 38 | "orchestra/testbench": "^3.4", 39 | "phpunit/phpunit": "~6.1|~7.0", 40 | "squizlabs/php_codesniffer": "^3.0" 41 | }, 42 | "scripts": { 43 | "phpunit": "php vendor/phpunit/phpunit/phpunit --coverage-html build/coverage", 44 | "phpcs": "php vendor/squizlabs/php_codesniffer/bin/phpcs", 45 | "phpcbf": "php vendor/squizlabs/php_codesniffer/bin/phpcbf" 46 | }, 47 | "config": { 48 | "preferred-install": "dist", 49 | "sort-packages": true, 50 | "optimize-autoloader": true 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "providers": [ 55 | "Laravel\\PricingPlans\\PricingPlansServiceProvider" 56 | ], 57 | "aliases": { 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resources/config/plans.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'Y', 16 | 'YES', 17 | 'TRUE', 18 | 'UNLIMITED', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Tables 24 | |-------------------------------------------------------------------------- 25 | | 26 | | If you want to customize name of your tables 27 | | 28 | */ 29 | 'tables' => [ 30 | 'features' => 'features', 31 | 'plans' => 'plans', 32 | 'plan_features' => 'plan_features', 33 | 'plan_subscriptions' => 'plan_subscriptions', 34 | 'plan_subscription_usages' => 'plan_subscription_usages', 35 | ], 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Models 40 | |-------------------------------------------------------------------------- 41 | | 42 | | If you want to use your own models you will want to update the following 43 | | array to make sure this package use them. 44 | | 45 | */ 46 | 'models' => [ 47 | 'Feature' => 'Laravel\\PricingPlans\\Models\\Feature', 48 | 'Plan' => 'Laravel\\PricingPlans\\Models\\Plan', 49 | 'PlanFeature' => 'Laravel\\PricingPlans\\Models\\PlanFeature', 50 | 'PlanSubscription' => 'Laravel\\PricingPlans\\Models\\PlanSubscription', 51 | 'PlanSubscriptionUsage' => 'Laravel\\PricingPlans\\Models\\PlanSubscriptionUsage', 52 | ], 53 | 54 | ]; 55 | -------------------------------------------------------------------------------- /resources/factories/FeatureFactory.php: -------------------------------------------------------------------------------- 1 | define(Feature::class, function (Generator $faker) { 8 | return [ 9 | 'name' => $faker->word, 10 | 'code' => $faker->unique()->slug, 11 | 'description' => $faker->sentence, 12 | 'interval_unit' => $faker->randomElement([null, Period::DAY, Period::WEEK, Period::MONTH, Period::YEAR]), 13 | 'interval_count' => $faker->numberBetween(0, 2), 14 | ]; 15 | }); 16 | -------------------------------------------------------------------------------- /resources/factories/PlanFactory.php: -------------------------------------------------------------------------------- 1 | define(Plan::class, function (Generator $faker) { 8 | return [ 9 | 'name' => $faker->word, 10 | 'code' => $faker->unique()->slug, 11 | 'description' => $faker->sentence, 12 | 'price' => rand(0, 9), 13 | 'interval_unit' => $faker->randomElement([Period::MONTH, Period::YEAR]), 14 | 'interval_count' => 1, 15 | 'trial_period_days' => $faker->numberBetween(0, 10), 16 | ]; 17 | }); 18 | -------------------------------------------------------------------------------- /resources/factories/PlanFeatureFactory.php: -------------------------------------------------------------------------------- 1 | define(PlanFeature::class, function (Generator $faker) { 9 | return [ 10 | 'plan_id' => factory(Plan::class)->create()->id, 11 | 'feature_id' => factory(Feature::class)->create()->id, 12 | 'value' => $faker->randomElement(['10','20','30','50','Y','N','UNLIMITED', null]), 13 | ]; 14 | }); 15 | -------------------------------------------------------------------------------- /resources/factories/PlanSubscriptionFactory.php: -------------------------------------------------------------------------------- 1 | define(PlanSubscription::class, function (Generator $faker) { 9 | return [ 10 | 'subscriber_type' => User::class, 11 | 'subscriber_id' => factory(User::class)->create()->id, 12 | 'plan_id' => factory(Plan::class)->create()->id, 13 | 'name' => $faker->word, 14 | 'canceled_immediately' => false, 15 | ]; 16 | }); 17 | -------------------------------------------------------------------------------- /resources/factories/PlanSubscriptionUsageFactory.php: -------------------------------------------------------------------------------- 1 | define(PlanSubscriptionUsage::class, function (Generator $faker) { 9 | return [ 10 | 'subscription_id' => factory(PlanSubscription::class)->create()->id, 11 | 'feature_id' => factory(Feature::class)->create()->id, 12 | 'used' => rand(1, 50), 13 | 'valid_until' => $faker->dateTime(), 14 | ]; 15 | }); 16 | -------------------------------------------------------------------------------- /resources/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Generator $faker) { 8 | return [ 9 | 'name' => $faker->name, 10 | 'email' => $faker->safeEmail, 11 | 'password' => Hash::make(str_random(10)), 12 | 'remember_token' => str_random(10), 13 | ]; 14 | }); 15 | -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 'Day', 5 | 'week' => 'Week', 6 | 'month' => 'Month', 7 | 'year' => 'Year', 8 | 9 | 'interval_description' => [ 10 | 'day' => 'Daily|Every :count Days', 11 | 'week' => 'Weekly|Every :count Weeks', 12 | 'month' => 'Monthly|Every :count Months', 13 | 'year' => 'Annual|Every :count Years', 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/lang/vi/messages.php: -------------------------------------------------------------------------------- 1 | 'Ngày', 5 | 'week' => 'Tuần', 6 | 'month' => 'Tháng', 7 | 'year' => 'Năm', 8 | 9 | 'interval_description' => [ 10 | 'day' => 'Hàng ngày|Trong :count ngày', 11 | 'week' => 'Hàng tuần|Trong :count tuần', 12 | 'month' => 'Hàng tháng|Trong :count tháng', 13 | 'year' => 'Hàng năm|Trong :count năm', 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /resources/migrations/2018_01_01_000000_create_plans_tables.php: -------------------------------------------------------------------------------- 1 | increments('id'); 20 | $table->string('name'); 21 | $table->string('code')->unique(); 22 | $table->text('description')->nullable(); 23 | $table->string('interval_unit')->default('month'); 24 | $table->smallInteger('interval_count')->unsigned()->default(1); 25 | 26 | $table->smallInteger('sort_order')->nullable(); 27 | $table->timestamps(); 28 | }); 29 | 30 | Schema::create($tables['plans'], function (Blueprint $table) { 31 | $table->increments('id'); 32 | $table->string('name'); 33 | $table->string('code')->unique(); 34 | $table->text('description')->nullable(); 35 | $table->decimal('price', 15, 4)->default('0.00'); 36 | $table->string('interval_unit')->default('month'); 37 | $table->smallInteger('interval_count')->unsigned()->default(1); 38 | $table->smallInteger('trial_period_days')->nullable(); 39 | 40 | $table->smallInteger('sort_order')->nullable(); 41 | $table->timestamps(); 42 | }); 43 | 44 | Schema::create($tables['plan_features'], function (Blueprint $table) { 45 | $table->increments('id'); 46 | $table->integer('plan_id')->unsigned(); 47 | $table->integer('feature_id')->unsigned(); 48 | $table->string('value'); 49 | $table->text('note')->nullable(); 50 | $table->timestamps(); 51 | 52 | $table->foreign('plan_id')->references('id')->on('plans')->onDelete('cascade'); 53 | $table->foreign('feature_id')->references('id')->on('features')->onDelete('cascade'); 54 | $table->unique(['plan_id', 'feature_id']); 55 | }); 56 | 57 | Schema::create($tables['plan_subscriptions'], function (Blueprint $table) { 58 | $table->increments('id'); 59 | $table->morphs('subscriber'); 60 | $table->integer('plan_id')->unsigned(); 61 | $table->string('name'); 62 | $table->boolean('canceled_immediately')->nullable(); 63 | $table->timestamp('trial_ends_at')->nullable(); 64 | $table->timestamp('starts_at')->nullable(); 65 | $table->timestamp('ends_at')->nullable(); 66 | $table->timestamp('canceled_at')->nullable(); 67 | $table->timestamps(); 68 | 69 | $table->foreign('plan_id')->references('id')->on('plans')->onDelete('cascade'); 70 | $table->index(['subscriber_type', 'subscriber_id', 'plan_id']); 71 | }); 72 | 73 | Schema::create($tables['plan_subscription_usages'], function (Blueprint $table) { 74 | $table->increments('id'); 75 | $table->integer('subscription_id')->unsigned(); 76 | $table->string('feature_code'); 77 | $table->smallInteger('used')->unsigned()->default(0); 78 | $table->timestamp('valid_until')->nullable(); 79 | $table->timestamps(); 80 | 81 | $table->foreign('subscription_id')->references('id')->on('plan_subscriptions')->onDelete('cascade'); 82 | $table->foreign('feature_code')->references('code')->on('features')->onDelete('cascade'); 83 | $table->unique(['subscription_id', 'feature_code']); 84 | }); 85 | } 86 | 87 | /** 88 | * Reverse the migrations. 89 | * 90 | * @return void 91 | */ 92 | public function down() 93 | { 94 | $tables = Config::get('plans.tables'); 95 | 96 | Schema::dropIfExists($tables['plan_subscription_usages']); 97 | Schema::dropIfExists($tables['plan_subscriptions']); 98 | Schema::dropIfExists($tables['plan_features']); 99 | Schema::dropIfExists($tables['plans']); 100 | Schema::dropIfExists($tables['features']); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Contracts/Subscriber.php: -------------------------------------------------------------------------------- 1 | subscription = $subscription; 22 | } 23 | 24 | /** 25 | * @return PlanSubscription 26 | */ 27 | public function getSubscription() 28 | { 29 | return $this->subscription; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Events/SubscriptionPlanChanged.php: -------------------------------------------------------------------------------- 1 | subscription = $subscription; 22 | } 23 | 24 | /** 25 | * @return PlanSubscription 26 | */ 27 | public function getSubscription() 28 | { 29 | return $this->subscription; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Events/SubscriptionRenewed.php: -------------------------------------------------------------------------------- 1 | subscription = $subscription; 22 | } 23 | 24 | /** 25 | * @return PlanSubscription 26 | */ 27 | public function getSubscription() 28 | { 29 | return $this->subscription; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/Concerns/BelongsToPlanModel.php: -------------------------------------------------------------------------------- 1 | belongsTo( 17 | Config::get('plans.models.Plan'), 18 | 'plan_id', 19 | 'id' 20 | ); 21 | } 22 | 23 | /** 24 | * Scope by plan id. 25 | * 26 | * @param \Illuminate\Database\Eloquent\Builder 27 | * @param int $planId 28 | * @return \Illuminate\Database\Eloquent\Builder 29 | */ 30 | public function scopeByPlan($query, $planId) 31 | { 32 | return $query->where('plan_id', $planId); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasCode.php: -------------------------------------------------------------------------------- 1 | where('code', $code); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Models/Concerns/Resettable.php: -------------------------------------------------------------------------------- 1 | interval_unit] ?? null; 20 | } 21 | 22 | /** 23 | * Get Interval Description 24 | * 25 | * @return string 26 | */ 27 | public function getIntervalDescriptionAttribute() 28 | { 29 | return Lang::choice('plans::messages.interval_description.' . $this->interval_unit, $this->interval_count); 30 | } 31 | 32 | /** 33 | * @return bool 34 | */ 35 | public function isResettable(): bool 36 | { 37 | return is_string($this->interval_unit) && is_int($this->interval_count); 38 | } 39 | 40 | /** 41 | * @param string|null|int|\DateTime $startedAt 42 | * @return \Carbon\Carbon 43 | */ 44 | public function getResetTime($startedAt = null) 45 | { 46 | return (new Period($this->interval_unit, $this->interval_count, $startedAt))->getEndAt(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Models/Concerns/Subscribable.php: -------------------------------------------------------------------------------- 1 | morphMany( 21 | Config::get('plans.models.PlanSubscription'), 22 | 'subscriber' 23 | ); 24 | } 25 | 26 | /** 27 | * Get a subscription by name. 28 | * 29 | * @param string $name Subscription name 30 | * @return \Laravel\PricingPlans\Models\PlanSubscription|null 31 | */ 32 | public function subscription(string $name = 'default') 33 | { 34 | if ($this->relationLoaded('subscriptions')) { 35 | return $this->subscriptions 36 | ->orderByDesc(function ($subscription) { 37 | return $subscription->created_at->getTimestamp(); 38 | }) 39 | ->first(function ($subscription) use ($name) { 40 | return $subscription->name === $name; 41 | }); 42 | } 43 | 44 | return $this->subscriptions() 45 | ->where('name', $name) 46 | ->orderByDesc('created_at') 47 | ->first(); 48 | } 49 | 50 | /** 51 | * Check if the user has a given subscription. 52 | * 53 | * @param string $subscription Subscription name 54 | * @param string|null $planCode 55 | * @return bool 56 | */ 57 | public function subscribed(string $subscription, string $planCode = null): bool 58 | { 59 | $planSubscription = $this->subscription($subscription); 60 | 61 | if (is_null($planSubscription)) { 62 | return false; 63 | } 64 | 65 | if (is_null($planCode) || $planCode == $planSubscription->plan->code) { 66 | return $subscription->isActive(); 67 | } 68 | 69 | return false; 70 | } 71 | 72 | /** 73 | * Subscribe user to a new plan. 74 | * 75 | * @param string $subscription Subscription name 76 | * @param \Laravel\PricingPlans\Models\Plan $plan 77 | * @return \Laravel\PricingPlans\SubscriptionBuilder 78 | */ 79 | public function newSubscription(string $subscription, Plan $plan) 80 | { 81 | return new SubscriptionBuilder($this, $subscription, $plan); 82 | } 83 | 84 | /** 85 | * Get subscription usage manager instance. 86 | * 87 | * @param string $subscription Subscription name 88 | * @return \Laravel\PricingPlans\SubscriptionUsageManager 89 | */ 90 | public function subscriptionUsage(string $subscription) 91 | { 92 | return new SubscriptionUsageManager($this->subscription($subscription)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Models/Feature.php: -------------------------------------------------------------------------------- 1 | setTable(Config::get('plans.tables.features')); 63 | } 64 | 65 | /** 66 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 67 | */ 68 | public function plans() 69 | { 70 | return $this->belongsToMany( 71 | Config::get('plans.models.Plan'), 72 | Config::get('plans.tables.plan_features'), 73 | 'feature_id', 74 | 'plan_id' 75 | )->using(Config::get('plans.models.PlanFeature')); 76 | } 77 | 78 | /** 79 | * Get feature usage. 80 | * 81 | * This will return all related subscriptions usages. 82 | * 83 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 84 | */ 85 | public function usage() 86 | { 87 | return $this->hasMany( 88 | Config::get('plans.models.PlanSubscriptionUsage'), 89 | 'feature_code', 90 | 'code' 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Models/Plan.php: -------------------------------------------------------------------------------- 1 | interval_unit) { 68 | $model->interval_unit = 'month'; 69 | } 70 | 71 | if (!$model->interval_count) { 72 | $model->interval_count = 1; 73 | } 74 | }); 75 | } 76 | 77 | /** 78 | * Plan constructor. 79 | * 80 | * @param array $attributes 81 | */ 82 | public function __construct(array $attributes = []) 83 | { 84 | parent::__construct($attributes); 85 | 86 | $this->setTable(Config::get('plans.tables.plans')); 87 | } 88 | 89 | /** 90 | * Get plan features. 91 | * 92 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 93 | */ 94 | public function features() 95 | { 96 | return $this 97 | ->belongsToMany( 98 | Config::get('plans.models.Feature'), 99 | Config::get('plans.tables.plan_features'), 100 | 'plan_id', 101 | 'feature_id' 102 | ) 103 | ->using(Config::get('plans.models.PlanFeature')) 104 | ->withPivot(['value', 'note']) 105 | ->orderBy('sort_order'); 106 | } 107 | 108 | /** 109 | * Get plan subscriptions. 110 | * 111 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 112 | */ 113 | public function subscriptions() 114 | { 115 | return $this->hasMany( 116 | Config::get('plans.models.PlanSubscription'), 117 | 'plan_id', 118 | 'id' 119 | ); 120 | } 121 | 122 | /** 123 | * Check if plan is free. 124 | * 125 | * @return bool 126 | */ 127 | public function isFree(): bool 128 | { 129 | return ((float)$this->price <= 0.00); 130 | } 131 | 132 | /** 133 | * Check if plan has trial. 134 | * 135 | * @return bool 136 | */ 137 | public function hasTrial(): bool 138 | { 139 | return (is_numeric($this->trial_period_days) and $this->trial_period_days > 0); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Models/PlanFeature.php: -------------------------------------------------------------------------------- 1 | belongsTo( 49 | Config::get('plans.models.Feature'), 50 | 'feature_id', 51 | 'id' 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Models/PlanSubscription.php: -------------------------------------------------------------------------------- 1 | ends_at) { 99 | $model->setNewPeriod(); 100 | } 101 | }); 102 | 103 | static::saved(function ($model) { 104 | /** @var PlanSubscription $model */ 105 | if ($model->getOriginal('plan_id') && $model->getOriginal('plan_id') !== $model->plan_id) { 106 | Event::dispatch(new SubscriptionPlanChanged($model)); 107 | } 108 | }); 109 | } 110 | 111 | /** 112 | * Plan constructor. 113 | * 114 | * @param array $attributes 115 | */ 116 | public function __construct(array $attributes = []) 117 | { 118 | parent::__construct($attributes); 119 | 120 | $this->setTable(Config::get('plans.tables.plan_subscriptions')); 121 | } 122 | 123 | /** 124 | * Get subscriber. 125 | * 126 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 127 | */ 128 | public function subscriber() 129 | { 130 | return $this->morphTo(); 131 | } 132 | 133 | /** 134 | * Get subscription usage. 135 | * 136 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 137 | */ 138 | public function usage() 139 | { 140 | return $this->hasMany( 141 | Config::get('plans.models.PlanSubscriptionUsage'), 142 | 'subscription_id', 143 | 'id' 144 | ); 145 | } 146 | 147 | /** 148 | * Get status attribute. 149 | * 150 | * @return string 151 | */ 152 | public function getStatusAttribute() 153 | { 154 | if ($this->isActive()) { 155 | return self::STATUS_ACTIVE; 156 | } 157 | 158 | if ($this->isCanceled()) { 159 | return self::STATUS_CANCELED; 160 | } 161 | 162 | if ($this->isEnded()) { 163 | return self::STATUS_ENDED; 164 | } 165 | } 166 | 167 | /** 168 | * Check if subscription is active. 169 | * 170 | * @return bool 171 | */ 172 | public function isActive(): bool 173 | { 174 | if (!$this->isEnded() || $this->onTrial()) { 175 | return true; 176 | } 177 | 178 | return false; 179 | } 180 | 181 | /** 182 | * Check if subscription is trialling. 183 | * 184 | * @return bool 185 | */ 186 | public function onTrial(): bool 187 | { 188 | if (!is_null($trialEndsAt = $this->trial_ends_at)) { 189 | return Carbon::now()->lt(Carbon::instance($trialEndsAt)); 190 | } 191 | 192 | return false; 193 | } 194 | 195 | /** 196 | * Check if subscription is canceled. 197 | * 198 | * @return bool 199 | */ 200 | public function isCanceled(): bool 201 | { 202 | return !is_null($this->canceled_at); 203 | } 204 | 205 | /** 206 | * Check if subscription is canceled immediately. 207 | * 208 | * @return bool 209 | */ 210 | public function isCanceledImmediately(): bool 211 | { 212 | return !is_null($this->canceled_at) && $this->canceled_immediately === true; 213 | } 214 | 215 | /** 216 | * Check if subscription period has ended. 217 | * 218 | * @return bool 219 | */ 220 | public function isEnded(): bool 221 | { 222 | $endsAt = Carbon::instance($this->ends_at); 223 | 224 | return Carbon::now()->gte($endsAt); 225 | } 226 | 227 | /** 228 | * Cancel subscription. 229 | * 230 | * @param bool $immediately 231 | * @return PlanSubscription 232 | * @throws \Throwable 233 | */ 234 | public function cancel($immediately = false) 235 | { 236 | $this->canceled_at = Carbon::now(); 237 | 238 | if ($immediately) { 239 | $this->canceled_immediately = true; 240 | $this->ends_at = $this->canceled_at; 241 | } 242 | 243 | $this->saveOrFail(); 244 | 245 | Event::dispatch(new SubscriptionCanceled($this)); 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * Change subscription plan. 252 | * 253 | * @param int|\Laravel\PricingPlans\Models\Plan $plan Plan Id or Plan Model Instance 254 | * @return PlanSubscription|false 255 | * @throws InvalidArgumentException 256 | */ 257 | public function changePlan($plan) 258 | { 259 | if (!($plan instanceof Plan)) { 260 | // Try find by Plan ID 261 | $plan = App::make(Config::get('plans.models.Plan'))->find($plan); 262 | 263 | if (!$plan) { 264 | // Try find by Plan Code 265 | $plan = App::make(Config::get('plans.models.Plan'))->findByCode($plan); 266 | } 267 | } 268 | 269 | if (is_null($plan) || !($plan instanceof Plan)) { 270 | throw new InvalidArgumentException('Invalid plan instance'); 271 | } 272 | 273 | // If plans doesn't have the same billing frequency (e.g., interval 274 | // and interval_count) we will update the billing dates starting 275 | // today... and since we are basically creating a new billing cycle, 276 | // the usage data will be cleared. 277 | if (is_null($this->plan) || 278 | $this->plan->interval_unit !== $plan->interval_unit || 279 | $this->plan->interval_count !== $plan->interval_count 280 | ) { 281 | // Set period 282 | $this->setNewPeriod($plan->interval_unit, $plan->interval_count); 283 | 284 | // Clear usage data 285 | $usageManager = new SubscriptionUsageManager($this); 286 | $usageManager->clear(); 287 | } 288 | 289 | // Attach new plan to subscription 290 | $this->plan_id = $plan->id; 291 | 292 | return $this; 293 | } 294 | 295 | /** 296 | * Renew subscription period. 297 | * 298 | * @return self 299 | * @throws LogicException 300 | */ 301 | public function renew() 302 | { 303 | if ($this->isEnded() and $this->isCanceled()) { 304 | throw new LogicException( 305 | 'Unable to renew canceled ended subscription.' 306 | ); 307 | } 308 | 309 | $subscription = $this; 310 | 311 | DB::transaction(function () use ($subscription) { 312 | // Clear usage data 313 | $usageManager = new SubscriptionUsageManager($subscription); 314 | $usageManager->clear(); 315 | 316 | // Renew period 317 | $subscription->setNewPeriod(); 318 | $subscription->canceled_at = null; 319 | $subscription->save(); 320 | }); 321 | 322 | Event::dispatch(new SubscriptionRenewed($this)); 323 | 324 | return $this; 325 | } 326 | 327 | /** 328 | * Get Subscription Ability instance. 329 | * 330 | * @return \Laravel\PricingPlans\SubscriptionAbility 331 | */ 332 | public function ability() 333 | { 334 | if (is_null($this->ability)) { 335 | return new SubscriptionAbility($this); 336 | } 337 | 338 | return $this->ability; 339 | } 340 | 341 | /** 342 | * Find by user id. 343 | * 344 | * @param \Illuminate\Database\Eloquent\Builder 345 | * @param \Laravel\PricingPlans\Contracts\Subscriber $subscriber 346 | * @return \Illuminate\Database\Eloquent\Builder 347 | */ 348 | public function scopeBySubscriber($query, $subscriber) 349 | { 350 | return $query->where('subscriber_id', $subscriber->getKey()) 351 | ->where('subscriber_type', get_class($subscriber)); 352 | } 353 | 354 | /** 355 | * Find subscription with an ending trial. 356 | * 357 | * @param \Illuminate\Database\Eloquent\Builder $query 358 | * @param int $dayRange 359 | * @return \Illuminate\Database\Eloquent\Builder 360 | */ 361 | public function scopeFindEndingTrial($query, $dayRange = 3) 362 | { 363 | $from = Carbon::now(); 364 | $to = Carbon::now()->addDays($dayRange); 365 | 366 | return $query->whereBetween('trial_ends_at', [$from, $to]); 367 | } 368 | 369 | /** 370 | * Find subscription with an ended trial. 371 | * 372 | * @param \Illuminate\Database\Eloquent\Builder $query 373 | * @return \Illuminate\Database\Eloquent\Builder 374 | */ 375 | public function scopeFindEndedTrial($query) 376 | { 377 | return $query->where('trial_ends_at', '<=', Carbon::now()); 378 | } 379 | 380 | /** 381 | * Find ending subscriptions. 382 | * 383 | * @param \Illuminate\Database\Eloquent\Builder $query 384 | * @param int $dayRange 385 | * @return \Illuminate\Database\Eloquent\Builder 386 | */ 387 | public function scopeFindEndingPeriod($query, $dayRange = 3) 388 | { 389 | $from = Carbon::now(); 390 | $to = Carbon::now()->addDays($dayRange); 391 | 392 | return $query->whereBetween('ends_at', [$from, $to]); 393 | } 394 | 395 | /** 396 | * Find ended subscriptions. 397 | * 398 | * @param \Illuminate\Database\Eloquent\Builder $query 399 | * @return \Illuminate\Database\Eloquent\Builder 400 | */ 401 | public function scopeFindEndedPeriod($query) 402 | { 403 | return $query->where('ends_at', '<=', Carbon::now()); 404 | } 405 | 406 | /** 407 | * Set subscription period. 408 | * 409 | * @param string $intervalUnit 410 | * @param int $intervalCount 411 | * @param null|int|string|\DateTime $startAt Start time 412 | * @return PlanSubscription 413 | */ 414 | protected function setNewPeriod(string $intervalUnit = '', int $intervalCount = 0, $startAt = null) 415 | { 416 | if (empty($intervalUnit)) { 417 | $intervalUnit = $this->plan->interval_unit; 418 | } 419 | 420 | if (empty($intervalCount)) { 421 | $intervalCount = $this->plan->interval_count; 422 | } 423 | 424 | $period = new Period($intervalUnit, $intervalCount, $startAt); 425 | 426 | $this->starts_at = $period->getStartAt(); 427 | $this->ends_at = $period->getEndAt(); 428 | 429 | return $this; 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/Models/PlanSubscriptionUsage.php: -------------------------------------------------------------------------------- 1 | setTable(Config::get('plans.tables.plan_subsription_usages')); 54 | } 55 | 56 | /** 57 | * Get feature. 58 | * 59 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 60 | */ 61 | public function feature() 62 | { 63 | return $this->belongsTo( 64 | Config::get('plans.models.Feature'), 65 | 'feature_code', 66 | 'code' 67 | ); 68 | } 69 | 70 | /** 71 | * Get subscription. 72 | * 73 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 74 | */ 75 | public function subscription() 76 | { 77 | return $this->belongsTo( 78 | Config::get('plans.models.PlanSubscription'), 79 | 'subscription_id', 80 | 'id' 81 | ); 82 | } 83 | 84 | /** 85 | * Scope by feature code. 86 | * 87 | * @param \Illuminate\Database\Eloquent\Builder $query 88 | * @param string|\Laravel\PricingPlans\Models\Feature $feature 89 | * @return \Illuminate\Database\Eloquent\Builder 90 | */ 91 | public function scopeByFeature($query, $feature) 92 | { 93 | return $query->where('feature_code', $feature instanceof Feature ? $feature->code : $feature); 94 | } 95 | 96 | /** 97 | * Check whether usage has been expired or not. 98 | * 99 | * @return bool 100 | */ 101 | public function isExpired() 102 | { 103 | if (is_null($this->valid_until)) { 104 | return false; 105 | } 106 | 107 | return Carbon::now()->gte($this->valid_until); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Period.php: -------------------------------------------------------------------------------- 1 | 'addDays', 27 | self::WEEK => 'addWeeks', 28 | self::MONTH => 'addMonths', 29 | self::YEAR => 'addYears', 30 | ]; 31 | 32 | /** 33 | * Starting date of the period. 34 | * 35 | * @var \Carbon\Carbon 36 | */ 37 | protected $startAt; 38 | 39 | /** 40 | * Ending date of the period. 41 | * 42 | * @var \Carbon\Carbon 43 | */ 44 | protected $endAt; 45 | 46 | /** 47 | * Interval 48 | * 49 | * @var string 50 | */ 51 | protected $intervalUnit; 52 | 53 | /** 54 | * Interval count 55 | * 56 | * @var int 57 | */ 58 | protected $intervalCount = 1; 59 | 60 | /** 61 | * Create a new Period instance. 62 | * 63 | * @param string $intervalUnit Interval Unit 64 | * @param int $intervalCount Interval count 65 | * @param null|string|int|\DateTime $startAt Starting point 66 | * @throws InvalidArgumentException 67 | */ 68 | public function __construct(string $intervalUnit = 'month', int $intervalCount = 1, $startAt = null) 69 | { 70 | if ($startAt instanceof DateTime) { 71 | $this->startAt = Carbon::instance($startAt); 72 | } elseif (is_int($startAt)) { 73 | $this->startAt = Carbon::createFromTimestamp($startAt); 74 | } elseif (empty($startAt)) { 75 | $this->startAt = new Carbon(); 76 | } else { 77 | $this->startAt = Carbon::parse($startAt); 78 | } 79 | 80 | if (!self::isValidIntervalUnit($intervalUnit)) { 81 | throw new InvalidArgumentException("Interval unit `{$intervalUnit}` is invalid"); 82 | } 83 | 84 | $this->intervalUnit = $intervalUnit; 85 | 86 | if ($intervalCount >= 0) { 87 | $this->intervalCount = $intervalCount; 88 | } 89 | 90 | $this->calculate(); 91 | } 92 | 93 | /** 94 | * Get start date. 95 | * 96 | * @return \Carbon\Carbon 97 | */ 98 | public function getStartAt() 99 | { 100 | return $this->startAt; 101 | } 102 | 103 | /** 104 | * Get end date. 105 | * 106 | * @return \Carbon\Carbon 107 | */ 108 | public function getEndAt() 109 | { 110 | return $this->endAt; 111 | } 112 | 113 | /** 114 | * Get period interval. 115 | * 116 | * @return string 117 | */ 118 | public function getIntervalUnit() 119 | { 120 | return $this->intervalUnit; 121 | } 122 | 123 | /** 124 | * Get period interval count. 125 | * 126 | * @return int 127 | */ 128 | public function getIntervalCount() 129 | { 130 | return $this->intervalCount; 131 | } 132 | 133 | /** 134 | * Calculate the end date of the period. 135 | * 136 | * @return void 137 | */ 138 | protected function calculate() 139 | { 140 | $method = $this->getMethod(); 141 | $this->endAt = (clone $this->startAt)->$method($this->intervalCount); 142 | } 143 | 144 | /** 145 | * Get computation method. 146 | * 147 | * @return string 148 | */ 149 | protected function getMethod() 150 | { 151 | return self::$intervalMapping[$this->intervalUnit]; 152 | } 153 | 154 | /** 155 | * Get all available intervals. 156 | * 157 | * @return array 158 | */ 159 | public static function getAllIntervals() 160 | { 161 | $intervals = []; 162 | 163 | foreach (array_keys(self::$intervalMapping) as $interval) { 164 | $intervals[$interval] = Lang::trans('plans::messages.' . $interval); 165 | } 166 | 167 | return $intervals; 168 | } 169 | 170 | /** 171 | * Check if a given interval is valid. 172 | * 173 | * @param string $intervalUnit 174 | * @return bool 175 | */ 176 | public static function isValidIntervalUnit($intervalUnit): bool 177 | { 178 | return array_key_exists($intervalUnit, self::$intervalMapping); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/PricingPlansServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom($pkg . '/lang', 'plans'); 24 | 25 | $this->publishes([ 26 | $pkg . '/migrations/2018_01_01_000000_create_plans_tables.php' 27 | => database_path('migrations/' . date('Y_m_d_His') . '_create_plans_tables.php') 28 | ], 'migrations'); 29 | 30 | $this->publishes([ 31 | $pkg . '/config/plans.php' => config_path('plans.php') 32 | ], 'config'); 33 | 34 | $this->publishes([ 35 | $pkg . '/lang' => resource_path('lang/vendor/plans'), 36 | ]); 37 | } 38 | 39 | /** 40 | * Register the application services. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->mergeConfigFrom(__DIR__ . '/../resources/config/plans.php', 'plans'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SubscriptionAbility.php: -------------------------------------------------------------------------------- 1 | subscription = $subscription; 25 | } 26 | 27 | /** 28 | * Determine if the feature is enabled and has 29 | * available uses. 30 | * 31 | * @param string $featureCode 32 | * @return bool 33 | */ 34 | public function canUse(string $featureCode): bool 35 | { 36 | // Get features and usage 37 | $featureValue = $this->value($featureCode); 38 | 39 | if (is_null($featureValue)) { 40 | return false; 41 | } 42 | 43 | // Match "boolean" type value 44 | if ($this->enabled($featureCode) === true) { 45 | return true; 46 | } 47 | 48 | // If the feature value is zero, let's return false 49 | // since there's no uses available. (useful to disable 50 | // countable features) 51 | if ($featureValue === '0') { 52 | return false; 53 | } 54 | 55 | // Check for available uses 56 | return $this->remainings($featureCode) > 0; 57 | } 58 | 59 | /** 60 | * Get how many times the feature has been used. 61 | * 62 | * @param string $featureCode 63 | * @return int 64 | */ 65 | public function consumed(string $featureCode): int 66 | { 67 | /** @var \Laravel\PricingPlans\Models\PlanSubscriptionUsage $usage */ 68 | foreach ($this->subscription->usage as $usage) { 69 | if ($usage->feature_code === $featureCode && !$usage->isExpired()) { 70 | return (int) $usage->used; 71 | } 72 | } 73 | 74 | return 0; 75 | } 76 | 77 | /** 78 | * Get the available uses. 79 | * 80 | * @param string $featureCode 81 | * @return int 82 | */ 83 | public function remainings(string $featureCode): int 84 | { 85 | return (int)$this->value($featureCode) - $this->consumed($featureCode); 86 | } 87 | 88 | /** 89 | * Check if subscription plan feature is enabled. 90 | * 91 | * @param string $featureCode 92 | * @return bool 93 | */ 94 | public function enabled(string $featureCode): bool 95 | { 96 | $featureValue = $this->value($featureCode); 97 | 98 | if (is_null($featureValue)) { 99 | return false; 100 | } 101 | 102 | // If value is one of the positive words configured then the 103 | // feature is enabled. 104 | if (in_array(strtoupper($featureValue), Config::get('plans.positive_words'))) { 105 | return true; 106 | } 107 | 108 | return false; 109 | } 110 | 111 | /** 112 | * Get feature value. 113 | * 114 | * @param string$featureCode 115 | * @param mixed $default 116 | * @return mixed 117 | */ 118 | public function value(string $featureCode, $default = null) 119 | { 120 | if (!$this->subscription->plan->relationLoaded('features')) { 121 | $this->subscription->plan->features()->getEager(); 122 | } 123 | 124 | /** @var \Laravel\PricingPlans\Models\Feature $feature */ 125 | foreach ($this->subscription->plan->features as $feature) { 126 | if ($featureCode === $feature->code) { 127 | return $feature->pivot->value; 128 | } 129 | } 130 | 131 | return $default; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/SubscriptionBuilder.php: -------------------------------------------------------------------------------- 1 | subscriber = $subscriber; 58 | $this->name = $name; 59 | $this->plan = $plan; 60 | } 61 | 62 | /** 63 | * Specify the trial duration period in days. 64 | * 65 | * @param int $trialDays 66 | * @return self 67 | */ 68 | public function trialDays(int $trialDays) 69 | { 70 | $this->trialDays = $trialDays; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Do not apply trial to the subscription. 77 | * 78 | * @return self 79 | */ 80 | public function skipTrial() 81 | { 82 | $this->skipTrial = true; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Create a new subscription. 89 | * 90 | * @param array $attributes 91 | * @return \Laravel\PricingPlans\Models\PlanSubscription 92 | */ 93 | public function create(array $attributes = []) 94 | { 95 | $now = Carbon::now(); 96 | 97 | if ($this->skipTrial) { 98 | $trialEndsAt = null; 99 | } elseif ($this->trialDays) { 100 | $trialEndsAt = $now->addDays($this->trialDays); 101 | } elseif ($this->plan->hasTrial()) { 102 | $trialEndsAt = $now->addDays($this->plan->trial_period_days); 103 | } else { 104 | $trialEndsAt = null; 105 | } 106 | 107 | return $this->subscriber->subscriptions()->create(array_replace([ 108 | 'plan_id' => $this->plan->id, 109 | 'trial_ends_at' => $trialEndsAt, 110 | 'name' => $this->name 111 | ], $attributes)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/SubscriptionUsageManager.php: -------------------------------------------------------------------------------- 1 | subscription = $subscription; 25 | } 26 | 27 | /** 28 | * Record usage. 29 | * 30 | * This will create or update a usage record. 31 | * 32 | * @param string $featureCode 33 | * @param int $uses 34 | * @param bool $incremental 35 | * @return \Laravel\PricingPlans\Models\PlanSubscriptionUsage 36 | * @throws \Throwable 37 | */ 38 | public function record(string $featureCode, $uses = 1, $incremental = true) 39 | { 40 | /** @var \Laravel\PricingPlans\Models\Feature $feature */ 41 | $feature = Feature::code($featureCode)->first(); 42 | 43 | $usage = $this->subscription->usage()->firstOrNew([ 44 | 'feature_code' => $feature->code, 45 | ]); 46 | 47 | if ($feature->isResettable()) { 48 | // Set expiration date when the usage record is new or doesn't have one. 49 | if (is_null($usage->valid_until)) { 50 | // Set date from subscription creation date so the reset period match the period specified 51 | // by the subscription's plan. 52 | $usage->valid_until = $feature->getResetTime($this->subscription->created_at); 53 | } elseif ($usage->isExpired()) { 54 | // If the usage record has been expired, let's assign 55 | // a new expiration date and reset the uses to zero. 56 | $usage->valid_until = $feature->getResetTime($usage->valid_until); 57 | $usage->used = 0; 58 | } 59 | } 60 | 61 | $usage->used = max($incremental ? $usage->used + $uses : $uses, 0); 62 | 63 | $usage->saveOrFail(); 64 | 65 | return $usage; 66 | } 67 | 68 | /** 69 | * Reduce usage. 70 | * 71 | * @param int $featureId 72 | * @param int $uses 73 | * @return \Laravel\PricingPlans\Models\PlanSubscriptionUsage 74 | * @throws \Throwable 75 | */ 76 | public function reduce($featureId, $uses = 1) 77 | { 78 | return $this->record($featureId, -$uses); 79 | } 80 | 81 | /** 82 | * Clear usage data. 83 | * 84 | * @return self 85 | */ 86 | public function clear() 87 | { 88 | $this->subscription->usage()->delete(); 89 | 90 | return $this; 91 | } 92 | } 93 | --------------------------------------------------------------------------------