subscriptionRepo->findActive($entityType, $entityId); } /** * پلن مؤثر: پلن اشتراک فعال، یا در نبود اشتراک، پلن پیش‌فرض «free». */ public function getEffectivePlan(string $entityType, int $entityId): ?SubscriptionPlan { $subscription = $this->getActiveSubscription($entityType, $entityId); return $subscription?->getPlan() ?? $this->planRepo->findByName('free'); } public function hasFeature(string $entityType, int $entityId, string $feature): bool { $plan = $this->getEffectivePlan($entityType, $entityId); return $plan !== null && $plan->hasFeature($feature); } public function getSecretaryLimit(string $entityType, int $entityId): int { $plan = $this->getEffectivePlan($entityType, $entityId); return $plan?->getMaxSecretaries() ?? 1; } public function hasUsedTrial(string $entityType, int $entityId): bool { return $this->subscriptionRepo->hasUsedTrial($entityType, $entityId); } public function activateTrial(string $entityType, int $entityId): ClinicSubscription { if ($this->hasUsedTrial($entityType, $entityId)) { throw new AppException(ErrorCodes::ERR_TRIAL_ALREADY_USED, null, 422); } $trialEnabled = $this->configRepo->get('trial_enabled'); if ($trialEnabled === '0') { throw new AppException(ErrorCodes::ERR_TRIAL_DISABLED, null, 422); } $basicPlan = $this->planRepo->findByName('basic'); if ($basicPlan === null) { throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, null, 500); } // Deactivating the trial period is a normal admin configuration, not a // broken install — surface it as "trial disabled", the same 422 the // trial_enabled flag produces, rather than a 500. $trialPeriod = $this->periodRepo->findTrialPeriodForPlan($basicPlan); if ($trialPeriod === null) { throw new AppException(ErrorCodes::ERR_TRIAL_DISABLED, null, 422); } $expiresAt = $this->calculateExpiresAt(null, $trialPeriod->getDurationMonths()); $subscription = new ClinicSubscription( $entityType, $entityId, $basicPlan, $trialPeriod, true, $expiresAt, null ); $this->subscriptionRepo->save($subscription); return $subscription; } public function createFromPayment(Payment $payment, string $entityType, int $entityId, string $periodUuid): ClinicSubscription { $period = $this->periodRepo->findByUuid($periodUuid); if ($period === null) { throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, null, 404); } $currentSub = $this->getActiveSubscription($entityType, $entityId); $currentExpires = $currentSub?->getExpiresAt(); $expiresAt = $this->calculateExpiresAt($currentExpires, $period->getDurationMonths()); $subscription = new ClinicSubscription( $entityType, $entityId, $period->getPlan(), $period, false, $expiresAt, $payment ); $this->subscriptionRepo->save($subscription); return $subscription; } /** حذف اشتراکِ ساخته‌شده از یک پرداخت (هنگام استرداد/برگشت وجه). */ public function deleteByPayment(\App\Payment\Entity\Payment $payment): void { $subscription = $this->subscriptionRepo->findByPayment($payment); if ($subscription !== null) { $this->subscriptionRepo->remove($subscription); } } public function calculateExpiresAt(?int $currentExpiresAt, int $durationMonths): int { $base = max($currentExpiresAt ?? 0, time()); return $base + $durationMonths * 30 * 86400; } }