diff --git a/src/Models/Concerns/HasSubscriptions.php b/src/Models/Concerns/HasSubscriptions.php index 14b3fe1..cd7e68a 100644 --- a/src/Models/Concerns/HasSubscriptions.php +++ b/src/Models/Concerns/HasSubscriptions.php @@ -106,7 +106,7 @@ public function setConsumedQuota($featureName, float $consumption) ->whereFeatureId($feature->id) ->firstOrNew(); - if ($featureConsumption->consumption === $consumption) { + if ($featureConsumption->consumption == $consumption) { return; } diff --git a/src/Models/Subscription.php b/src/Models/Subscription.php index 0a27907..3d88ba5 100644 --- a/src/Models/Subscription.php +++ b/src/Models/Subscription.php @@ -110,9 +110,15 @@ public function renew(?Carbon $expirationDate = null): self ]); $expirationDate = $this->getRenewedExpiration($expirationDate); + $graceDaysEndedAt = null; + + if ($this->plan->grace_days) { + $graceDaysEndedAt = $expirationDate->copy()->addDays($this->plan->grace_days); + } $this->update([ 'expired_at' => $expirationDate, + 'grace_days_ended_at' => $graceDaysEndedAt, ]); event(new SubscriptionRenewed($this)); diff --git a/tests/Models/Concerns/HasSubscriptionsTest.php b/tests/Models/Concerns/HasSubscriptionsTest.php index 29cc427..ad72a18 100644 --- a/tests/Models/Concerns/HasSubscriptionsTest.php +++ b/tests/Models/Concerns/HasSubscriptionsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Models\Concerns; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; @@ -21,6 +22,7 @@ use LucasDotVin\Soulbscription\Models\SubscriptionRenewal; use OutOfBoundsException; use OverflowException; +use ReflectionClass; use Tests\Mocks\Models\User; use Tests\TestCase; @@ -427,6 +429,42 @@ public function testModelGetFeaturesFromTickets() ); } + public function testModelGetFeaturesFromPreviouslyLoadedTickets() + { + $feature = Feature::factory()->createOne(); + $subscriber = User::factory()->createOne(); + + $reflection = new ReflectionClass($subscriber); + $property = $reflection->getProperty('loadedTicketFeatures'); + $property->setAccessible(true); + $property->setValue($subscriber, Collection::make([$feature])); + + config()->set('soulbscription.feature_tickets', true); + + $features = $subscriber->getFeaturesAttribute(); + + $this->assertCount(1, $features); + $this->assertTrue($features->contains($feature)); + } + + public function testModelGetFeaturesFromPreviouslyLoadedSubscription() + { + $feature = Feature::factory()->createOne(); + $subscriber = User::factory()->createOne(); + + $reflection = new ReflectionClass($subscriber); + $property = $reflection->getProperty('loadedSubscriptionFeatures'); + $property->setAccessible(true); + $property->setValue($subscriber, Collection::make([$feature])); + + config()->set('soulbscription.feature_tickets', true); + + $features = $subscriber->getFeaturesAttribute(); + + $this->assertCount(1, $features); + $this->assertTrue($features->contains($feature)); + } + public function testModelGetFeaturesFromNonExpirableTickets() { $feature = Feature::factory()->consumable()->createOne(); @@ -805,6 +843,31 @@ public function testItCanSetQuotaFeatureConsumption() ]); } + public function testItDoesNothingWhileSettingQuotaIfTheGivenAmountIsTheSameAsTheBalance() + { + $charges = $this->faker->numberBetween(5, 10); + $consumption = $this->faker->numberBetween(1, $charges); + + $plan = Plan::factory()->createOne(); + $feature = Feature::factory()->quota()->createOne(); + $feature->plans()->attach($plan, [ + 'charges' => $charges, + ]); + + $subscriber = User::factory()->createOne(); + $subscriber->subscribeTo($plan); + + $subscriber->consume($feature->name, $consumption); + $subscriber->setConsumedQuota($feature->name, $consumption); + + $this->assertDatabaseHas('feature_consumptions', [ + 'consumption' => $consumption, + 'feature_id' => $feature->id, + 'subscriber_id' => $subscriber->id, + 'expired_at' => null, + ]); + } + public function testItRaisesAnExceptionWhenSettingConsumedQuotaForANotQuotaFeature() { $charges = $this->faker->numberBetween(5, 10); @@ -908,7 +971,7 @@ public function testItDoesNotReturnNegativeChargesForFeatures() $this->assertEquals(0, $subscriber->getRemainingCharges($feature->name)); } - public function testItReturnsNegativeBalanceForFeatures() + public function testItReturnsNegativeBalanceForPostpaidFeatures() { $charges = $this->faker->numberBetween(5, 10); $consumption = $this->faker->numberBetween($charges + 1, $charges * 2); @@ -927,6 +990,16 @@ public function testItReturnsNegativeBalanceForFeatures() $this->assertLessThan(0, $subscriber->balance($feature->name)); } + public function testItReturnsZeroForUnavailableFeatures() + { + $feature = Feature::factory()->createOne(); + $subscriber = User::factory()->createOne(); + + $remainingCharges = $subscriber->getRemainingCharges($feature->name); + + $this->assertEquals(0, $remainingCharges); + } + public function testItReturnsRemainingChargesOnlyForTheGivenUser() { config(['soulbscription.feature_tickets' => true]); @@ -943,4 +1016,84 @@ public function testItReturnsRemainingChargesOnlyForTheGivenUser() $this->assertEquals($charges, $subscriber->getRemainingCharges($feature->name)); } + + public function testItCanCheckIfSubscriberHasFeature() + { + $plan = Plan::factory()->createOne(); + $feature = Feature::factory()->createOne(); + $feature->plans()->attach($plan); + + $subscriber = User::factory()->createOne(); + $subscriber->subscribeTo($plan); + + $this->assertTrue($subscriber->hasFeature($feature->name)); + } + + public function testItCanCheckIfSubscriberDoesNotHaveFeature() + { + $plan = Plan::factory()->createOne(); + $feature = Feature::factory()->createOne(); + + $subscriber = User::factory()->createOne(); + $subscriber->subscribeTo($plan); + + $this->assertFalse($subscriber->hasFeature($feature->name)); + } + + public function testItCanAlwaysConsumeAPostpaidFeature() + { + $charges = $this->faker->numberBetween(5, 10); + $consumption = $this->faker->numberBetween($charges + 1, $charges * 2); + + $plan = Plan::factory()->createOne(); + $feature = Feature::factory()->postpaid()->createOne(); + $feature->plans()->attach($plan, [ + 'charges' => $charges, + ]); + + $subscriber = User::factory()->createOne(); + $subscriber->subscribeTo($plan); + + $this->assertTrue($subscriber->canConsume($feature->name, $consumption)); + + $subscriber->consume($feature->name, $consumption); + + $this->assertDatabaseHas('feature_consumptions', [ + 'consumption' => $consumption, + 'feature_id' => $feature->id, + 'subscriber_id' => $subscriber->id, + ]); + } + + public function testItSetsAnEmptyExpirationIfThePlanHasNoPeriodicity() + { + $plan = Plan::factory()->createOne([ + 'periodicity' => null, + ]); + + $subscriber = User::factory()->createOne(); + $subscription = $subscriber->subscribeTo($plan); + + $this->assertNull($subscription->expired_at); + } + + public function testItReturnsZeroForCurrentConsumptionWhenSubscriberDoesNotHaveFeature() + { + $feature = Feature::factory()->createOne(); + $subscriber = User::factory()->createOne(); + + $currentConsumption = $subscriber->getCurrentConsumption($feature->name); + + $this->assertEquals(0, $currentConsumption); + } + + public function testItReturnsZeroForTotalChargesWhenSubscriberDoesNotHaveFeature() + { + $feature = Feature::factory()->createOne(); + $subscriber = User::factory()->createOne(); + + $totalCharges = $subscriber->getTotalCharges($feature->name); + + $this->assertEquals(0, $totalCharges); + } } diff --git a/tests/Models/SubscriptionTest.php b/tests/Models/SubscriptionTest.php index e2cc4bd..730d586 100644 --- a/tests/Models/SubscriptionTest.php +++ b/tests/Models/SubscriptionTest.php @@ -343,4 +343,70 @@ public function testModelReturnsOnlyNotCanceledSubscriptionsWithTheScope() fn ($subscription) => $this->assertContains($subscription->id, $returnedSubscriptions->pluck('id')) ); } + + public function testModelUpdatesGraceDaysEndedAtWhenRenewing() + { + $subscriber = User::factory()->create(); + $plan = Plan::factory()->create([ + 'grace_days' => $graceDays = $this->faker()->randomDigitNotNull(), + ]); + + $subscription = Subscription::factory() + ->for($plan) + ->for($subscriber, 'subscriber') + ->create([ + 'grace_days_ended_at' => now()->subDay(), + ]); + + $subscription->renew(); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'grace_days_ended_at' => $subscription->expired_at->addDays($graceDays), + ]); + } + + public function testModelLeavesGraceDaysEmptyWhenRenewingIfPlanDoesNotHaveIt() + { + $subscriber = User::factory()->create(); + $plan = Plan::factory()->create([ + 'grace_days' => 0, + ]); + + $subscription = Subscription::factory() + ->for($plan) + ->for($subscriber, 'subscriber') + ->create([ + 'grace_days_ended_at' => null, + ]); + + $subscription->renew(); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'grace_days_ended_at' => null, + ]); + } + + public function testModelUsesProvidedExpirationAtRenewing() + { + $subscriber = User::factory()->create(); + $plan = Plan::factory()->create(); + + $subscription = Subscription::factory() + ->for($plan) + ->for($subscriber, 'subscriber') + ->create([ + 'expired_at' => now()->subDay(), + ]); + + $expectedExpiredAt = now()->addDays($days = $this->faker()->randomDigitNotNull())->toDateTimeString(); + + $subscription->renew(now()->addDays($days)); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'expired_at' => $expectedExpiredAt, + ]); + } }