zapishis-client/packages/graphql/api/subscriptions.ts

381 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 * 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<typeof GQL.CreateSubscriptionDocument>) {
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<typeof GQL.CreateSubscriptionHistoryDocument>,
) {
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,
subscription_price: trialPrice.documentId,
},
});
return subscriptionData;
}
async getSubscription({
telegramId,
}: Pick<VariablesOf<typeof GQL.GetCustomerDocument>, '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<typeof GQL.GetSubscriptionHistoryDocument>) {
const { query } = await this.getGraphQLClient();
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 this.getGraphQLClient();
const result = await query({
query: GQL.GetSubscriptionPricesDocument,
variables,
});
return result.data;
}
async getSubscriptions(variables?: VariablesOf<typeof GQL.GetSubscriptionsDocument>) {
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<typeof GQL.UpdateSubscriptionDocument>) {
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<typeof GQL.UpdateSubscriptionHistoryDocument>,
) {
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 { customer } = await this._getUser();
const { orders } = await ordersService.getOrders({
filters: {
datetime_end: {
lte: now.endOf('month').toISOString(),
},
datetime_start: {
gte: now.startOf('month').toISOString(),
},
slot: {
master: {
documentId: {
eq: customer.documentId,
},
},
},
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 };
}
}