zapishis-client/packages/graphql/api/subscriptions.ts
2025-09-12 13:20:20 +03:00

351 lines
11 KiB
TypeScript
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.

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<typeof GQL.CreateSubscriptionDocument>) {
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<typeof GQL.CreateSubscriptionHistoryDocument>,
) {
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<typeof GQL.GetSubscriptionDocument>) {
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<typeof GQL.GetSubscriptionHistoryDocument>) {
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<typeof GQL.GetSubscriptionPricesDocument>) {
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<typeof GQL.UpdateSubscriptionDocument>) {
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<typeof GQL.UpdateSubscriptionHistoryDocument>,
) {
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 };
}
}