import { getClientWithToken } from '../apollo/client'; import * as GQL from '../types'; import { BaseService } from './base'; import { OrdersService } from './orders'; import { type VariablesOf } from '@graphql-typed-document-node/core'; import dayjs from 'dayjs'; export const ERRORS = { FAILED_TO_CREATE_TRIAL_SUBSCRIPTION: 'Не удалось создать пробную подписку', SUBSCRIPTION_PRICES_NOT_FOUND: 'Цены подписки не найдены', SUBSCRIPTION_SETTING_NOT_FOUND: 'Настройки подписки не найдены', TRIAL_PERIOD_ALREADY_USED: 'Пробный период уже был использован', TRIAL_PERIOD_NOT_ACTIVE: 'Пробный период неактивен', TRIAL_PERIOD_NOT_FOUND: 'Пробный период не найден', }; export class SubscriptionsService extends BaseService { async createOrUpdateSubscription(payload: { period: GQL.Enum_Subscriptionprice_Period }) { // ищем цену по выбранному периоду const { subscriptionPrices } = await this.getSubscriptionPrices({ filters: { period: { eq: payload.period } }, }); const subscriptionPrice = subscriptionPrices[0]; if (!subscriptionPrice) throw new Error('Subscription price not found'); // получаем текущую подписку const { subscription: existingSubscription } = await this.getSubscription({ telegramId: this._user.telegramId, }); let expiresAt: string; if (existingSubscription?.expiresAt) { // --- продлеваем подписку --- expiresAt = dayjs(existingSubscription.expiresAt) .add(subscriptionPrice.days, 'day') .toISOString(); const result = await this.updateSubscription({ data: { expiresAt }, documentId: existingSubscription.documentId, }); // создаём запись в истории await this.createSubscriptionHistory({ data: { amount: subscriptionPrice.amount, currency: 'RUB', description: subscriptionPrice.description ?? 'Продление подписки', endDate: expiresAt, period: subscriptionPrice.period, source: GQL.Enum_Subscriptionhistory_Source.Renewal, startDate: existingSubscription.expiresAt, state: GQL.Enum_Subscriptionhistory_State.Success, subscription: result?.updateSubscription?.documentId, }, }); } else { // --- создаём новую подписку --- const { customer } = await this.checkIsBanned(); if (!customer?.documentId) throw new Error('Customer not found'); const expiresAtNew = dayjs().add(subscriptionPrice.days, 'day').toISOString(); const result = await this.createSubscription({ data: { autoRenew: true, customer: customer.documentId, expiresAt: expiresAtNew, isActive: true, }, }); expiresAt = result?.createSubscription?.expiresAt ?? expiresAtNew; // создаём запись в истории await this.createSubscriptionHistory({ data: { amount: subscriptionPrice.amount, currency: 'RUB', description: subscriptionPrice.description ?? 'Новая подписка', endDate: expiresAt, period: subscriptionPrice.period, source: GQL.Enum_Subscriptionhistory_Source.Payment, startDate: dayjs().toISOString(), state: GQL.Enum_Subscriptionhistory_State.Success, subscription: result?.createSubscription?.documentId, }, }); } return { expiresAt, formattedDate: dayjs(expiresAt).toDate().toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', }), }; } async createSubscription(variables: VariablesOf) { await this.checkIsBanned(); const { mutate } = await getClientWithToken(); const mutationResult = await mutate({ mutation: GQL.CreateSubscriptionDocument, variables, }); const error = mutationResult.errors?.at(0); if (error) throw new Error(error.message); return mutationResult.data; } async createSubscriptionHistory( variables: VariablesOf, ) { await this.checkIsBanned(); const { mutate } = await getClientWithToken(); const mutationResult = await mutate({ mutation: GQL.CreateSubscriptionHistoryDocument, variables, }); const error = mutationResult.errors?.at(0); if (error) throw new Error(error.message); return mutationResult.data; } async createTrialSubscription() { // Получаем пользователя и проверяем бан const { customer } = await this.checkIsBanned(); // Проверяем, не использовал ли пользователь уже пробный период const hasUserTrial = await this.hasUserTrialSubscription(); if (hasUserTrial) throw new Error(ERRORS.TRIAL_PERIOD_ALREADY_USED); // Получаем цены подписки для определения длительности пробного периода const { subscriptionPrices } = await this.getSubscriptionPrices({ filters: { isActive: { eq: true, }, }, }); if (!subscriptionPrices) throw new Error(ERRORS.SUBSCRIPTION_PRICES_NOT_FOUND); // Ищем пробный период const trialPrice = subscriptionPrices.find( (price) => price?.period === GQL.Enum_Subscriptionprice_Period.Trial, ); if (!trialPrice) throw new Error(ERRORS.TRIAL_PERIOD_NOT_FOUND); if (!trialPrice.isActive) throw new Error(ERRORS.TRIAL_PERIOD_NOT_ACTIVE); const trialPeriodDays = trialPrice?.days; const now = dayjs(); const expiresAt = now.add(trialPeriodDays, 'day'); // Создаем пробную подписку const subscriptionData = await this.createSubscription({ data: { autoRenew: false, customer: customer?.documentId, expiresAt: expiresAt.toISOString(), isActive: true, }, }); if (!subscriptionData?.createSubscription) { throw new Error(ERRORS.FAILED_TO_CREATE_TRIAL_SUBSCRIPTION); } const subscription = subscriptionData.createSubscription; // Создаем запись в истории подписки await this.createSubscriptionHistory({ data: { amount: 0, currency: 'RUB', description: `Пробный период на ${trialPeriodDays} дней`, endDate: expiresAt.toISOString(), period: GQL.Enum_Subscriptionhistory_Period.Trial, source: GQL.Enum_Subscriptionhistory_Source.Trial, startDate: now.toISOString(), state: GQL.Enum_Subscriptionhistory_State.Success, subscription: subscription.documentId, }, }); return subscriptionData; } async getSubscription(variables: VariablesOf) { await this.checkIsBanned(); const { query } = await getClientWithToken(); const result = await query({ query: GQL.GetSubscriptionDocument, variables, }); const subscription = result.data.subscriptions.at(0); const hasActiveSubscription = Boolean( subscription?.isActive && subscription?.expiresAt && new Date() < new Date(subscription.expiresAt), ); const { maxOrdersPerMonth, remainingOrdersCount } = await this.getRemainingOrdersCount(); return { hasActiveSubscription, maxOrdersPerMonth, remainingOrdersCount, subscription }; } async getSubscriptionHistory(variables: VariablesOf) { await this.checkIsBanned(); const { query } = await getClientWithToken(); const result = await query({ query: GQL.GetSubscriptionHistoryDocument, variables, }); const subscriptionHistories = result.data.subscriptionHistories; return { subscriptionHistories }; } async getSubscriptionPrices(variables?: VariablesOf) { await this.checkIsBanned(); const { query } = await getClientWithToken(); const result = await query({ query: GQL.GetSubscriptionPricesDocument, variables, }); return result.data; } async getSubscriptionSettings() { await this.checkIsBanned(); const { query } = await getClientWithToken(); const result = await query({ query: GQL.GetSubscriptionSettingsDocument, }); return result.data; } async hasUserTrialSubscription() { const { customer } = await this.checkIsBanned(); const { subscription: existingSubscription } = await this.getSubscription({ telegramId: customer?.telegramId, }); if (!existingSubscription) return false; const { subscriptionHistories } = await this.getSubscriptionHistory({ filters: { period: { eq: GQL.Enum_Subscriptionhistory_Period.Trial }, subscription: { documentId: { eq: existingSubscription.documentId } }, }, }); return subscriptionHistories?.some( (history) => history?.state === GQL.Enum_Subscriptionhistory_State.Success, ); } async updateSubscription(variables: VariablesOf) { await this.checkIsBanned(); const { mutate } = await getClientWithToken(); const mutationResult = await mutate({ mutation: GQL.UpdateSubscriptionDocument, variables, }); const error = mutationResult.errors?.at(0); if (error) throw new Error(error.message); return mutationResult.data; } async updateSubscriptionHistory( variables: VariablesOf, ) { await this.checkIsBanned(); const { mutate } = await getClientWithToken(); const mutationResult = await mutate({ mutation: GQL.UpdateSubscriptionHistoryDocument, variables, }); const error = mutationResult.errors?.at(0); if (error) throw new Error(error.message); return mutationResult.data; } private async getRemainingOrdersCount() { const ordersService = new OrdersService(this._user); const now = dayjs(); const { orders } = await ordersService.getOrders({ filters: { datetime_end: { lte: now.endOf('month').toISOString(), }, datetime_start: { gte: now.startOf('month').toISOString(), }, state: { eq: GQL.Enum_Order_State.Completed, }, }, }); const { subscriptionSetting } = await this.getSubscriptionSettings(); if (!subscriptionSetting) throw new Error(ERRORS.SUBSCRIPTION_SETTING_NOT_FOUND); const { maxOrdersPerMonth } = subscriptionSetting; let remainingOrdersCount = maxOrdersPerMonth - (orders?.length ?? 0); if (remainingOrdersCount < 0) remainingOrdersCount = 0; return { maxOrdersPerMonth, remainingOrdersCount }; } }