Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9fa8fc5
Bump dependencies for Laravel 11
laravel-shift Mar 2, 2024
2f7d01f
Update GitHub Actions for Laravel 11
laravel-shift Mar 2, 2024
fb5ccd8
Merge pull request #96 from laravel-shift/l11-compatibility
lucasdotvin Apr 8, 2024
3662a78
fix: Replace unsignedInteger and unsignedDecimal calls
lucasdotvin Apr 8, 2024
02707b3
feat: Set up tests with PHP 8.3
lucasdotvin Apr 8, 2024
c31adf3
fix: Fix date difference parameters order
lucasdotvin Apr 8, 2024
e7815d3
Bump dependencies for Laravel 12
laravel-shift Feb 16, 2025
7841ac9
Update GitHub Actions for Laravel 12
laravel-shift Feb 16, 2025
63567d9
test: Cover grace days update
lucasdotvin Mar 20, 2025
9ee55b5
feat: Update grace days when renewing
lucasdotvin Mar 20, 2025
6334cc7
test: Cover new renewing-related cases
lucasdotvin Mar 20, 2025
4d19c53
fix: Get at least one recurrence
lucasdotvin Mar 20, 2025
7ef10f0
test: Cover plans without grace days
lucasdotvin Mar 20, 2025
05ffe80
feat: Leave grace days empty when necessary
lucasdotvin Mar 20, 2025
bcde5fe
refactor: Use factories for set grace days
lucasdotvin Mar 20, 2025
9880729
Merge pull request #117 from laravel-shift/l12-compatibility
lucasdotvin Mar 20, 2025
1697c44
feat: Normalize recurrence delta manually
lucasdotvin Mar 20, 2025
262ca23
Merge branch 'develop' of github.com:lucasdotvin/laravel-soulbscripti…
lucasdotvin Mar 20, 2025
442f42a
Merge branch 'main' into develop
lucasdotvin Mar 20, 2025
d899df8
Merge branch 'main' into develop
lucasdotvin Mar 20, 2025
7af5c35
Merge branch 'main' into develop
lucasdotvin Mar 20, 2025
e4aa9ac
test: Max test coverage
lucasdotvin Mar 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Models/Concerns/HasSubscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function setConsumedQuota($featureName, float $consumption)
->whereFeatureId($feature->id)
->firstOrNew();

if ($featureConsumption->consumption === $consumption) {
if ($featureConsumption->consumption == $consumption) {
return;
}

Expand Down
6 changes: 6 additions & 0 deletions src/Models/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
155 changes: 154 additions & 1 deletion tests/Models/Concerns/HasSubscriptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +22,7 @@
use LucasDotVin\Soulbscription\Models\SubscriptionRenewal;
use OutOfBoundsException;
use OverflowException;
use ReflectionClass;
use Tests\Mocks\Models\User;
use Tests\TestCase;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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]);
Expand All @@ -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);
}
}
66 changes: 66 additions & 0 deletions tests/Models/SubscriptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);
}
}