- Implemented the ability for admins to grant subscriptions to doctors and clinics without payment. - Added new API endpoint `/api/v1/admin/subscription/grant` for granting subscriptions. - Updated the subscription model to track the admin who granted the subscription. - Enhanced the subscription report to include details about granted subscriptions. - Introduced a new `is_granted` field to indicate if a subscription was granted by an admin. - Updated the database schema to support the new functionality with a migration. - Added tests to ensure the correct behavior of the subscription granting process.
110 lines
4.2 KiB
PHP
110 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Subscription;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Config\Repository\SiteConfigRepository;
|
|
use App\Shared\Constant\ErrorCodes;
|
|
use App\Shared\Exception\AppException;
|
|
use App\Subscription\Entity\ClinicSubscription;
|
|
use App\Subscription\Entity\SubscriptionPeriod;
|
|
use App\Subscription\Entity\SubscriptionPlan;
|
|
use App\Subscription\Repository\ClinicSubscriptionRepository;
|
|
use App\Subscription\Repository\SubscriptionPeriodRepository;
|
|
use App\Subscription\Repository\SubscriptionPlanRepository;
|
|
use App\Subscription\Service\SubscriptionService;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* An admin granting a subscription must produce a paid-tier subscription with no
|
|
* payment attached, an audit trail, and no side effect on the target's unused
|
|
* trial.
|
|
*/
|
|
class GrantSubscriptionTest extends TestCase
|
|
{
|
|
private ?ClinicSubscription $saved = null;
|
|
|
|
private function service(?SubscriptionPeriod $period, ?ClinicSubscription $active): SubscriptionService
|
|
{
|
|
$subscriptionRepo = $this->createMock(ClinicSubscriptionRepository::class);
|
|
$subscriptionRepo->method('findActive')->willReturn($active);
|
|
$subscriptionRepo->method('save')->willReturnCallback(function (ClinicSubscription $s): void {
|
|
$this->saved = $s;
|
|
});
|
|
|
|
$periodRepo = $this->createMock(SubscriptionPeriodRepository::class);
|
|
$periodRepo->method('findByUuid')->willReturn($period);
|
|
|
|
return new SubscriptionService(
|
|
$subscriptionRepo,
|
|
$this->createMock(SubscriptionPlanRepository::class),
|
|
$periodRepo,
|
|
$this->createMock(SiteConfigRepository::class),
|
|
);
|
|
}
|
|
|
|
private function period(int $durationMonths): SubscriptionPeriod
|
|
{
|
|
$period = $this->createMock(SubscriptionPeriod::class);
|
|
$period->method('getPlan')->willReturn($this->createMock(SubscriptionPlan::class));
|
|
$period->method('getDurationMonths')->willReturn($durationMonths);
|
|
|
|
return $period;
|
|
}
|
|
|
|
public function testGrantCreatesSubscriptionWithoutPaymentAndRecordsTheAdmin(): void
|
|
{
|
|
$admin = $this->createMock(User::class);
|
|
$service = $this->service($this->period(1), null);
|
|
|
|
$subscription = $service->grant('doctor', 7, 'period-uuid', $admin);
|
|
|
|
$this->assertSame($subscription, $this->saved);
|
|
$this->assertSame('doctor', $subscription->getEntityType());
|
|
$this->assertSame(7, $subscription->getEntityId());
|
|
$this->assertNull($subscription->getPayment());
|
|
$this->assertSame($admin, $subscription->getGrantedBy());
|
|
$this->assertTrue($subscription->isGranted());
|
|
$this->assertEqualsWithDelta(time() + 30 * 86400, $subscription->getExpiresAt(), 5);
|
|
}
|
|
|
|
/** Granting must never burn an unused trial — `hasUsedTrial` reads this flag. */
|
|
public function testGrantIsNeverMarkedAsTrial(): void
|
|
{
|
|
$service = $this->service($this->period(1), null);
|
|
|
|
$subscription = $service->grant('clinic', 3, 'period-uuid', $this->createMock(User::class));
|
|
|
|
$this->assertFalse($subscription->isTrial());
|
|
$this->assertTrue($subscription->toArray()['is_granted']);
|
|
}
|
|
|
|
public function testUnknownPeriodIsRejected(): void
|
|
{
|
|
$service = $this->service(null, null);
|
|
|
|
try {
|
|
$service->grant('doctor', 1, 'missing-uuid', $this->createMock(User::class));
|
|
$this->fail('expected AppException');
|
|
} catch (AppException $e) {
|
|
$this->assertSame(ErrorCodes::ERR_NOT_FOUND_001, $e->getErrorCode());
|
|
$this->assertSame(404, $e->getHttpStatus());
|
|
}
|
|
}
|
|
|
|
/** Boundary: an active subscription is extended from its own expiry, not from today. */
|
|
public function testGrantExtendsAnActiveSubscriptionInsteadOfRestartingIt(): void
|
|
{
|
|
$currentExpiry = time() + 20 * 86400;
|
|
|
|
$active = $this->createMock(ClinicSubscription::class);
|
|
$active->method('getExpiresAt')->willReturn($currentExpiry);
|
|
|
|
$service = $this->service($this->period(1), $active);
|
|
|
|
$subscription = $service->grant('doctor', 7, 'period-uuid', $this->createMock(User::class));
|
|
|
|
$this->assertEqualsWithDelta($currentExpiry + 30 * 86400, $subscription->getExpiresAt(), 5);
|
|
}
|
|
}
|