add EventManager and Subscription

This commit is contained in:
root
2026-01-13 12:08:40 +03:00
parent 3d39c1ba07
commit 3c24c250e5
6 changed files with 785 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
<?php
namespace App\Services;
use CodeIgniter\Events\Events;
/**
* EventManager - обертка над событиями CodeIgniter 4
*
* Предоставляет два типа подписок на события:
* - moduleOn(): выполняется только если модуль активен и подписка оплачена
* - systemOn(): выполняется всегда, без проверки подписки
*
* Используется для создания интеграций между модулями, которые должны
* работать только при наличии активной подписки на соответствующий модуль.
*/
class EventManager
{
/**
* Сервис подписок на модули
*
* @var ModuleSubscriptionService|null
*/
private ?ModuleSubscriptionService $moduleSubscriptionService = null;
/**
* Конфигурация модулей
*
* @var \Config\BusinessModules|null
*/
private ?\Config\BusinessModules $modulesConfig = null;
/**
* Код модуля, к которому привязаны события
*
* @var string|null
*/
private ?string $moduleCode = null;
/**
* Кэш подписки на модуль
*
* @var bool|null
*/
private ?bool $moduleActive = null;
/**
* Получить экземпляр сервиса подписок
*
* @return ModuleSubscriptionService
*/
private function getModuleSubscriptionService(): ModuleSubscriptionService
{
if ($this->moduleSubscriptionService === null) {
$this->moduleSubscriptionService = service('moduleSubscription');
}
return $this->moduleSubscriptionService;
}
/**
* Получить конфигурацию модулей
*
* @return \Config\BusinessModules
*/
private function getModulesConfig(): \Config\BusinessModules
{
if ($this->modulesConfig === null) {
$this->modulesConfig = new \Config\BusinessModules();
}
return $this->modulesConfig;
}
/**
* Привязать события к конкретному модулю
*
* Все последующие вызовы moduleOn() будут проверять
* подписку на указанный модуль.
*
* @param string $moduleCode Код модуля
* @return $this
*/
public function forModule(string $moduleCode): self
{
$this->moduleCode = $moduleCode;
$this->moduleActive = null;
return $this;
}
/**
* Проверить, активен ли модуль
*
* @return bool
*/
private function isModuleActive(): bool
{
if ($this->moduleCode === null) {
return false;
}
if ($this->moduleActive === null) {
$orgId = session('org_id') ?? null;
$this->moduleActive = $this->getModuleSubscriptionService()
->isModuleActive($this->moduleCode, $orgId);
}
return $this->moduleActive;
}
/**
* Подписаться на событие с проверкой подписки на модуль
*
* Обработчик будет выполнен только если:
* 1. Модуль глобально включен в конфигурации
* 2. У организации есть активная подписка на модуль
*
* @param string $event Имя события
* @param callable $callback Обработчик события
* @param int $priority Приоритет события (по умолчанию 100)
* @return bool True если подписка создана, False если модуль не активен
*/
public function moduleOn(
string $event,
callable $callback,
int $priority = 100
): bool {
if ($this->moduleCode === null) {
throw new \RuntimeException(
'Module code not set. Use forModule() method first.'
);
}
$modulesConfig = $this->getModulesConfig();
// Проверяем, что модуль существует в конфигурации
if (!isset($modulesConfig->modules[$this->moduleCode])) {
log_message(
'error',
"EventManager: Module '{$this->moduleCode}' not found in config"
);
return false;
}
// Если модуль отключен глобально, не подписываемся
if (empty($modulesConfig->modules[$this->moduleCode]['enabled'])) {
log_message(
'info',
"EventManager: Module '{$this->moduleCode}' is disabled globally"
);
return false;
}
// Проверяем подписку организации
if (!$this->isModuleActive()) {
log_message(
'debug',
"EventManager: Organization subscription not active for module '{$this->moduleCode}'"
);
return false;
}
// Подписываемся на событие
Events::on($event, $callback, $priority);
log_message(
'debug',
"EventManager: Subscribed to event '{$event}' for module '{$this->moduleCode}'"
);
return true;
}
/**
* Подписаться на событие без проверки подписки
*
* Обработчик будет выполнен всегда, независимо
* от статуса подписки на модуль.
*
* Используется для системных событий, которые должны
* работать для всех организаций.
*
* @param string $event Имя события
* @param callable $callback Обработчик события
* @param int $priority Приоритет события (по умолчанию 100)
* @return void
*/
public function systemOn(
string $event,
callable $callback,
int $priority = 100
): void {
Events::on($event, $callback, $priority);
log_message(
'debug',
"EventManager: System event subscribed: '{$event}'"
);
}
/**
* Отписаться от события
*
* @param string $event Имя события
* @param callable|null $callback Конкретный обработчик (если null - все обработчики)
* @return void
*/
public function off(string $event, ?callable $callback = null): void
{
if ($callback === null) {
Events::off($event);
} else {
Events::off($event, $callback);
}
}
/**
* Проверить, активен ли текущий модуль
*
* Удобный метод для использования внутри обработчиков событий.
*
* @return bool
*/
public function currentModuleActive(): bool
{
return $this->isModuleActive();
}
/**
* Получить код текущего модуля
*
* @return string|null
*/
public function getCurrentModuleCode(): ?string
{
return $this->moduleCode;
}
}
+394
View File
@@ -0,0 +1,394 @@
<?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();
}
}