bpuig / laravel-subby Goto Github PK
View Code? Open in Web Editor NEWLaravel Plan and Subscriptions manager.
Home Page: https://bpuig.github.io/laravel-subby
License: MIT License
Laravel Plan and Subscriptions manager.
Home Page: https://bpuig.github.io/laravel-subby
License: MIT License
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
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
:).
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'm so happy I have found your fork :)
I just wanted to let know that the link to changelog is wrong in the README.md (should have the extension .html instead of .md)
(And actually that document is missing some information available at https://github.com/bpuig/laravel-subby/releases)
For example at https://github.com/bpuig/laravel-subby/tree/4.0.0-alpha.3/docs the link "plan" goes to https://github.com/bpuig/laravel-subby/blob/4.0.0-alpha.3/models/plan-model.md
Maybe it's worth mentioning in plan-feature-model.md
that after creating a plan one needs to attach it to user's Subscription?
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].
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.
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
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.
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.
Hello,
This function getSubscriptionRemainingUsagePriceProrate
always calculates the price for monthly period after change plan from monthly to yearly.
An other issue, the ends_at doesn't change if we change plan from monthly to yearly.
Thanks
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:
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.
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.
Actually how the tier
should be used? I have not found any information about it in the docs
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.
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.
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
.
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?
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?
Scope $subscriptions = PlanSubscription::findEndingPeriod(3)->get();
will unnecessary return trial subscriptions which were not prolonged (activated).
While using a trial plans either ends_at
should be set to the same value as trial_ends_at
or the above mentions scope should return only active subscriptions.
Actually, I have just discovered that features and features in relationships are not returned in the order set by the sort_order
column.
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'?
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.
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?
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
.
As now it's very often necessary to create multilanguage systems, I opt for translatable names & descriptions for plans, features and subscriptions. I'd suggest using https://github.com/spatie/laravel-translatable which is very easy to implement.
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();
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.
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.
For example $subscription->getFeatureRemainings('analytics')
will error with "Unsupported operand types: string - int". I think in this case we could silently return null
(helpful when we are getting user's features details "in bulk")?
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.
With all these things that pop up over and over again like:
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.
Hi there.
+1 for this package however was wondering if there is some form of documentation or any ETA?
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')
As far as I understand, job batching is now used at https://github.com/bpuig/laravel-subby-schedule and only there, the Laravel 8 is required.
Could you allow for Laravel 6 as well here? I think it should work. I will give it a try as I have one project with v6.
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
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()
)
Since the package will handle schedules again, it would be nice to handle renewals, because it's almost the same.
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()
?
I want to be able to have free plans without trial period and monthly intervals is it possible ?
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(
//
)
);
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.
(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.
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
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?
I still wonder how to properly handle renewals of the trial subscriptions.
Scenario:
recordFeatureUsage()
)->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.
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
?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.