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'; import minMax from 'dayjs/plugin/minMax'; if (!dayjs.prototype.minMax) { dayjs.extend(minMax); } export const ERRORS = { FAILED_TO_CREATE_TRIAL_SUBSCRIPTION: 'Не удалось оформить доступ к пробному периоду', SUBSCRIPTION_PRICES_NOT_FOUND: 'Цены Pro доступа не найдены', SUBSCRIPTION_SETTING_NOT_FOUND: 'Настройки Pro доступа не найдены', 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 }, paymentId?: string, ) { 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, }); const newExpiresAt = dayjs .max(dayjs(existingSubscription?.expiresAt), dayjs()) .add(subscriptionPrice.days, 'day'); const { customer } = await this.checkIsBanned(); const result = await this.createSubscription({ data: { active: true, customer: customer.documentId, expiresAt: newExpiresAt.toISOString(), }, }); // Добавляем в последнюю подписку ссылку на новую только что созданную if (result?.createSubscription && existingSubscription) await this.updateSubscription({ data: { active: true, nextSubscription: result.createSubscription.documentId, }, documentId: existingSubscription?.documentId, }); await this.createSubscriptionHistory({ data: { amount: subscriptionPrice.amount, currency: 'RUB', description: existingSubscription ? 'Продление Pro доступа' : 'Новая подписка', paymentId, period: subscriptionPrice.period, source: GQL.Enum_Subscriptionhistory_Source.Payment, state: GQL.Enum_Subscriptionhistory_State.Success, subscription: result?.createSubscription?.documentId, subscription_price: subscriptionPrice.documentId, }, }); return { expiresAt: newExpiresAt.toDate(), formattedDate: newExpiresAt.toDate().toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', }), }; } async createSubscription(variables: VariablesOf) { await this.checkIsBanned(); const { mutate } = await this.getGraphQLClient(); 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 this.getGraphQLClient(); 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 hasTrial = await this.hasTrialSubscription(); if (hasTrial) throw new Error(ERRORS.TRIAL_PERIOD_ALREADY_USED); // Получаем цены подписки для определения длительности пробного периода const { subscriptionPrices } = await this.getSubscriptionPrices({ filters: { active: { 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.active) 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: { active: true, customer: customer?.documentId, expiresAt: expiresAt.toISOString(), }, }); 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} дней`, period: trialPrice.period, source: GQL.Enum_Subscriptionhistory_Source.Trial, state: GQL.Enum_Subscriptionhistory_State.Success, subscription: subscription.documentId, }, }); return subscriptionData; } async getSubscription({ telegramId, }: Pick, 'telegramId'>) { await this.checkIsBanned(); const data = await this.getSubscriptions({ filters: { active: { eq: true, }, customer: { telegramId: { eq: telegramId }, }, }, }); const subscription = data.subscriptions.find((x) => !x?.nextSubscription?.documentId); const remainingDays = subscription ? this.getRemainingDays(subscription) : 0; const hasActiveSubscription = subscription?.active && remainingDays > 0; const { maxOrdersPerMonth, remainingOrdersCount } = await this.getRemainingOrdersCount(); return { hasActiveSubscription, maxOrdersPerMonth, remainingDays, remainingOrdersCount, subscription, }; } async getSubscriptionHistory(variables: VariablesOf) { const { query } = await this.getGraphQLClient(); 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 this.getGraphQLClient(); const result = await query({ query: GQL.GetSubscriptionPricesDocument, variables, }); return result.data; } async getSubscriptions(variables?: VariablesOf) { await this.checkIsBanned(); const { query } = await this.getGraphQLClient(); const result = await query({ query: GQL.GetSubscriptionsDocument, variables, }); return result.data; } async getSubscriptionSettings() { await this.checkIsBanned(); const { query } = await this.getGraphQLClient(); const result = await query({ query: GQL.GetSubscriptionSettingsDocument, }); return result.data; } async hasTrialSubscription() { const { customer } = await this._getUser(); const { subscriptionHistories } = await this.getSubscriptionHistory({ filters: { or: [ { source: { eq: GQL.Enum_Subscriptionhistory_Source.Trial, }, }, { period: { eq: GQL.Enum_Subscriptionprice_Period.Trial, }, }, ], subscription: { customer: { documentId: { eq: customer?.documentId, }, }, }, }, }); return subscriptionHistories?.some( (history) => history?.state === GQL.Enum_Subscriptionhistory_State.Success, ); } async updateSubscription(variables: VariablesOf) { await this.checkIsBanned(); const { mutate } = await this.getGraphQLClient(); 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 this.getGraphQLClient(); 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 getRemainingDays(subscription: GQL.SubscriptionFieldsFragment) { if (!subscription) return 0; const remainingDays = dayjs(subscription?.expiresAt).diff(dayjs(), 'day', true); if (remainingDays <= 0) return 0; return Math.ceil(remainingDays); } 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 }; } }