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); } }