395 lines
12 KiB
PHP
395 lines
12 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Config\BusinessModules;
|
||
use App\Models\OrganizationSubscriptionModel;
|
||
|
||
/**
|
||
* Сервис для работы с подписками на модули
|
||
*
|
||
* Предоставляет API для проверки статуса подписок,
|
||
* активации модулей и управления триальными периодами.
|
||
*/
|
||
class ModuleSubscriptionService
|
||
{
|
||
protected OrganizationSubscriptionModel $subscriptionModel;
|
||
protected BusinessModules $modulesConfig;
|
||
protected ?int $activeOrgId = null;
|
||
|
||
public function __construct(
|
||
?OrganizationSubscriptionModel $subscriptionModel = null,
|
||
?BusinessModules $modulesConfig = null
|
||
) {
|
||
$this->subscriptionModel = $subscriptionModel ?? new OrganizationSubscriptionModel();
|
||
$this->modulesConfig = $modulesConfig ?? config('BusinessModules');
|
||
}
|
||
|
||
/**
|
||
* Получение активного ID организации из сессии
|
||
*/
|
||
protected function getActiveOrgId(): ?int
|
||
{
|
||
if ($this->activeOrgId === null) {
|
||
$this->activeOrgId = session()->get('active_org_id') ?? 0;
|
||
}
|
||
|
||
return $this->activeOrgId ?: null;
|
||
}
|
||
|
||
/**
|
||
* Проверка активности модуля для текущей организации
|
||
*
|
||
* @param string $moduleCode Код модуля (crm, booking, tasks, proof)
|
||
* @param int|null $organizationId ID организации (null = из сессии)
|
||
* @return bool
|
||
*/
|
||
public function isModuleActive(string $moduleCode, ?int $organizationId = null): bool
|
||
{
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
// Базовый модуль всегда активен
|
||
if ($moduleCode === 'base') {
|
||
return true;
|
||
}
|
||
|
||
return $this->subscriptionModel->isModuleActive($orgId, $moduleCode);
|
||
}
|
||
|
||
/**
|
||
* Проверка что модуль доступен (активен или в триале)
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int|null $organizationId
|
||
* @return bool
|
||
*/
|
||
public function isModuleAvailable(string $moduleCode, ?int $organizationId = null): bool
|
||
{
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
// Базовый модуль всегда доступен
|
||
if ($moduleCode === 'base') {
|
||
return true;
|
||
}
|
||
|
||
// Проверяем подписку
|
||
$subscription = $this->subscriptionModel->getSubscription($orgId, $moduleCode);
|
||
|
||
if (!$subscription) {
|
||
return false;
|
||
}
|
||
|
||
// Активен или в триале
|
||
return in_array($subscription['status'], ['trial', 'active'], true);
|
||
}
|
||
|
||
/**
|
||
* Проверка что модуль в триальном периоде
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int|null $organizationId
|
||
* @return bool
|
||
*/
|
||
public function isInTrial(string $moduleCode, ?int $organizationId = null): bool
|
||
{
|
||
// Базовый модуль не имеет триала
|
||
if ($moduleCode === 'base') {
|
||
return false;
|
||
}
|
||
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
return $this->subscriptionModel->isInTrial($orgId, $moduleCode);
|
||
}
|
||
|
||
/**
|
||
* Проверка что триал истекает скоро (для показа уведомлений)
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int $daysThreshold Порог в днях
|
||
* @param int|null $organizationId
|
||
* @return bool
|
||
*/
|
||
public function isTrialExpiringSoon(
|
||
string $moduleCode,
|
||
int $daysThreshold = 3,
|
||
?int $organizationId = null
|
||
): bool {
|
||
if (!$this->isInTrial($moduleCode, $organizationId)) {
|
||
return false;
|
||
}
|
||
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
$daysLeft = $this->subscriptionModel->getDaysUntilExpire($orgId, $moduleCode);
|
||
|
||
return $daysLeft !== null && $daysLeft <= $daysThreshold && $daysLeft > 0;
|
||
}
|
||
|
||
/**
|
||
* Получение дней до окончания подписки/триала
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int|null $organizationId
|
||
* @return int|null Количество дней или null если не активна
|
||
*/
|
||
public function getDaysUntilExpire(string $moduleCode, ?int $organizationId = null): ?int
|
||
{
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return null;
|
||
}
|
||
|
||
return $this->subscriptionModel->getDaysUntilExpire($orgId, $moduleCode);
|
||
}
|
||
|
||
/**
|
||
* Получение информации о модуле из конфигурации
|
||
*
|
||
* @param string $moduleCode
|
||
* @return array|null
|
||
*/
|
||
public function getModuleInfo(string $moduleCode): ?array
|
||
{
|
||
return $this->modulesConfig->getModule($moduleCode);
|
||
}
|
||
|
||
/**
|
||
* Получение всех доступных модулей для организации
|
||
*
|
||
* @param int|null $organizationId
|
||
* @return array Список кодов модулей
|
||
*/
|
||
public function getActiveModules(?int $organizationId = null): array
|
||
{
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return ['base'];
|
||
}
|
||
|
||
$activeModules = $this->subscriptionModel->getActiveModules($orgId);
|
||
|
||
// Всегда добавляем базовый модуль
|
||
$activeModules[] = 'base';
|
||
|
||
return array_unique($activeModules);
|
||
}
|
||
|
||
/**
|
||
* Запуск триального периода для модуля
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int|null $organizationId
|
||
* @param int $trialDays
|
||
* @return bool
|
||
*/
|
||
public function startTrial(
|
||
string $moduleCode,
|
||
?int $organizationId = null,
|
||
int $trialDays = 14
|
||
): bool {
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
// Проверяем что модуль существует
|
||
if (!$this->modulesConfig->exists($moduleCode)) {
|
||
return false;
|
||
}
|
||
|
||
// Проверяем что модуль платный
|
||
$moduleInfo = $this->modulesConfig->getModule($moduleCode);
|
||
if (!$moduleInfo || $moduleInfo['trial_days'] <= 0) {
|
||
return false;
|
||
}
|
||
|
||
// Проверяем что триал ещё не был использован
|
||
if ($this->subscriptionModel->isInTrial($orgId, $moduleCode)) {
|
||
return false; // Уже в триале
|
||
}
|
||
|
||
// Проверяем что нет активной подписки
|
||
if ($this->isModuleActive($moduleCode, $orgId)) {
|
||
return false; // Уже активна
|
||
}
|
||
|
||
return (bool) $this->subscriptionModel->startTrial(
|
||
$orgId,
|
||
$moduleCode,
|
||
$moduleInfo['trial_days']
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Активация платной подписки (для ручного включения или после платежа)
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int $months
|
||
* @param int|null $organizationId
|
||
* @return bool
|
||
*/
|
||
public function activate(
|
||
string $moduleCode,
|
||
int $months = 1,
|
||
?int $organizationId = null
|
||
): bool {
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
// Проверяем что модуль существует
|
||
if (!$this->modulesConfig->exists($moduleCode)) {
|
||
return false;
|
||
}
|
||
|
||
return $this->subscriptionModel->activate($orgId, $moduleCode, $months);
|
||
}
|
||
|
||
/**
|
||
* Отмена подписки
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int|null $organizationId
|
||
* @return bool
|
||
*/
|
||
public function cancel(string $moduleCode, ?int $organizationId = null): bool
|
||
{
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
return $this->subscriptionModel->cancel($orgId, $moduleCode);
|
||
}
|
||
|
||
/**
|
||
* Проверка доступа к функции модуля
|
||
*
|
||
* @param string $moduleCode
|
||
* @param string $feature Опционально - конкретная фича
|
||
* @return bool
|
||
*/
|
||
public function canUseModule(string $moduleCode, string $feature = ''): bool
|
||
{
|
||
if (!$this->isModuleActive($moduleCode)) {
|
||
return false;
|
||
}
|
||
|
||
// Для триальных версий могут быть ограничения на некоторые фичи
|
||
if ($this->isInTrial($moduleCode)) {
|
||
// Проверяем доступность фичи в триале
|
||
// (можно расширить через конфигурацию)
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Получение статуса подписки для отображения в UI
|
||
*
|
||
* @param string $moduleCode
|
||
* @param int|null $organizationId
|
||
* @return array
|
||
*/
|
||
public function getSubscriptionStatus(
|
||
string $moduleCode,
|
||
?int $organizationId = null
|
||
): array {
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
$moduleInfo = $this->modulesConfig->getModule($moduleCode);
|
||
$subscription = $orgId
|
||
? $this->subscriptionModel->getSubscription($orgId, $moduleCode)
|
||
: null;
|
||
|
||
$status = 'unavailable';
|
||
$daysLeft = null;
|
||
$message = '';
|
||
|
||
if ($moduleCode === 'base') {
|
||
$status = 'active';
|
||
$message = 'Базовый модуль';
|
||
} elseif (!$subscription) {
|
||
if ($moduleInfo && $moduleInfo['trial_days'] > 0) {
|
||
$status = 'trial_available';
|
||
$message = 'Доступен триал ' . $moduleInfo['trial_days'] . ' дней';
|
||
} else {
|
||
$status = 'locked';
|
||
$message = 'Приобретите модуль';
|
||
}
|
||
} elseif ($subscription['status'] === 'trial') {
|
||
$daysLeft = $this->subscriptionModel->getDaysUntilExpire($orgId, $moduleCode);
|
||
if ($daysLeft && $daysLeft > 0) {
|
||
$status = 'trial';
|
||
$message = "Триал: {$daysLeft} дн.";
|
||
} else {
|
||
$status = 'expired';
|
||
$message = 'Триал истёк';
|
||
}
|
||
} elseif ($subscription['status'] === 'active') {
|
||
$daysLeft = $this->subscriptionModel->getDaysUntilExpire($orgId, $moduleCode);
|
||
$status = 'active';
|
||
$message = $daysLeft ? "Осталось {$daysLeft} дн." : 'Активна';
|
||
} elseif (in_array($subscription['status'], ['expired', 'cancelled'])) {
|
||
$status = 'expired';
|
||
$message = 'Подписка завершена';
|
||
}
|
||
|
||
return [
|
||
'status' => $status,
|
||
'message' => $message,
|
||
'days_left' => $daysLeft,
|
||
'module' => $moduleInfo,
|
||
'subscription' => $subscription,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Получение цены модуля
|
||
*
|
||
* @param string $moduleCode
|
||
* @param string $period
|
||
* @return int
|
||
*/
|
||
public function getPrice(string $moduleCode, string $period = 'monthly'): int
|
||
{
|
||
return $this->modulesConfig->getPrice($moduleCode, $period);
|
||
}
|
||
|
||
/**
|
||
* Проверка что пользователь может управлять подписками организации
|
||
*
|
||
* @param int|null $organizationId
|
||
* @return bool
|
||
*/
|
||
public function canManageSubscriptions(?int $organizationId = null): bool
|
||
{
|
||
$orgId = $organizationId ?? $this->getActiveOrgId();
|
||
|
||
if (!$orgId) {
|
||
return false;
|
||
}
|
||
|
||
// Проверяем права через AccessService
|
||
return service('access')->canManageModules();
|
||
}
|
||
}
|