Laravel Soulbscription - This package provides a straightforward interface to handle subscriptions and features consumption.

Overview

Laravel Soulbscription

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

About

This package provides a straightforward interface to handle subscriptions and features consumption.

Installation

You can install the package via composer:

composer require lucasdotvin/laravel-soulbscription

The package migrations are loaded automatically, but you can still publish them with this command:

php artisan vendor:publish --tag="laravel-soulbscription-migrations"
php artisan migrate

Upgrades

If you already use this package and need to move to a newer version, don't forget to publish the upgrade migrations:

php artisan vendor:publish --tag="laravel-soulbscription-migrations-upgrades"
php artisan migrate

Usage

To start using it, you just have to add the given trait to your User model (or any entity you want to have subscriptions):



namespace App\Models;

use LucasDotVin\Soulbscription\Models\Concerns\HasSubscriptions;

class User
{
    use HasSubscriptions;
}

And that's it!

Setting Features Up

First things first, you have to define the features you'll offer. In the example below, we are creating two features: one to handle how much minutes each user can spend with deploys and if they can use subdomains.



namespace Database\Seeders;

use Illuminate\Database\Seeder;
use LucasDotVin\Soulbscription\Enums\PeriodicityType;
use LucasDotVin\Soulbscription\Models\Feature;

class FeatureSeeder extends Seeder
{
    public function run()
    {
        $deployMinutes = Feature::create([
            'consumable'       => true,
            'name'             => 'deploy-minutes',
            'periodicity_type' => PeriodicityType::Day,
            'periodicity'      => 1,
        ]);

        $customDomain = Feature::create([
            'consumable' => false,
            'name'       => 'custom-domain',
        ]);
    }
}

By saying the deploy-minutes is a consumable feature, we are telling the users can use it a limited number of times (or until a given amount). On the other hand, by passing PeriodicityType::Day and 1 as its periodicity_type and periodicity respectively, we said that it should be renewed everyday. So a user could spend his minutes today and have it back tomorrow, for instance.

It is important to keep in mind that both plans and consumable features have its periodicity, so your users can, for instance, have a monthly plan with weekly features.

The other feature we defined was $customDomain, which was a not consumable feature. By being not consumable, this feature implies only that the users with access to it can perform a given action (in this case, use a custom domain).

Creating Plans

Now you need to define the plans available to subscription in your app:



namespace Database\Seeders;

use Illuminate\Database\Seeder;
use LucasDotVin\Soulbscription\Enums\PeriodicityType;
use LucasDotVin\Soulbscription\Models\Plan;

class PlanSeeder extends Seeder
{
    public function run()
    {
        $silver = Plan::create([
            'name'             => 'silver',
            'periodicity_type' => PeriodicityType::Month,
            'periodicity'      => 1,
        ]);

        $gold = Plan::create([
            'name'             => 'gold',
            'periodicity_type' => PeriodicityType::Month,
            'periodicity'      => 1,
        ]);
    }
}

Everything here is quite simple, but it is worth to emphasize: by receiving the periodicity options above, the two plans are defined as monthly.

Grace Days

You can define a number of grace days to each plan, so your users will not loose access to their features immediately on expiration:

$gold = Plan::create([
    'name'             => 'gold',
    'periodicity_type' => PeriodicityType::Month,
    'periodicity'      => 1,
    'grace_days'       => 7,
]);

With the configuration above, the subscribers of the "gold" plan will have seven days between the plan expiration and their access being suspended.

Associating Plans with Features

As each feature can belong to multiple plans (and they can have multiple features), you have to associate them:

use LucasDotVin\Soulbscription\Models\Feature;

// ...

$deployMinutes = Feature::whereName('deploy-minutes')->first();
$subdomains    = Feature::whereName('subdomains')->first();

$silver->features()->attach($deployMinutes, ['charges' => 15]);

$gold->features()->attach($deployMinutes, ['charges' => 25]);
$gold->features()->attach($subdomains);

It is necessary to pass a value to charges when associating a consumable feature with a plan.

In the example above, we are giving 15 minutes of deploy time to silver users and 25 to gold users. We are also allowing gold users to use subdomains.

Subscribing

Now that you have a set of plans with their own features, it is time to subscribe users to them. Registering subscriptions is quite simple:



namespace App\Listeners;

use App\Events\PaymentApproved;

class SubscribeUser
{
    public function handle(PaymentApproved $event)
    {
        $subscriber = $event->user;
        $plan       = $event->plan;

        $subscriber->subscribeTo($plan);
    }
}

In the example above, we are simulating an application that subscribes its users when their payments are approved. It is easy to see that the method subscribeTo requires only one argument: the plan the user is subscribing to. There are other options you can pass to it to handle particular cases that we're gonna cover below.

By default, the subscribeTo method calculates the expiration considering the plan periodicity, so you don't have to worry about it.

Defining Expiration and Start Date

You can override the subscription expiration by passing the $expiration argument to the method call. Below, we are setting the subscription of a given user to expire only in the next year.

$subscriber->subscribeTo($plan, expiration: today()->addYear());

It is possible also to define when a subscription will effectively start (the default behavior is to start it immediately):



namespace App\Http\Controllers;

use App\Http\Requests\StudentStoreFormRequest;
use App\Models\Course;
use App\Models\User;
use LucasDotVin\Soulbscription\Models\Plan;

class StudentController extends Controller
{
    public function store(StudentStoreFormRequest $request, Course $course)
    {
        $student = User::make($request->validated());
        $student->course()->associate($course);
        $student->save();

        $plan = Plan::find($request->input('plan_id'));
        $student->subscribeTo($plan, startDate: $course->starts_at);

        return redirect()->route('admin.students.index');
    }
}

Above, we are simulating an application for a school. It has to subscribe students at their registration, but also ensure their subscription will make effect only when the course starts.

Switching Plans

Users change their mind all the time and you have to deal with it. If you need to change the current plan o a user, simply call the method switchTo:

$student->switchTo($newPlan);

If you don't pass any arguments, the method will suppress the current subscription and start a new one immediately.

This call will fire a SubscriptionStarted(Subscription $subscription) event.

Scheduling a Switch

If you want to keep your user with the current plan until its expiration, pass the $immediately parameter as false:

$primeMonthly = Plan::whereName('prime-monthly')->first();
$user->subscribeTo($primeMonthly);

...

$primeYearly = Plan::whereName('prime-yearly')->first();
$user->switchTo($primeYearly, immediately: false);

In the example above, the user will keep its monthly subscription until its expiration and then start on the yearly plan. This is pretty useful when you don't want to deal with partial refunds, as you can bill your user only when the current paid plan expires.

Under the hood, this call will create a subscription with a start date equal to the current expiration, so it won't affect your application until there.

This call will fire a SubscriptionScheduled(Subscription $subscription) event.

Renewing

To renew a subscription, simply call the renew() method:

$subscriber->subscription->renew();

This method will fire a SubscriptionRenewed(Subscription $subscription) event.

It will calculate a new expiration based on the current date.

Canceling

There is a thing to keep in mind when canceling a subscription: it won't revoke the access immediately. To avoid making you need to handle refunds of any kind, we keep the subscription active and just mark it as canceled, so you just have to not renew it in the future. If you need to suppress a subscription immediately, give a look on the method suppress().

To cancel a subscription, use the method cancel():

$subscriber->subscription->cancel();

This method will mark the subscription as canceled by filling the column canceled_at with the current timestamp.

This method will fire a SubscriptionCanceled(Subscription $subscription) event.

Suppressing

To suppress a subscription (and immediately revoke it), use the method suppress():

$subscriber->subscription->suppress();

This method will mark the subscription as suppressed by filling the column suppressed_at with the current timestamp.

This method will fire a SubscriptionSuppressed(Subscription $subscription) event.

Starting

To start a subscription, use the method start():

$subscriber->subscription->start(); // To start it immediately
$subscriber->subscription->start($startDate); // To determine when to start

This method will fire a SubscriptionStarted(Subscription $subscription) event when no argument is passed, and fire a SubscriptionStarted(Subscription $subscription) event when the provided start date is future.

This method will mark the subscription as started (or scheduled to start) by filling the column started_at.

Feature Consumption

To register a consumption of a given feature, you just have to call the consume method and pass the feature name and the consumption amount (you don't need to provide it for not consumable features):

$subscriber->consume('deploy-minutes', 4.5);

The method will check if the feature is available and throws exceptions if they are not: OutOfBoundsException if the feature is not available to the plan, and OverflowException if it is available, but the charges are not enough to cover the consumption.

This call will fire a FeatureConsumed($subscriber, Feature $feature, FeatureConsumption $featureConsumption) event.

Check Availability

To check if a feature is available to consumption, you can use one of the methods below:

$subscriber->canConsume('deploy-minutes', 10);

To check if a user can consume a certain amount of a given feature (it checks if the user has access to the feature and if he has enough remaining charges).

$subscriber->cantConsume('deploy-minutes', 10);

It calls the canConsume() method under the hood and reverse the return.

$subscriber->hasFeature('deploy-minutes');

To simply checks if the user has access to a given feature (without looking for its charges).

$subscriber->missingFeature('deploy-minutes');

Similarly to cantConsume, it returns the reverse of hasFeature.

Feature Tickets

Tickets are a simple way to allow your subscribers to acquire charges for a feature. When a user receives a ticket, he is allowed to consume its charges, just like he would do in a normal subscription. Tickets can be used to extend regular subscriptions-based systems (so you can, for instance, sell more charges of a given feature) or even to build a fully pre-paid service, where your users pay only for what they want to use.

Creating Tickets

To create a ticket, you can use the method giveTicketFor. This method expects the feature name, the expiration and optionally a number of charges (you can ignore it when creating tickets for not consumable features):

$subscriber->giveTicketFor('deploy-minutes', today()->addMonth(), 10);

This method will fire a FeatureTicketCreated($subscriber, Feature $feature, FeatureTicket $featureTicket) event.

In the example above, the user will receive ten more minutes to execute deploys until the next month.

Not Consumable Features

You can create tickets for not consumable features, so your subscribers will receive access to them just for a certain period:

class UserFeatureTrialController extends Controller
{
    public function store(FeatureTrialRequest $request, User $user)
    {
        $featureName = $request->input('feature_name');
        $expiration = today()->addDays($request->input('trial_days'));
        $user->giveTicketFor($featureName, $expiration);

        return redirect()->route('admin.users.show', $user);
    }
}

In the example above, the user will be able to try a feature for a certain amount of days.

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

Comments
  •   LucasDotVin\Soulbscription\Enums\PeriodicityType::getDateDifference(): Argument #3 ($unit) must be of type string, null given, called in /home/schneider/Code/tonote/vendor/lucasdotvin/laravel-soulbscription/src/Models/Concerns/HandlesRecurrence.php on line 20

    LucasDotVin\Soulbscription\Enums\PeriodicityType::getDateDifference(): Argument #3 ($unit) must be of type string, null given, called in /home/schneider/Code/tonote/vendor/lucasdotvin/laravel-soulbscription/src/Models/Concerns/HandlesRecurrence.php on line 20

    LucasDotVin\Soulbscription\Enums\PeriodicityType::getDateDifference(): Argument #3 ($unit) must be of type string, null given, called in /home/schneider/Code/tonote/vendor/lucasdotvin/laravel-soulbscription/src/Models/Concerns/HandlesRecurrence.php on line 20

    bug good first issue question 
    opened by Schneidershades 7
  • Migrations issue when using in mutli-tenant architecture

    Migrations issue when using in mutli-tenant architecture

    Hi Lucas,

    First of all thank you for your amazing package. I am using this package in a saas and the landlord migration runs fine. The problem is when migrating the tenant database it also runs in the tenant db even though it is not needed because of the loadMigrationsFrom in the boot function of the provider. It might be helpful to not call the loadMigrationsFrom and let each user publish the migration for people that might want to use it in a saas.

    Thank you for your time.

    enhancement good first issue 
    opened by FabriceAbbey 6
  • got error why trying to migrate

    got error why trying to migrate

       Illuminate\Database\QueryException 
    
      SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'expired_at' (SQL: create table `subscriptions` (`id` bigint unsigned not null auto_increment primary key, `plan_id` bigint unsigned not null, `canceled_at` timestamp null, `expired_at` timestamp not null, `grace_days_ended_at` timestamp null, `started_at` date not null, `suppressed_at` timestamp null, `was_switched` tinyint(1) not null default '0', `deleted_at` timestamp null, `created_at` timestamp null, `updated_at` timestamp null, `subscriber_type` varchar(255) not null, `subscriber_id` bigint unsigned not null) default character set utf8mb4 collate 'utf8mb4_unicode_ci')
    good first issue question 
    opened by Temian1 5
  • SQLSTATE[HY000]: General error: 1364 Field 'subscriber_type' doesn't have a default value

    SQLSTATE[HY000]: General error: 1364 Field 'subscriber_type' doesn't have a default value

    When I tried using $user->subscribeTo($plan), there something returned SQLSTATE[HY000]: General error: 1364 Field 'subscriber_type' doesn't have a default value. Then after spending some hours I manually modified the Subscription.php eloquent model and the issue got fixed. I don't know why it happened but the project worked fine after installation and suddenly this issue came. I'm wondering that reverting git commits didn't fix the issue! (I did composer update). Using laravel 9.3.1

    What I have done to fix: added the 2nd element to the array in $this->fill() function parameters of start() function in Subscription.php file.

    [
                    'started_at' => $startDate,
                    'subscriber_type' => User::class,
    ]
    
    bug good first issue 
    opened by Ign0r3dH4x0r 4
  • Bugfix: The balance of the feature was miscalculated and was considering every subscriber

    Bugfix: The balance of the feature was miscalculated and was considering every subscriber

    The issue is that when a feature ticket is assigned to the subscriber, and if there are more than 1 subscriber for the same ticket, the package loads the balance as summed up for all the feature tickets.

    The whereHas has the scope for who is the subscriber, but the with misses the scope, and because of that, when the feature tickets for a particular feature is loaded, it loaded summed up charges for the tickets assigned to anyone.

    Example:

    I assign 5 charges as a feature ticket when a user confirms their account:

    $user->giveTicketFor('document', null, 5);
    

    So this happens for each user who registered and confirmed their account.

    Now when we see how much balance for each user has using:

    $user->balance('document')
    

    If the system has only 1 user, it will return 5.

    If there are more than one users, say n users, it will return 5 * n.

    bug good first issue 
    opened by khatriafaz 3
  • Switching subscription plans can results in negative balance

    Switching subscription plans can results in negative balance

    Hi, great package! I'm using it together with cashier and noticed an issue with feature consumption when switching plans.

    Imagine a scenario where you have Plan A (100 min.) and Plan B (25 min.)

    The user subscribes to Plan A, uses 50 minutes, and switches then to Plan B. The user has now (negative) -25 min. balance.

    Maybe I missed something in the documentation?

    Cheers

    bug good first issue 
    opened by martin-ro 3
  • Unable to get the plan of the expired subscription

    Unable to get the plan of the expired subscription

    I think this is deliberate (or I may be missing some functionality), but I must report it because it makes working with this package a lot harder that it should.

    Once a subscription has ended, it stops retrieving it and returns null. What if I want to access the plan of the expired subscription? Currently, it's not possible because you just get a null.

    The documentation doesn't mention anything about renewing a subscription, how do I renew a subscription that is null?

    documentation good first issue 
    opened by LuanHimmlisch 3
  • Dependencies conflict

    Dependencies conflict

    Hi, I'm trying to install your package on a fresh Laravel install (v 9.13) and also in an another package as a dependency.

    I got this :

    Your requirements could not be resolved to an installable set of packages.
    
      Problem 1
        - Root composer.json requires lucasdotvin/laravel-soulbscription ^2.0 -> satisfiable by lucasdotvin/laravel-soulbscription[2.0.0, 2.0.1].
        - lucasdotvin/laravel-soulbscription[2.0.0, ..., 2.0.1] require illuminate/contracts ^8.73 -> found illuminate/contracts[v8.73.0, ..., 8.x-dev] but these were not loaded, likely because it conflicts with another require.
    

    And with dev dependencies, it seems that the orchestra/testbench package used is out to date.

    Cheers.

    bug good first issue 
    opened by jahazzz 3
  • See all subscriptions a user has purchased

    See all subscriptions a user has purchased

    Hey Lucas! I'm working on a course management system and using your package to maintain subscriptions. There will be various categories like Python, Excel, Linux. The user can purchase subscription of these categories to get their queries resolved. A user can purchase a plan of subscription of many categories (e.g., I can have a different plan for Python and another plan for Excel at the same time). I'm using auth()->user()->subscription But this is returning the latest purchased plan only. I want to show the user all the plans he has subscribed to. How is it possible through this package could you please tell me?

    This is not a bug issue. I had to raise a bug issue because the other issue raising options were giving me a 404 error in your repository. Please reply as soon as possible.

    enhancement good first issue 
    opened by DezyNation 2
  • Subscription Renewal Issue

    Subscription Renewal Issue

    Hi Again in the read me file you mentioned about the renewal "It will calculate a new expiration based on the current date".

    If you consider the normal scenario where one wants to renew one week before expiry to maintain active subscription. in this case will renewed subscription start from the date of renewal? or starts form the expiry date of the current subscription which should be the case.

    bug documentation good first issue 
    opened by centrust 2
  • Could this package be used together with laravel/cashier-paddle?

    Could this package be used together with laravel/cashier-paddle?

    This isn't an issue, but more a question/discussion (discussions tab is disabled):

    I've stumbled upon this great package while looking for a "usage-engine" to replace my home-grown one in a side project. I love how flexible soulbscription is.

    • quote features
    • tickets
    • different periodicity_type

    My problem: The project already uses laravel/cashier-paddle to handle billing and subscriptions.

    cashier-paddle already exposes a subscription-method on the Model, that would use HasSubscriptions. So I run into a FatalError when adding the trait to the User-model for example.

    The table name clash with my existing app, but that can be solved by overrding the $table-property and using my own version of the Subscription-model.

    So long story short:

    • Do you have any guidance on how the package could be used together with laravel/cashier-paddle/laravel/cashier?
    • Is there a way to "just" use the feature consumption part of the package?

    Haven't invested much time yet, so maybe this can all be solved in "userland". But maybe someone here already has the perfect solution for this.

    Thanks again for publishing this package and sharing how a usage-engine can be built. ❤️

    enhancement good first issue 
    opened by stefanzweifel 2
  • Plans and consumption for the same plan but diferent PeriodicityType

    Plans and consumption for the same plan but diferent PeriodicityType

    Hello! I'm reading your doc to create plans https://github.com/lucasdotvin/laravel-soulbscription?ref=madewithlaravel.com#creating-plans and you give an example to create a monthly plan, but what happens if the same plan has Quarterly and Annual PeriodicityType? how would I do it in that case?

    $gold = Plan::create([
                'name'             => 'gold',
                'periodicity_type' => PeriodicityType::Month,
                'periodicity'      => 1,
            ]);
    
    $gold = Plan::create([
                'name'             => 'gold',
                'periodicity_type' => PeriodicityType::Month,
                'periodicity'      => 3,
            ]);
    
    $gold = Plan::create([
                'name'             => 'gold',
                'periodicity_type' => PeriodicityType::Month,
                'periodicity'      => 12,
            ]);
    `
    it would be like this?
    And how would it work the consumption of features be in those cases?
    
    Regards
    opened by pixsolution 0
Releases(3.0.1)
Owner
Lucas Vinicius
I'm a back-end developer and it says a lot about me.
Lucas Vinicius
A straightforward implementation of the Circuit Breaker pattern for Laravel 9

Laravel Circuit Breaker A straightforward implementation of the Circuit Breaker pattern for Laravel Framework 9 (using Memcached). Installation You ca

Leonardo Vieira 2 Mar 22, 2022
Laravel User Activity Log - a package for Laravel 8.x that provides easy to use features to log the activities of the users of your Laravel app

Laravel User Activity Log - a package for Laravel 8.x that provides easy to use features to log the activities of the users of your Laravel app

null 9 Dec 14, 2022
A laravel package to handle cascade delete and restore on model relations.

Laravel Model Soft Cascade A laravel package to handle cascade delete and restore on model relations. This package not only handle the cascade delete

Touhidur Rahman 18 Apr 29, 2022
27Laracurl Laravel wrapper package for PHP cURL class that provides OOP interface to cURL. [10/27/2015] View Details

Laracurl Laravel cURL Wrapper for Andreas Lutro's OOP cURL Class Installation To install the package, simply add the following to your Laravel install

zjango 8 Sep 9, 2018
Laravel breeze is a PHP Laravel library that provides Authentication features such as Login page , Register, Reset Password and creating all Sessions Required.

About Laravel breeze To give you a head start building your new Laravel application, we are happy to offer authentication and application starter kits

null 3 Jul 30, 2022
A laravel package to handle sanitize process of model data to create/update model records.

Laravel Model UUID A simple package to sanitize model data to create/update table records. Installation Require the package using composer: composer r

null 66 Sep 19, 2022
A simple laravel package to handle multiple key based model route binding

Laravel Model UUID A simple package to handle the multiple key/column based route model binding for laravel package Installation Require the package u

null 13 Mar 2, 2022
A laravel package to handle model specific additional meta fields in an elegant way.

Laravel Meta Fields A php package for laravel framework to handle model meta data in a elegant way. Installation Require the package using composer: c

Touhidur Rahman 26 Apr 5, 2022
A package to handle the SEO in any Laravel application, big or small.

Never worry about SEO in Laravel again! Currently there aren't that many SEO-packages for Laravel and the available ones are quite complex to set up a

Ralph J. Smit 267 Jan 2, 2023
Simple package to handle response properly in your API.

Simple package to handle response properly in your API. This package uses Fractal and is based on Build APIs You Won't Hate book.

Optania 375 Oct 28, 2022
Laravel 4.* and 5.* service providers to handle PHP errors, dump variables, execute PHP code remotely in Google Chrome

Laravel 4.* service provider for PHP Console See https://github.com/barbushin/php-console-laravel/releases/tag/1.2.1 Use "php-console/laravel-service-

Sergey 73 Jun 1, 2022
Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

Introduction Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services. It handles almost all of the boilerpl

The Laravel Framework 2.2k Jan 4, 2023
Laravel Cashier Paddle provides an expressive, fluent interface to Paddle's subscription billing services.

Introduction Laravel Cashier Paddle provides an expressive, fluent interface to Paddle's subscription billing services. It handles almost all of the b

The Laravel Framework 189 Jan 5, 2023
A simple laravel state machine to handle model transitions, based on a pre-defined list of rules

A simple state machine that allows transitioning model states based on pre-defined rules. Installation You can install the package via composer: compo

Jack Mollart 18 Apr 2, 2022
Handle all the hard stuff related to EU MOSS tax/vat regulations, the way it should be.

VatCalculator Handle all the hard stuff related to EU MOSS tax/vat regulations, the way it should be. Integrates with Laravel and Cashier — or in a st

Dries Vints 1.1k Jan 5, 2023
Handle all the hard stuff related to EU MOSS tax/vat regulations, the way it should be.

VatCalculator Handle all the hard stuff related to EU MOSS tax/vat regulations, the way it should be. Integrates with Laravel and Cashier — or in a st

Dries Vints 1.1k Jan 7, 2023
Localization Helper - Package for convenient work with Laravel's localization features and fast language files generation

Localization Helper Package for convenient work with Laravel's localization features and fast language files generation. Installation Via Composer $ c

Galymzhan Begimov 0 Jul 13, 2019
Evo is a Laravel package that leverages PHP 8 features.

Evo is a Laravel package that leverages PHP 8 features. It change the way you write Laravel app into something like this: #[RoutePrefix('users')] clas

Muhammad Syifa 14 Jun 1, 2022
This package is to add a web interface for Laravel 5 and earlier Artisan.

Nice Artisan This package is to add a web interface for Laravel 5 and earlier Artisan. Installation Add Nice Artisan to your composer.json file : For

null 218 Nov 29, 2022