Coder Social home page Coder Social logo

bpuig / laravel-subby Goto Github PK

View Code? Open in Web Editor NEW
100.0 100.0 39.0 614 KB

Laravel Plan and Subscriptions manager.

Home Page: https://bpuig.github.io/laravel-subby

License: MIT License

PHP 100.00%
laravel laravel-subscriptions subscriptions-manager

laravel-subby's People

Contributors

boryn avatar bpuig avatar serdartaylan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

laravel-subby's Issues

Need a little help

How properly subscribe user to NEW plan and stop from other

For example i have Free plan by default (which i SUBSCRIBE on user signup)
After that user can upgrade to other paid plan

How to do it properly?
Please provide example

Also couldnt find anything about subcrtiption CRON JOBS for autocancellation of old subscriptions

Fallback plan

Let's imagine a user has a "Pro" subscription. They decide to cancel or just not to renew. Actually then we cut them off of all the features, even the ones available in the "Free" plan.

Maybe it would be good to implement an optional fallback/default plan? If they intentionally cancel, we can changePlan() for them, but if the subscription just expires, the user is left with all features blocked because canUseFeature() will just return false :).

How to define the same plans with different payment periods?

Hi @bpuig! Actually what is your way to define the same plan but with different payment periods? I mean we want to have "Basic" plan paid monthly or yearly. I'd go with tags basic-monthly and basic-yearly defining appropriately the price and invoice_interval fields. But with this approach I come into a problem of grabbing and presenting the all payment options for the "Basic" tag. It's quite cumbersome to filter the tag field by LIKE 'basic-%'.

PS. All these "Basic" plans use the same feature set. With my approach I need to duplicate the definitions at plan_features.

I can not install on Laravel 9 !

Problem 1
- bpuig/laravel-subby[v5.0.0, ..., v5.x-dev] require laravel/framework ^6.0|^7.0|^8.0 -> found laravel/framework[v6.0.0, ..., 6.x-dev, v7.0
.0, ..., 7.x-dev, v8.0.0, ..., 8.x-dev] but it conflicts with your root composer.json require (^9.0).
- bpuig/laravel-subby v5.0.2 requires laravel/framework ^v8.38.0 -> found laravel/framework[v8.38.0, ..., 8.x-dev] but it conflicts with yo
ur root composer.json require (^9.0).
- Root composer.json requires bpuig/laravel-subby ^5.0 -> satisfiable by bpuig/laravel-subby[v5.0.0, v5.0.1, v5.0.2, v5.x-dev].

Postpaid settlement mode

Usually subscriptions are paid upfront, in the "prepaid" mode. But there are companies which sign individual contracts with their customers and they allow them to settle the payment later "postpaid" (at the end of billing period), according to the real usage.

It seems quite an easy implementation - we'd need an extra field payment_mode (or settlement_mode) in the plan_subscriptions. Or just prepaid with value 1 as default.

And such a subscription would be always active, independent of ends_at, unless explicitly canceled.

When do you plan to release v5?

Hi @bpuig!

When do you plan to release v5? Now to use the library in the v5 version, one needs to use
composer require bpuig/laravel-subby:dev-main which is not a perfect solution.

I'd rather already switched to using
composer require bpuig/laravel-subby:^5

Method isInGrace() + scope scopeFindInGracePeriod()

I thought that we could need isInGrace() method?

And as well a scope scopeFindInGracePeriod() for getting the subscribers during the grace period ending in X days?

The both methods would be very helpful for displaying appropriate information to the user in the front or to send them email reminders.

Call to a member function getFeatureUsage() on null

The could be some gracefull handling if we refer to an non-existing subscription tag.

When I call $usage = $user->subscription('primary')->getFeatureUsage('social_profiles'); and there is no 'primary' subscription, I get 'Call to a member function getFeatureUsage() on null'. I think it would be better just to return null, as this user does not the subscription 'primary' so that he does not have the feature either.

The same goes for getFeatureRemainings(), etc.

Keeping track of subscription changes

The plan_subscriptions table shows the current state of the users' subscriptions, but we have no idea of their history. And this information is often very valuable.

Of course, someone would (at least should) keep track of the payments, but it does not give the full image of subscription changes.

I'd see this functionality inside the library itself, with a simple table plan_subscription_log with fields:

  • id
  • tag
  • subscriber_type
  • subscriber_id
  • plan_id
  • name
  • price
  • currency
  • action
  • timestamp

where action could be one of: 'trial_started' / 'renewed' (treated as well as started) / 'canceled'.

This way we could know what user did with their subscription, when they decided to upgrade/downgrade/renew/cancel, how long they were with us, did they have any gaps in the subscription, etc. It's often very valuable information for the support team.

Datetime fields to Timestamp fields

IMHO all 'DATETIME' fields should be changed to 'TIMESTAMP' (in the plan_subscriptions and plan_subscription_usage).

Using 'TIMESTAMP' type fields we are sure that they are always stored in the same timezone (in UTC), according to docs:
MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.)

MySQL just saves "now moment in time" as the timestamp and this "now" is universal independent of which timezone the user was at. Good for comparisons, and not making any error with timezone shifts.

What is a 'tier'?

Actually how the tier should be used? I have not found any information about it in the docs

Ressetable period issue

As mentioned here #77 (comment)

If subscriber does not use the app for 1 period, when usage is going to be recorded again. It will be set to next month from the previous period and use one. Subscribers get 1 usage for every period they've been out.

`syncPlan` should / could not renew the subscription

I wanted to set the new plan to the subscription (before payment). And I have just noticed that when I wanted to syncPlan with $syncInvoicing = true for saving the new payment period I have noticed that with these lines:

// Set new start and end date
$period = new Period($plan->invoice_interval, $plan->invoice_period);

$this->starts_at = $period->getStartDate();
$this->ends_at = $period->getEndDate();

the subscription actually gets renewed. IMHO I would keep changing plan and the renewal separate. So syncPlan and changePlan should not automatically renew the sub, or do it optionally with an argument.

Pricing per country

I know there is currency field but sometimes it's necessary to define the prices not only per currency but also per country. For example different pricing schema among different countries in the euro zone. Or just different pricing across different countries.

I'd see additional field country in the plans table which would allow creating pricing plans per country. I know one could use a special naming for tags, for example "basic-usd-us", "basic-gbp-uk", "pro-usd-us", "pro-gbp-uk", etc. but filtering such plans is a pure pain. With additional scope, filtering by country would be very easy.

Or if not with country specifically, maybe we could just go with a more generic filed name, like group?

I see this change necessary only in plans, it does not need to be copied to plan_subscriptions.

Behaviour of the (second) `renew()`

renew() sets the starts_at at now and ends_at at "30 days" from now. The same happens when I fire the renew() for the second time. And when a person pays twice we actually should prolong ` his subscription for another "30 days", shouldn't we?

One method to get subscriber's features along with usage

Is there any method for getting the subscriber's available features along with their usage? It could be useful to conveniently display this information in the user's profile.

I can see that at this moment we can run:
$features = $user->subscription('main')->features()->get();

and
$usage = $user->subscription('main')->usage()->get();

and later combine both collections?

`starts_at` for the trial plan

After creating a new subsription with a plan having a defined 7 day trial with $user->newSubscription('primary', $plan);, the field starts_at gets the date of the trial_ends_at / ends_at. Is it an expected behaviour?

image

Default of resettable_interval

Defing this feature

new PlanFeature(
     [
         'tag'        => 'api',
         'name'       => 'API Module',
         'value'      => false,
         'sort_order' => 70,
     ]
 ),

and later seeing 'month' in the resettable_interval is a little bit misleading. Wouldn't it better to have resettable_interval nullable with default 'null'?

Individual features per subscriber

In some projects, I have noticed the need to individually define the features for the users.

I mean, let the user have a 'middle' plan but let them as well have feature 'x' from the 'pro' plan (for example we give them this feature for testing or we agreed they can use it).

I thought about duplicating a plan (along with its features) before creating a subscription, like $plan = Plan::findByTag('middle'); $plan = $plan->duplicate('middle-gr4ud8'); or $plan = Plan::createFromExisting('middle-gr4ud8', 'middle'); and later we can individually set the necessary features for the subscriber.

Another solution would be to create a subscription based on the unchanged plan and allow to override the features? Probably this solution would be more difficult to implement as this would involve creating another table in the database.

Behaviour of modified plan features

As far as I understand, when the plan features change, users stay with the "old" features rules? One needs to use $user->subscription('main')->syncPlanFeatures(); to reflect changes in the user's configuration?

This part of syncPlanFeatures() is only mentioned in plan-subscription-model.md - I think it's worth mentioning as well in the plan-subscription-feature-model.md because it's not that obvious.

What if we would need to update feature set for all the users? Will syncPlanFeatures() work on the Collection of subscribers?

More details about plan_subscription_features in the Create a Subscription doc

Maybe it's worth mentioning in plan-subscription-model.md in the 'Create a Subscription' section that now the features got copied to plan_subscription_features? I mean an explanation that a subscriber does not use feature set directly from plan_features, that plan_features is only a template. Someone may think you create a plan, you attach it to subscription and use definitions from plan_features.

changePlan -> deleteFeaturesNotInPlan Bug

Hello @bpuig
Thanks for this great package,
I noticed that deleteFeaturesNotInPlan doesn't delete features not in plan we change to.
I fix it by updating this :

// Retrieve current features that are not related to a plan
$featuresWithPlan = $this->features()->withoutPlan()->get();

To :

// Retrieve current features that are not related to a plan
$featuresWithPlan = $this->features()->get();

Feature upgrades in the middle of an active subscription

While upgrading a feature in the middle of an active subscription, it would nice to be able to calculate the proportion of remaining days per invoice_interval.

Let's say someone's subscription period finishes at 21th day of each month. Today we have 13th, so there are 8 days to go and while doing the upgrade, the subscriber should pay 8/30 * price of the feature. I know the payments are out of scope of this library but this calculation would be very convenient for handling upgrade payments for a partial period.

Default database migration isn't compatible with MySQL

The following migration with default table settings producing MySQL error:

  SQLSTATE[42000]: Syntax error or access violation: 1059 Identifier name 'plan_subscription_schedules_scheduleable_type_scheduleable_id_index' is too long (SQL: alter table `plan_subscription_schedules` add index `plan_subscription_schedules_scheduleable_type_scheduleable_id_index`(`scheduleable_type`, `scheduleable_id`))
        Schema::create(config('subby.tables.plan_subscription_schedules'), function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('subscription_id');
            $table->morphs('scheduleable');
            $table->timestamp('scheduled_at')->nullable();
            $table->timestamp('failed_at')->nullable();
            $table->timestamp('succeeded_at')->nullable();

            $table->unique(['subscription_id', 'scheduleable_type', 'scheduleable_id', 'scheduled_at'], 'unique_plan_subscription_keys');

            $table->foreign('subscription_id', 'plan_subscription_fk')->references('id')->on(config('subby.tables.plan_subscriptions'))->onDelete('cascade')->onUpdate('cascade');
        });

Workaround is to edit default table name in config/shubby.php to a shorter one.

Still true `isOnTrial()` even if paid (renewed)

Scenario: User registers, gets trial subscription upon registration and soon (after 1 minute or 1 day) pays and prolongs subscription.

The $subscription->renew() prolongs his ends_at but does not delete the trial_ends_at. And checking $user->subscription('main')->isOnTrial() returns true which in my opinion is erroneous.

Similarly, PlanSubscription::findEndingTrial(10)->get() returns this freshly renewed subscription which could cause problems when we'd like to send some reminding emails to the users on trials.

Scheduler back to the main library?

With all these things that pop up over and over again like:

  • (double) renew
  • renew subscription upon the trial finishes
  • change plan (downgrade / upgrade) upon next renewal

I thought maybe it would be a good idea to make again a scheduler an integral part of this package?

But for wider compatibility I would resign from job batches. Actually, I would even resign from jobs altogether. Why not just make the implementation in the command ($schedule->command(...)->everyMinute()->runInBackground();)? Commands already run in background and using jobs seems to me superfluous. This would also demand less requirements - just running the scheduler and no need to configure queue drivers or worker.

For example, upon issuing a renewal, the task could be saved into plan_subscription_schedules to be run upon ends_at (or trial_ends_at) and scheduler will pick it up upon right moment and make appropriate changes.

Documentation

Hi there.

+1 for this package however was wondering if there is some form of documentation or any ETA?

getFeatureByTag() in Subscription

To get to the subscription feature (for example to update it) we need to use $user->subscription('main')->features()->where('tag', 'social_profiles'). It would be great to use a simpler form like $user->subscription('main')->getFeatureByTag('social_profiles')

Foreign key constraint is incorrectly formed

SQLSTATE[HY000]: General error: 1005 Can't create table `laravel`.`plan_features` (errno: 150 "Foreign key constraint is incorrectly formed") (SQL: alter table `plan_features` add constraint `plan_features_plan_id_foreign` foreign key (`plan_id`) references `plans` (`id`) on delete cascade on update cascade)

Using PHP 8 & 10.4.24-MariaDB

Graceful handling of double features create

When I run the below code twice (or just try to create the feature with an existing tag)

$user->subscription('primary')->features()->create(
            [
                'tag'                 => 'pictures_per_social_profile',
                'name'                => 'Pictures per social profile',
                'value'               => 30,
                'sort_order'          => 10,
                'resettable_period'   => 1,
                'resettable_interval' => 'month'
            ]
        );

I get:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1-pictures_per_social_profile' for key 'plan_subscription_features.plan_subscription_features_subscription_id_tag_unique' (SQL: insert into plan_subscription_features (tag, name, value, sort_order, resettable_period, resettable_interval, subscription_id, updated_at, created_at) values (pictures_per_social_profile, Pictures per social profile, 30, 10, 1, month, 1, 2021-05-13 06:46:21, 2021-05-13 06:46:21))

Maybe better would be to return false as a not successful query?

The same goes for creating plan with the same tag ($plan = Plan::create())

Renewal handling

Since the package will handle schedules again, it would be nice to handle renewals, because it's almost the same.

Default name of subscription

As majority of implementations would use just one subscription, maybe the name 'main' could be a default one and used 'in the background'?

I mean possibility of using $user->subscription-> instead of every time attaching 'main' $user->subscription('main')->
and during creation maybe using $user->newMainSubscription() ?

Free plan

I want to be able to have free plans without trial period and monthly intervals is it possible ?

Email notifications about expiring subscriptions

Hi! I was wondering about a scheduler for email notifications about expiring subscriptions. Thereotically the mechanim should not be that complicated.

I would see a config with time before end divided per subscription tag:
['main']['sub'] => [7, 3, 1] //days before subscription ends

or even expressed in hours so that we can send an email 4 or 1 hour before the end.
['primary']['trial'] => [72, 24, 4, 1]

Scheduler would pick up subscribers (in the loop according to the config) and send a notification with 'mail' channel given as an example. If someone would like to use some other like sms / web push, they'd need to override the base notification method:

Notification::send(
    $subscribers,
    new ExpiringSubscriptionNotification(
	//
    )
);

Update of invoice fields upon changePlan($plan)

I have a $plan with invoice_interval of a value 'year'. I do:

$plan::find(2); // the one with `invoice_interval` set to 'year'
$subscription = $user->subscription();
$subscription->changePlan($plan);

After checking the db, I have indeed changed plan_id, price and currency but not invoice_period and invoice_interval fields. As $syncInvoicing is by default true in changePlan() I would expect that invoice_period and invoice_interval got updated as well in the plan_subscriptions table?

The ends_at was properly extended by a year from now.

Grace period

(I'll share some more thoughts regarding the upcoming new v5 version)

I saw you decided to remove the grace period fields because as I understood they had no coverage in the code?

But IMHO it would be good to implement the functionality of the grace period. There are situations where people want to continue with our app, but there were some issues with their card (expired, stolen, used daily limit, etc.) they could be even unaware of. Until we try to charge the money, neither we or the user know about the problem.

And when this situation arises, we should give them some grace_period (always expressed in days?) to solve the issue with the card and not abruptly stop access to the app/features.

I think using of grace period should only modify the behaviour of isActive() and not hasEnded(). Maybe an extra method like isInGracePeriod() could be useful as well.

Subscribing to a non-active plan

I wonder if

$plan = Plan::find(2);
$user->newSubscription('main', $plan);

should be possible if plan of id=2 has is_active set to 0? Or it's better to leave it to developer

"Canceled subscriptions with an ended period can't be renewed."

What to do if a user cancels a subscription and (after some time) he wants to reactivate the subscription? We cannot do it.

$user->subscription('main')->cancel();

they got cancelled as expected, they come back after 6 months, they pay and we want again create a subscription for them:

$plan = Plan::find(1);
$user->newSubscription('main', $plan);

and we got: A subscription with tag main is duplicated for this subscriber.

I am not sure why canceled subscription cannot be set active again? Do we treat cancelled subscriptions "as history"?

Maybe the library should create a new row for the new subscription with the same tag and allow only one active (not cancelled) row? Should we have a broader unique index with canceled_at field? Thereotically MySQL allows multiple NULLs in a column with a unique constraint, but would it be a good solution?

Handling of renewals of trials

I still wonder how to properly handle renewals of the trial subscriptions.

Scenario:

  • user registers at the website
  • at the moment of registration gets a subscription with a trial (Basic / Pro - does not matter, but with trial days)
  • consumes X times the features (which are registered with recordFeatureUsage())
  • at the end of the trial decides to pay
  • payment is successful and now, upon issuing ->renew() all usages consumed during trial are reset as in the renew() method there is $subscription->usage()->delete();

Am I missing something? I'd rather kept the usages consumed during trial.

Shouldn't `getFeatureByTag()` throw an Exception?

Now, if someone queries a non-existent feature, e.g. $subscription->getFeatureByTag('test'), they get null in response. But maybe getFeatureByTag() should return an exception as an analogy to PlanSubscriptionNotFound?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.