bp/app/Services/ModuleSubscriptionService.php

395 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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