Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release Notes for Craft Commerce

## Unreleased

- Added `craft\commerce\base\Plan::getActiveSubscriptionCount()`.
- Added `craft\commerce\services\Subscriptions::getActiveSubscriptionCountByPlanId()`.
- Fixed a bug where the Subscription Plans index page was counting expired and suspended subscriptions as active. ([#4381](https://github.com/craftcms/commerce/issues/4381))

## 5.7.6 - 2026-10-08

- Fixed a bug where all catalog pricing rules’ prices could be removed if a rule was deleted while its catalog pricing job was running. ([#4374](https://github.com/craftcms/commerce/issues/4374))
Expand Down
11 changes: 11 additions & 0 deletions src/base/Plan.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* @property GatewayInterface $gateway
* @property Entry|null $information
* @property int $subscriptionCount
* @property-read int $activeSubscriptionCount
* @property User $user
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 2.0
Expand Down Expand Up @@ -123,6 +124,16 @@ public function getSubscriptionCount(): int
return Commerce::getInstance()->getSubscriptions()->getSubscriptionCountByPlanId($this->id);
}

/**
* Returns the number of active subscriptions for this plan.
*
* @since 5.7.7
*/
public function getActiveSubscriptionCount(): int
{
return Commerce::getInstance()->getSubscriptions()->getActiveSubscriptionCountByPlanId($this->id);
}

/**
* Returns whether there exists an active subscription for this plan for this user.
*/
Expand Down
13 changes: 13 additions & 0 deletions src/services/Subscriptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,19 @@ public function getSubscriptionCountByPlanId(int $planId): int
return SubscriptionRecord::find()->where(['planId' => $planId])->count();
}

/**
* Returns the number of active subscriptions for a plan.
*
* @since 5.7.7
*/
public function getActiveSubscriptionCountByPlanId(int $planId): int
{
return Subscription::find()
->planId($planId)
->status(Subscription::STATUS_ACTIVE)
->count();
}

/**
* Returns subscription count for a plan.
*
Expand Down
2 changes: 1 addition & 1 deletion src/templates/subscriptions/plans/index.twig
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
status: plan.enabled ? true : false,
handle: plan.handle|e,
gateway: plan.gateway.name|t('site')|e,
subscriptions: plan.subscriptionCount,
subscriptions: plan.activeSubscriptionCount,
information: plan.planInformationId ? 'Yes'|t('commerce')|e : 'No'|t('commerce')|e,
}]) %}
{% endfor %}
Expand Down
154 changes: 154 additions & 0 deletions tests/unit/services/SubscriptionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craftcommercetests\unit\services;

use Codeception\Test\Unit;
use Craft;
use craft\commerce\base\Plan;
use craft\commerce\elements\Subscription;
use craft\commerce\Plugin;
use craft\commerce\services\Subscriptions;
use craft\helpers\DateTimeHelper;
use craftcommercetests\fixtures\SubscriptionPlansFixture;
use UnitTester;

/**
* SubscriptionsTest
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 5.7.7
*/
class SubscriptionsTest extends Unit
{
protected UnitTester $tester;

protected Subscriptions $service;

/**
* @var Subscription[]
*/
private array $_subscriptions = [];

public function _fixtures(): array
{
return [
'plans' => ['class' => SubscriptionPlansFixture::class],
];
}

/**
* @param array<string, mixed> $attributes
* @param int $expectedCount
* @dataProvider activeSubscriptionCountDataProvider
*/
public function testGetActiveSubscriptionCountByPlanId(array $attributes, int $expectedCount): void
{
$plan = $this->_getPlan('monthly');
$this->_createSubscription($plan, $attributes);

self::assertSame($expectedCount, $this->service->getActiveSubscriptionCountByPlanId($plan->id));
self::assertSame($expectedCount, $plan->getActiveSubscriptionCount());
}

/**
* @return array<string, array{array<string, mixed>, int}>
*/
public function activeSubscriptionCountDataProvider(): array
{
return [
'active' => [[], 1],
'expired' => [['isExpired' => true, 'dateExpired' => DateTimeHelper::now()], 0],
'suspended' => [['isSuspended' => true, 'dateSuspended' => DateTimeHelper::now()], 0],
'not-started' => [['hasStarted' => false], 0],
'canceled-not-expired' => [['isCanceled' => true, 'dateCanceled' => DateTimeHelper::now()], 1],
];
}

public function testGetActiveSubscriptionCountByPlanIdExcludesTrashedSubscriptions(): void
{
$plan = $this->_getPlan('monthly');
$this->_createSubscription($plan);
$trashed = $this->_createSubscription($plan);

Craft::$app->getElements()->deleteElement($trashed);

self::assertSame(1, $this->service->getActiveSubscriptionCountByPlanId($plan->id));
}

public function testGetActiveSubscriptionCountByPlanIdIsScopedToPlan(): void
{
$monthlyPlan = $this->_getPlan('monthly');
$weeklyPlan = $this->_getPlan('weekly-disabled');

$this->_createSubscription($monthlyPlan);
$this->_createSubscription($weeklyPlan);
$this->_createSubscription($weeklyPlan);

self::assertSame(1, $this->service->getActiveSubscriptionCountByPlanId($monthlyPlan->id));
self::assertSame(2, $this->service->getActiveSubscriptionCountByPlanId($weeklyPlan->id));
}

public function testGetSubscriptionCountByPlanIdIncludesInactiveSubscriptions(): void
{
$plan = $this->_getPlan('monthly');
$this->_createSubscription($plan);
$this->_createSubscription($plan, ['isExpired' => true, 'dateExpired' => DateTimeHelper::now()]);
$this->_createSubscription($plan, ['isSuspended' => true, 'dateSuspended' => DateTimeHelper::now()]);
$this->_createSubscription($plan, ['hasStarted' => false]);

self::assertSame(4, $this->service->getSubscriptionCountByPlanId($plan->id));
self::assertSame(4, $plan->getSubscriptionCount());
self::assertSame(1, $plan->getActiveSubscriptionCount());
}

protected function _before(): void
{
parent::_before();

$this->service = Plugin::getInstance()->getSubscriptions();
}

protected function _after(): void
{
foreach ($this->_subscriptions as $subscription) {
Craft::$app->getElements()->deleteElementById($subscription->id, Subscription::class, null, true);
}

$this->_subscriptions = [];

parent::_after();
}

private function _getPlan(string $key): Plan
{
$planId = $this->tester->grabFixture('plans')->getModel($key)->id;

return Plugin::getInstance()->getPlans()->getPlanById($planId);
}

/**
* @param array<string, mixed> $attributes
*/
private function _createSubscription(Plan $plan, array $attributes = []): Subscription
{
$subscription = new Subscription();
$subscription->userId = 1;
$subscription->planId = $plan->id;
$subscription->gatewayId = $plan->gatewayId;
$subscription->reference = 'test-active-count-' . uniqid();
$subscription->trialDays = 0;
$subscription->hasStarted = true;
$subscription->subscriptionData = ['test' => 'active-count'];
Craft::configure($subscription, $attributes);

Craft::$app->getElements()->saveElement($subscription, false);
$this->_subscriptions[] = $subscription;

return $subscription;
}
}