bpuig / laravel-subby

Laravel Plan and Subscriptions manager.
https://bpuig.github.io/laravel-subby
MIT License
104 stars 42 forks source link

New trial behavior #77

Closed bpuig closed 3 years ago

bpuig commented 3 years ago

Discarded #75, now this is a bit simpler.

New behavior

Trial modes

There are two available trial modes: inside or outside. This defines how the trial will be counted when renewal time is due. Also renewal only affects subscription period, it can be renewed for as many periods as you want.

USAGE WILL NOT BE CLEARED when user has had trial time. This is what gives sense to both methods. If you need to clear, you can do it manually upon renewal.

When a new subscription to a plan is made:

If plan has trial

If plan has trial, subscriber does not have subscription but only a trial. Subscription period starts and ends at null and this is considered subscription is not made. Because in a real case scenario, when a subscriber has a trial it does not have a subscription yet, so the invoice period is made and charged after the trial has ended.

Renewal when trial is "inside"

If trial mode is inside; when trial ends and is renewed invoice period will have substracted the days of trial that have been used.

Example: 7 day trial in a 30 day subscription period.

In summary: this is NOT a free trial. User always ends up paying the full price for full period.

Renewal when trial is "outside"

If trial mode is outside; when trial ends and is renewed, invoice period will start at the moment it's renewed.

Example: 7 day trial in a 30 day subscription period.

In summary: this is IS a free trial. User does not pay for the trial period, but for the next subscription period.

If plan does not have trial

If plan does not have trial, subscriber has subscription. Because when a plan does not have trial, a new subscription activates a new invoicing period.

TODO:

bpuig commented 3 years ago

Now this makes sense. Care to give a thought @boryn ?

bpuig commented 3 years ago

This should also fix #74 because you can only have one thing, trial or subscription, they will not collide.

boryn commented 3 years ago

Could you prepare a beta v5 release? Would be much easier to give it a try

bpuig commented 3 years ago

Could you prepare a beta v5 release? Would be much easier to give it a try

I'm afraid that would be not possible, it would mean I'd have to merge this PR, you can use the branch this PR is on: https://github.com/bpuig/laravel-subby/tree/new_trial_behaviour

boryn commented 3 years ago

OK

boryn commented 3 years ago

Thank you for this implementation, seems to be much more logical. I share my first thoughts:

"Outside" mode

"Inside" mode

Shouldn't trial_mode field be copied from plan to plan_subscriptions as well? The plan definition can be changed in the meantime and we should use the definition from upon trial creation.

Free plan

Universal information about end of trial/subscription

bpuig commented 3 years ago

I strongly believe the user should have the right to use their free trial up to the initially set trial_ends_at ("7 days") and when they pay in the 3rd day of trial, the starts_at should start at trial_ends_at and not now(). We should not "punish" them when they quickly decide to pay for our app. (Just when they decide to cancel, we by default allow them to use the app up to the end of the period).

Done, now you get the remaining days! But trial still ends because I don't want to have trial and subscription at same time. Either one or the other.

In this mode shouldn't the usage be cleared? They used what they got for free during the trial, and with renew() should start a new period with reset, fresh features. We need to consider as well 30 or even 90 days trials. And they may renew (actually start) subscription with a different plan.

I don't think so, because if you get 7 days trial, consume everything and renew, you'd get 1 month of usage for free because you used all the available features and then got renewed with a clear usage. Trial means you just get free time, not features.

I have made such a simulation:

The way I calculate now won't make that happen.

Shouldn't trial_mode field be copied from plan to plan_subscriptions as well? The plan definition can be changed in the meantime and we should use the definition from upon trial creation.

Yes it should, all the trial data.

With the plan of price 0, I think the subscription should be valid "forever". Now, upon creating a new subscription, I got it just valid until 17 of July. Maybe with price 0 we should just set starts_at and leave ends_at as null which would mean active indefinitely? With the free plan nobody would care to renew it every month...

You should get a subscription that renews every given invoice period. For the amount of 0€, so free.

It's usual to display to the user information when their subscription ends (either with trial or without). We can grab the information if it is trial or not (isOnTrial()) but I consider getting the end of the period very cumbersome with conditional logic (if on trial, get trial_ends_at, if not on trial get ends_at). That's why I'd recommend something more generic inside the PlanSubscription.php like:

public function getPeriodEndsAtAttribute()
    {
        if ($this->isOnTrial()) {
            return $this->trial_ends_at;
        }

        return $this->ends_at;
    }

I'll take a look

bpuig commented 3 years ago

And what about the problem with doubling/quadrupling the allowance of the features? It won't happen now?

Too many things to remember but I think I was wrong and you were right, that features where reset by periods or something like that. Also renewal does not clear usage now. Multiple renewal should be OK.

It took the way that we have features more decoupled of subscription time. You subscribe for X time and features do their own thing with their own reset periods. While you are subscribed you can use them, if you are not, you can't.

bpuig commented 3 years ago

I think I found another issue(s).

Renewal date for features is retrieved by: $period = new Period($this->resettable_interval, $this->resettable_period, $dateFrom ?? now());

And $dateFrom is set here:

if ($feature->resettable_period) {
    // Set expiration date when the usage record is new or doesn't have one.
    if (is_null($usage->valid_until)) {
        // Set date from subscription creation date so the reset
        // period match the period specified by the subscription's plan.
        $usage->valid_until = $feature->getResetDate($this->created_at);
    } elseif ($usage->hasExpired()) {
        // If the usage record has been expired, let's assign
        // a new expiration date and reset the uses to zero.
        $usage->valid_until = $feature->getResetDate($usage->valid_until);
        $usage->used = 0;
    }
}

ISSUE 1

$feature->getResetDate($this->created_at);

The creation date does not mean anything right now.

ISSUE 2

Soooooo... what happens 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 would get 1 usage for every period they've been out. Super weird

Example:

bpuig commented 3 years ago

Sorry for all that commits, I don't know what happened.

boryn commented 3 years ago

I strongly believe the user should have the right to use their free trial up to the initially set trial_ends_at ("7 days") and when they pay in the 3rd day of trial, the starts_at should start at trial_ends_at and not now(). We should not "punish" them when they quickly decide to pay for our app. (Just when they decide to cancel, we by default allow them to use the app up to the end of the period).

Done, now you get the remaining days! But trial still ends because I don't want to have trial and subscription at same time. Either one or the other.

So how does it work now? Upon renewal, trial_ends_at is set to starts_at at now()? And ends_at is prolonged by "30 days" from the previous value of trial_ends_at?

In this mode shouldn't the usage be cleared? They used what they got for free during the trial, and with renew() should start a new period with reset, fresh features. We need to consider as well 30 or even 90 days trials. And they may renew (actually start) subscription with a different plan.

I don't think so, because if you get 7 days trial, consume everything and renew, you'd get 1 month of usage for free because you used all the available features and then got renewed with a clear usage. Trial means you just get free time, not features.

I don't understand "Trial means you just get free time, not features". Companies often give 7-day trial for the "Pro" plan and allow users both to have time and to test the features for free. I rather see this scenario. And later when you subscribe, you get fresh set of features. Maybe it should be on an option then?

With the plan of price 0, I think the subscription should be valid "forever". Now, upon creating a new subscription, I got it just valid until 17 of July. Maybe with price 0 we should just set starts_at and leave ends_at as null which would mean active indefinitely? With the free plan nobody would care to renew it every month...

You should get a subscription that renews every given invoice period. For the amount of 0€, so free.

But should I (developer) care about refreshing it every month? Normally renewal comes "from outside" (payment). When we set a free 0€ subscription, it is not natural that we additionaly should care about prolonging it every month. It just should be infinite.

boryn commented 3 years ago

I think I found another issue(s).

Do you have an idea how to solve it? I think we need a check whether a date is inside a subscription period and if $usage->valid_until is inside the active subscription, we should probably use start of the subscription period? BUT we do not have this, it should be manually calculated: ends_at minus invoice period.

Buuuut... then feature usage is not decoupled from subscriptions... And sub period can be a year, and feature a month...

Maybe there should be a loop for incrementing $usage->valid_until until we land on "current" period before doing the real feature record? I mean something like this:

_valid_until = valid_until + resettable period if now() "inside" (validuntil + resettable period) then break the loop and record feature usage

(of course when valid_until is already in current period, there is no loop necessary)

boryn commented 3 years ago

Wait... You already solved it at https://github.com/bpuig/laravel-subby/pull/86/commits/e335d97d90bfbd1758c5f71cd884801ca283d49f ! Funny thing that we came to the same conclusion of doing it in a loop :)

bpuig commented 3 years ago

So how does it work now? Upon renewal, trial_ends_at is set to starts_at at now()? And ends_at is prolonged by "30 days" from the previous value of trial_ends_at?

Trial ends at now and then:

I don't understand "Trial means you just get free time, not features". Companies often give 7-day trial for the "Pro" plan and allow users both to have time and to test the features for free. I rather see this scenario. And later when you subscribe, you get fresh set of features. Maybe it should be on an option then?

Then you are giving a month set of features for free, do you really want that? I can look into making it an option, makes sense, not economically, but makes sense... 😆

But should I (developer) care about refreshing it every month? Normally renewal comes "from outside" (payment). When we set a free 0€ subscription, it is not natural that we additionaly should care about prolonging it every month. It just should be infinite.

Subscriptions should auto-renew until canceled. I know there is the payment thing that should be sorted sometime, but right now the package is a "bubble" that should not care what happens in the outside world (payment platform).

boryn commented 3 years ago

I don't understand "Trial means you just get free time, not features". Companies often give 7-day trial for the "Pro" plan and allow users both to have time and to test the features for free. I rather see this scenario. And later when you subscribe, you get fresh set of features. Maybe it should be on an option then?

Then you are giving a month set of features for free, do you really want that? I can look into making it an option, makes sense, not economically, but makes sense...

Sometimes you need to give a user something totally for free (even the Pro "sub") so that they get to know your solution and only later ask them to pay. If the trial is longer, eg. for 30 days and they later prolong, they need to have fresh usage limits. And even if trial is 7-14 days, when they decided to prolong, they get a new subscription and in my opinion, they need to get fresh usage limits as well. I consider a free trial to be a real free trial. So I think, it would be good implementing such an option.

But should I (developer) care about refreshing it every month? Normally renewal comes "from outside" (payment). When we set a free 0€ subscription, it is not natural that we additionaly should care about prolonging it every month. It just should be infinite.

Subscriptions should auto-renew until canceled. I know there is the payment thing that should be sorted sometime, but right now the package is a "bubble" that should not care what happens in the outside world (payment platform).

Hmm... So I was not aware of the auto-renewal of free subscriptions. Where does it happen in the code?

bpuig commented 3 years ago

If the trial is longer, eg. for 30 days and they later prolong, they need to have fresh usage limits.

I'll try to implement the behavior somehow 👍

Hmm... So I was not aware of the auto-renewal of free subscriptions. Where does it happen in the code?

I meant YOU need to auto-renew the subscriptions. The package was (is) thought that you handle renewals when subscription ends, and you should keep track of renewals.

boryn commented 3 years ago

Hmm... So I was not aware of the auto-renewal of free subscriptions. Where does it happen in the code?

I meant YOU need to auto-renew the subscriptions. The package was (is) thought that you handle renewals when subscription ends, and you should keep track of renewals.

That's natural that subscriptions with the price need to have the outside event to trigger the renewal. But the free subscriptions don't even demand the outside world, they should just exist and be available. And that's why I talk about some special handling of the free subscriptions, inside the package "bubble" itself.

AFAIR this manual renewing of free subscriptions is not mentioned in the docs? Or I didn't notice that. Common sense tells me that if the subscription is free, it's just can be used forever and no external steps need to be taken. It would be a big nuisance to set up an extra scheduler job just to keep free subs "alive".

IMHO it would be reasonable to fill the starts_at and leave the ends_at with null, we'd need a new method isFree() (true if price equals 0 (and maybe if ends_at is null)) and in such a case the isActive() would return true if || $this->isFree(). hasEnded() should as well return false if isFree().

bpuig commented 3 years ago

I think this is almost good to go. I have been away for some time so I'm not fully fresh about what we were talking.

This branch accepts renewal by periods. Also renewal does not clear usage just manipulates periods, since that is done by the usage model automatically.

If you want to take a look at it, I'll live it open some more time in case I find anything more.

Exceptions for free subscriptions will not be made, they will be treated as a regular subscription with 0 price. This will keep the package more coherent and with the minimum exceptions and variants possible as long as it is usable.

:smile:

boryn commented 3 years ago

Thank you! I hope to be able to test it the next week and will give some feedback