From bd2b86a42d93247ef0eaa719934e178a5dbecda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20Cu=C3=B1ado=20Barral?= Date: Sat, 29 Jun 2024 10:06:21 +0200 Subject: [PATCH 1/3] feat/Add getter method for the reimbursable amount for unused subscription time --- src/Subscription.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Subscription.php b/src/Subscription.php index 116e68c..8715c08 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -612,6 +612,28 @@ public function getCurrencyAttribute() return optional($this->plan())->amount()->getCurrency()->getCode(); } + /** + * Gets the amount to be refunded for the subscription's unused time. + * + * @param \Carbon\Carbon|null $now + * @return \Money\Money + */ + public function getReimburseAmountForUnusedTime(?Carbon $now = null): ?Money + { + $now = $now ?: now(); + + if ($this->onTrial()) { + return null; + } + if (round($this->getCycleLeftAttribute($now), 5) == 0) { + return null; + } + + return $this->reimbursableAmount() + ->negative() + ->multiply(sprintf('%.8F', $this->getCycleLeftAttribute($now))); + } + /** * Handle a failed payment. * From 9353c9c6c65875c68aad0bd7a67626e4090e5869 Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Tue, 16 Jul 2024 13:55:29 +0200 Subject: [PATCH 2/3] add tests for getReimburesmentAmountForUnusedTime` --- src/Subscription.php | 2 +- tests/Database/Factories/OrderItemFactory.php | 7 +++ tests/SubscriptionTest.php | 49 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Subscription.php b/src/Subscription.php index 8715c08..5397b43 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -661,7 +661,7 @@ public static function handlePaymentPaid(OrderItem $item) if ($subscription->ends_at !== null) { DB::transaction(function () use ($item, $subscription) { - if (! $subscription->scheduled_order_item_id) { + if (!$subscription->scheduled_order_item_id) { $item = $subscription->scheduleNewOrderItemAt($subscription->ends_at); } diff --git a/tests/Database/Factories/OrderItemFactory.php b/tests/Database/Factories/OrderItemFactory.php index 08501e5..37dac38 100644 --- a/tests/Database/Factories/OrderItemFactory.php +++ b/tests/Database/Factories/OrderItemFactory.php @@ -74,4 +74,11 @@ public function USD() 'currency' => 'USD', ]); } + + public function withOrder(): self + { + return $this->state(fn () => [ + 'order_id' => OrderFactory::new(), + ]); + } } diff --git a/tests/SubscriptionTest.php b/tests/SubscriptionTest.php index 0ac7c18..c1691c2 100644 --- a/tests/SubscriptionTest.php +++ b/tests/SubscriptionTest.php @@ -8,6 +8,7 @@ use Laravel\Cashier\Cashier; use Laravel\Cashier\Events\SubscriptionResumed; use Laravel\Cashier\Subscription; +use Laravel\Cashier\Tests\Database\Factories\OrderFactory; use Laravel\Cashier\Tests\Database\Factories\OrderItemFactory; use Laravel\Cashier\Tests\Database\Factories\SubscriptionFactory; use Laravel\Cashier\Tests\Fixtures\User; @@ -589,4 +590,52 @@ public function canQueryRecurringSubscriptions() $this->assertEquals(1, Subscription::whereRecurring()->count()); $this->assertEquals(2, Subscription::whereNotRecurring()->count()); } + + /** @test */ + public function halfWayThroughSubscriptionReturnsPositiveReimburesmentAmount() + { + $this->withConfiguredPlans(); + + $subscriptionHalfWayThrough = SubscriptionFactory::new() + ->has( + OrderItemFactory::new(['unit_price' => 100]) + ->processed() + ->withOrder() + ->EUR(), + 'scheduledOrderItem' + ) + ->create([ + 'cycle_started_at' => now()->subDays(20), + 'cycle_ends_at' => now()->addDays(20), + ]); + + $this->assertEquals( + money('-50', 'EUR'), + $subscriptionHalfWayThrough->getReimburseAmountForUnusedTime() + ); + } + + /** @test */ + public function nonReimbursableSubscriptionReturnsNoReimbursementAmount() + { + $this->withConfiguredPlans(); + + $nonReimbursable = SubscriptionFactory::new() + ->has( + OrderItemFactory::new(['unit_price' => 100]) + ->processed() + ->withOrder() + ->EUR(), + 'scheduledOrderItem' + ) + ->create([ + 'cycle_started_at' => now()->subMonth(), + 'cycle_ends_at' => now()->subDay(), + ]); + + $this->assertEquals( + null, + $nonReimbursable->getReimburseAmountForUnusedTime() + ); + } } From 617df4b82f38197b4a0cd330470451e4753ab207 Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Wed, 17 Jul 2024 15:01:01 +0200 Subject: [PATCH 3/3] wip --- src/Helpers/helpers.php | 14 +++++++------- src/Subscription.php | 21 +++++++++++++-------- tests/SubscriptionTest.php | 9 ++++----- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Helpers/helpers.php b/src/Helpers/helpers.php index a833c22..17e8d14 100644 --- a/src/Helpers/helpers.php +++ b/src/Helpers/helpers.php @@ -6,7 +6,7 @@ use Money\Money; use Money\Parser\DecimalMoneyParser; -if (! function_exists('object_to_array_recursive')) { +if (!function_exists('object_to_array_recursive')) { /** * Recursively cast an object into an array. * @@ -23,7 +23,7 @@ function object_to_array_recursive($object) } } -if (! function_exists('money')) { +if (!function_exists('money')) { /** * Create a Money object from a Mollie Amount array. * @@ -37,7 +37,7 @@ function money($value, string $currency) } } -if (! function_exists('decimal_to_money')) { +if (!function_exists('decimal_to_money')) { /** * Create a Money object from a decimal string / currency pair. * @@ -53,7 +53,7 @@ function decimal_to_money(string $value, string $currency) } } -if (! function_exists('mollie_array_to_money')) { +if (!function_exists('mollie_array_to_money')) { /** * Create a Money object from a Mollie Amount array. * @@ -66,7 +66,7 @@ function mollie_array_to_money(array $array) } } -if (! function_exists('money_to_mollie_array')) { +if (!function_exists('money_to_mollie_array')) { /** * Create a Mollie Amount array from a Money object. * @@ -84,7 +84,7 @@ function money_to_mollie_array(Money $money) } } -if (! function_exists('mollie_object_to_money')) { +if (!function_exists('mollie_object_to_money')) { /** * Create a Money object from a Mollie Amount object. * @@ -97,7 +97,7 @@ function mollie_object_to_money(object $object) } } -if (! function_exists('money_to_decimal')) { +if (!function_exists('money_to_decimal')) { /** * Format the money as basic decimal diff --git a/src/Subscription.php b/src/Subscription.php index 5397b43..cb279b7 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -613,20 +613,22 @@ public function getCurrencyAttribute() } /** - * Gets the amount to be refunded for the subscription's unused time. + * Gets the amount to be reimbursed for the subscription's unused time. + * + * Result range value: (-X up to 0) * * @param \Carbon\Carbon|null $now * @return \Money\Money */ - public function getReimburseAmountForUnusedTime(?Carbon $now = null): ?Money + public function getReimbursableAmountForUnusedTime(?Carbon $now = null): Money { $now = $now ?: now(); if ($this->onTrial()) { - return null; + return $this->zero(); } if (round($this->getCycleLeftAttribute($now), 5) == 0) { - return null; + return $this->zero(); } return $this->reimbursableAmount() @@ -792,12 +794,10 @@ protected function reimburse(Money $amount, array $overrides = []) */ protected function reimbursableAmount() { - $zeroAmount = new Money('0.00', new Currency($this->currency)); - // Determine base amount eligible to reimburse $latestProcessedOrderItem = $this->latestProcessedOrderItem(); if (!$latestProcessedOrderItem) { - return $zeroAmount; + return $this->zero(); } $reimbursableAmount = $latestProcessedOrderItem->getTotal() @@ -831,7 +831,7 @@ protected function reimbursableAmount() // Guard against a negative value if ($reimbursableAmount->isNegative()) { - return $zeroAmount; + return $this->zero(); } return $reimbursableAmount; @@ -971,4 +971,9 @@ public function latestProcessedOrderItem() { return $this->orderItems()->processed()->orderByDesc('process_at')->first(); } + + private function zero(): Money + { + return new Money('0.00', new Currency($this->currency)); + } } diff --git a/tests/SubscriptionTest.php b/tests/SubscriptionTest.php index c1691c2..add5c51 100644 --- a/tests/SubscriptionTest.php +++ b/tests/SubscriptionTest.php @@ -8,7 +8,6 @@ use Laravel\Cashier\Cashier; use Laravel\Cashier\Events\SubscriptionResumed; use Laravel\Cashier\Subscription; -use Laravel\Cashier\Tests\Database\Factories\OrderFactory; use Laravel\Cashier\Tests\Database\Factories\OrderItemFactory; use Laravel\Cashier\Tests\Database\Factories\SubscriptionFactory; use Laravel\Cashier\Tests\Fixtures\User; @@ -592,7 +591,7 @@ public function canQueryRecurringSubscriptions() } /** @test */ - public function halfWayThroughSubscriptionReturnsPositiveReimburesmentAmount() + public function halfwayThroughSubscriptionReturnsPositiveReimbursementAmount() { $this->withConfiguredPlans(); @@ -611,7 +610,7 @@ public function halfWayThroughSubscriptionReturnsPositiveReimburesmentAmount() $this->assertEquals( money('-50', 'EUR'), - $subscriptionHalfWayThrough->getReimburseAmountForUnusedTime() + $subscriptionHalfWayThrough->getReimbursableAmountForUnusedTime() ); } @@ -634,8 +633,8 @@ public function nonReimbursableSubscriptionReturnsNoReimbursementAmount() ]); $this->assertEquals( - null, - $nonReimbursable->getReimburseAmountForUnusedTime() + money(0, 'EUR'), + $nonReimbursable->getReimbursableAmountForUnusedTime() ); } }