feat: add admin subscription granting feature
- 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.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user