Skip to content
This repository has been archived by the owner on Nov 9, 2021. It is now read-only.

Commit

Permalink
Merge pull request #12 from renoki-co/feature/check-overquota
Browse files Browse the repository at this point in the history
[feature] Check quotas before swapping
  • Loading branch information
rennokki authored Aug 5, 2020
2 parents 667c80e + becd5bb commit 2d90e33
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 7 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
PADDLE_VENDOR_ID: ${{ secrets.PADDLE_VENDOR_ID }}
PADDLE_VENDOR_AUTH_CODE: ${{ secrets.PADDLE_VENDOR_AUTH_CODE }}
PADDLE_TEST_PLAN: ${{ secrets.PADDLE_TEST_PLAN }}
PADDLE_TEST_FREE_PLAN: ${{ secrets.PADDLE_TEST_FREE_PLAN }}

- uses: codecov/codecov-action@v1
with:
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,30 @@ Saas::plan('Gold Plan', 'gold-plan')
]);
```

## Checking for overexceeded quotas

When swapping from a bigger plan to a small plan, you might restrict users from doing it unless all the quotas do not exceed the smaller plan's quotas.

For example, an user can subscribe to a plan that has 10 teams, it creates 10 teams, but later decides to downgrade. In this case, you can check which features
are exceeding:

```php
$freePlan = Saas::plan('Free Plan', 'free-plan');
$paidPlan = Saas::plan('Paid Plan', 'paid-plan');

// Returning an Illuminate\Support\Collection instance with each
// item as RenokiCo\CashierRegister\Feature instance.
$overQuotaFeatures = $subscription->featuresOverQuotaWhenSwapping(
$paidPlan->getId()
);

foreach ($overQuotaFeatures as $feature) {
// $feature->getName();
}
```

**Please keep in mind that this works only for non-resettable features, like Teams, Members, etc. due to the fact that features, when swapping between plans, should be handled manually, either wanting to keep them as-is or resetting them using `resetQuotas()`**

## Static items

In case you are not using plans, you can describe items once in Cashier Register's service provider and then leverage it for some neat usage:
Expand Down
46 changes: 39 additions & 7 deletions src/Concerns/HasQuotas.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace RenokiCo\CashierRegister\Concerns;

use RenokiCo\CashierRegister\Feature;
use RenokiCo\CashierRegister\Models\Usage;
use RenokiCo\CashierRegister\Saas;

trait HasQuotas
{
Expand Down Expand Up @@ -116,11 +118,12 @@ public function getUsedQuota(string $id): int
* Get the feature quota remaining.
*
* @param string $id
* @param null|string $planId
* @return int
*/
public function getRemainingQuota(string $id): int
public function getRemainingQuota(string $id, $planId = null): int
{
$featureValue = $this->getFeatureQuota($id);
$featureValue = $this->getFeatureQuota($id, $planId);

if ($featureValue < 0) {
return -1;
Expand All @@ -133,11 +136,14 @@ public function getRemainingQuota(string $id): int
* Get the feature quota.
*
* @param string $id
* @param null|string $planId
* @return int
*/
public function getFeatureQuota(string $id): int
public function getFeatureQuota(string $id, $planId = null): int
{
$feature = $this->getPlan()->getFeature($id);
$plan = $planId ? Saas::getPlan($planId) : $this->getPlan();

$feature = $plan->getFeature($id);

if (! $feature) {
return 0;
Expand All @@ -150,17 +156,43 @@ public function getFeatureQuota(string $id): int
* Check if the feature is over the assigned quota.
*
* @param string $id
* @param null|string $planId
* @return bool
*/
public function featureOverQuota(string $id): bool
public function featureOverQuota(string $id, $planId = null): bool
{
$feature = $this->getPlan()->getFeature($id);
$plan = $planId ? Saas::getPlan($planId) : $this->getPlan();

$feature = $plan->getFeature($id);

if ($feature->isUnlimited()) {
return false;
}

return $this->getRemainingQuota($id) < 0;
return $this->getRemainingQuota($id, $planId) < 0;
}

/**
* Check if there are features over quota
* if the current subscription would be swapped
* to a new one.
*
* @param string $planId
* @return \Illuminate\Support\Collection
*/
public function featuresOverQuotaWhenSwapping(string $planId)
{
$plan = Saas::getPlan($planId);

return collect($plan->getFeatures())
->filter
->isNotResettable()
->filter(function (Feature $feature) use ($plan) {
return $this->getRemainingQuota(
$feature->getId(), $plan->getId()
) < 0;
})
->values();
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ public function isResettable(): bool
return $this->resettable;
}

/**
* Check if this feature is not resettable after each billing cycle.
*
* @return bool
*/
public function isNotResettable(): bool
{
return ! $this->isResettable();
}

/**
* Check if the feature has unlimited uses.
*
Expand Down
31 changes: 31 additions & 0 deletions tests/PaddleFeatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,35 @@ public function test_feature_usage_on_unlimited()
-1, $subscription->getRemainingQuota('teams')
);
}

public function test_downgrading_plan()
{
$user = factory(User::class)->create();

$freePlan = Saas::getPlan(static::$stripeFreePlanId);

$paidPlan = Saas::getPlan(static::$stripePlanId);

$subscription = $user->subscriptions()->create([
'name' => 'main',
'paddle_id' => 1,
'paddle_plan' => static::$paddlePlanId,
'paddle_status' => 'active',
'quantity' => 1,
]);

$subscription->recordFeatureUsage('teams', 10);

$overQuotaFeatures = $subscription->featuresOverQuotaWhenSwapping(
static::$paddleFreePlanId
);

$this->assertCount(
1, $overQuotaFeatures
);

$this->assertEquals(
'teams', $overQuotaFeatures->first()->getId()
);
}
}
25 changes: 25 additions & 0 deletions tests/StripeFeatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,29 @@ public function test_feature_usage_on_unlimited()
-1, $subscription->getRemainingQuota('teams')
);
}

public function test_downgrading_plan()
{
$user = factory(User::class)->create();

$freePlan = Saas::getPlan(static::$stripeFreePlanId);

$paidPlan = Saas::getPlan(static::$stripePlanId);

$subscription = $user->newSubscription('main', static::$stripePlanId)->create('pm_card_visa');

$subscription->recordFeatureUsage('teams', 10);

$overQuotaFeatures = $subscription->featuresOverQuotaWhenSwapping(
static::$stripeFreePlanId
);

$this->assertCount(
1, $overQuotaFeatures
);

$this->assertEquals(
'teams', $overQuotaFeatures->first()->getId()
);
}
}
41 changes: 41 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ abstract class TestCase extends Orchestra
{
protected static $productId;

protected static $freeProductId;

protected static $stripePlanId;

protected static $stripeFreePlanId;

protected static $paddlePlanId;

protected static $paddleFreePlanId;

/**
* {@inheritdoc}
*/
Expand All @@ -44,11 +50,23 @@ public function setUp(): void
Saas::feature('Seats', 'teams', 10)->notResettable(),
]);

Saas::plan('Free Plan', static::$stripeFreePlanId)
->features([
Saas::feature('Build Minutes', 'build.minutes', 10),
Saas::feature('Seats', 'teams', 5)->notResettable(),
]);

Saas::plan('Monthly $20', static::$paddlePlanId)
->features([
Saas::feature('Build Minutes', 'build.minutes', 3000),
Saas::feature('Seats', 'teams', 10)->notResettable(),
]);

Saas::plan('Free Plan', static::$paddleFreePlanId)
->features([
Saas::feature('Build Minutes', 'build.minutes', 10),
Saas::feature('Seats', 'teams', 5)->notResettable(),
]);
}

/**
Expand All @@ -62,14 +80,24 @@ public static function setUpBeforeClass(): void

static::$stripePlanId = 'monthly-10-'.Str::random(10);

static::$stripeFreePlanId = 'free-'.Str::random(10);

static::$productId = 'product-1'.Str::random(10);

static::$freeProductId = 'product-free'.Str::random(10);

Product::create([
'id' => static::$productId,
'name' => 'Laravel Cashier Test Product',
'type' => 'service',
]);

Product::create([
'id' => static::$freeProductId,
'name' => 'Laravel Cashier Test Product',
'type' => 'service',
]);

Plan::create([
'id' => static::$stripePlanId,
'nickname' => 'Monthly $10',
Expand All @@ -80,7 +108,19 @@ public static function setUpBeforeClass(): void
'product' => static::$productId,
]);

Plan::create([
'id' => static::$stripeFreePlanId,
'nickname' => 'Free',
'currency' => 'USD',
'interval' => 'month',
'billing_scheme' => 'per_unit',
'amount' => 0,
'product' => static::$freeProductId,
]);

static::$paddlePlanId = getenv('PADDLE_TEST_PLAN') ?: env('PADDLE_TEST_PLAN');

static::$paddleFreePlanId = getenv('PADDLE_TEST_FREE_PLAN') ?: env('PADDLE_TEST_FREE_PLAN');
}

/**
Expand All @@ -91,6 +131,7 @@ public static function tearDownAfterClass(): void
parent::tearDownAfterClass();

static::deleteStripeResource(new Plan(static::$stripePlanId));
static::deleteStripeResource(new Plan(static::$stripeFreePlanId));
}

/**
Expand Down

0 comments on commit 2d90e33

Please sign in to comment.