mollie / laravel-cashier-mollie

Official Mollie integration for Laravel Cashier
https://www.cashiermollie.com/
MIT License
137 stars 44 forks source link

Changing subscription plan #264

Open ignaciocunado opened 2 months ago

ignaciocunado commented 2 months ago

Whilst implementing functionality for users to be able to change their subscription plan/quantity, we came across the following scenario:

  1. A user has a yearly subscription and wants to downgrade into a monthly subscription (this should happen at the end of their current cycle since they agreed to pay for a year)
  2. Using swapNextCycle, their subscription attribute next_plan is set for a monthly subscription and a new order item is scheduled for the end of their cycle.
  3. This same user now wants to increase their subscription count (eg. from 1 to 2) which should happen immediately since this is considered an upgrade.

Problem: If we use the current functionality, a new cycle will start when upgrading their subscription count. This will cause the scheduled order item containing the next_plan to be removed.

When should the plan downgrade happen?

ignaciocunado commented 2 months ago

I know this is a pretty specific use case, but we wanted to see if maybe anybody else had come across this scenario and if there was a possible fix for this.

I will submit a PR if I find a possible and suitable implementation.

sandervanhooft commented 2 months ago

Hi @ignaciocunado ,

Thanks for sharing this. The use case makes sense. People change their minds all the time, especially subscription customers ;).

Problem: If we use the current functionality, a new cycle will start when upgrading their subscription count. This will cause the scheduled order item containing the next_plan to be removed.

Were you able to confirm the behavior you're describing? The next_plan is actually stored on the Subscription model, not on the scheduled OrderItem. If possible, could you provide a failing test?

ignaciocunado commented 2 months ago
/** @test */
    public function swapPlanThenCount()
    {
        $user = $this->getUserWithZeroBalance();
        $subscription = $this->getSubscriptionForUser($user);

        // Swap to new plan
        $subscription = $subscription->swapNextCycle('weekly-20-1')->fresh();
        $this->assertEquals('monthly-10-1', $subscription->plan);
        $this->assertEquals('weekly-20-1', $subscription->next_plan);
        $cycle_should_have_started_at = now()->subWeeks(2);
        $cycle_should_end_at = $cycle_should_have_started_at->copy()->addMonth();
        $new_order_item = $subscription->scheduledOrderItem;
        $this->assertCarbon($cycle_should_end_at, $new_order_item->process_at, 1); // based on previous plan's cycle
        $this->assertEquals(2200, $new_order_item->total);
        $this->assertEquals(200, $new_order_item->tax);
        $this->assertEquals('Twice as expensive monthly subscription', $new_order_item->description);
        Event::assertNotDispatched(SubscriptionPlanSwapped::class);

        // Increase subscription count
        $subscription = $subscription->incrementQuantity(1, false)->fresh();
        $this->assertEquals(2, $subscription->quantity);
        $new_order_item = $subscription->scheduledOrderItem;
        $this->assertNotNull($subscription->next_plan);
        $this->assertEquals($subscription->plan, 'monthly-10-1'); // Plan has not been updated
        $this->assertEquals(4400, $new_order_item->total); // Plan not updated on the scheduled order item (twice as before as quantity is doubled). First assertion that fails
        $this->assertEquals(400, $new_order_item->tax); // Same here
        $this->assertEquals('Twice as expensive monthly subscription', $new_order_item->description);
    }
Time: 00:01.175, Memory: 52.50 MB

There was 1 failure:

1) Laravel\Cashier\Tests\SwapSubscriptionPlanTest::swapPlanThenCount
Failed asserting that 2200 matches expected 4400.

/Users/ignacunado/Herd/laravel-cashier-mollie/tests/SwapSubscriptionPlanTest.php:332

FAILURES!
Tests: 232, Assertions: 1778, Failures: 1.
Script ./vendor/bin/phpunit tests handling the test event returned with error code 1

This test shows that when updating the plan at the end of the cycle and then using incrementQuantity, a new orderItem is scheduled and processed with the same plan and the new quantity. Even though the next_plan attribute on the subscription model remains set, there are no scheduled order items for this new plan, so I guess it will never be changed?

ignaciocunado commented 2 months ago

So I guess that there are (at least) two options here with the current functionality.

  1. If a function like restartCycleWithModifications is called which schedules a new order item at now() is called and the subscription object has the next_plan attribute set (meaning the plan should change for the next cycle), then we change the plan on this order item (immediately). This has flaws as a user could terminate a yearly subscription before the end of the cycle and get their money back (set next plan to a weekly subscription, change quantity and then cancel at the end of the week).
  2. If a function like restartCycleWithModifications is called which schedules a new order item at now() is called and the subscription object has the next_plan attribute set (meaning the plan should change for the next cycle), then we change the plan for the next order item. This is also kind of bad. Say a user has a yearly subscription ending in February, and have set it to downgrade to a monthly one which will take effect in February. In January the user decides to increment their subscription quantity by 1. Then they would need to wait a whole other year to swap to a monthly subscription as the cycle restarts.

Again, pretty specific scenarios but they can happen.