test payment
This commit is contained in:
parent
6228832aff
commit
eab6da5e89
@ -30,12 +30,14 @@ sharebot =
|
|||||||
.description = Поделиться ботом
|
.description = Поделиться ботом
|
||||||
help =
|
help =
|
||||||
.description = Список команд и поддержка
|
.description = Список команд и поддержка
|
||||||
|
subscribe =
|
||||||
|
.description = Оформить подписку
|
||||||
|
|
||||||
commands-list =
|
commands-list =
|
||||||
📋 Доступные команды:
|
📋 Доступные команды:
|
||||||
• /addcontact — добавить контакт клиента
|
• /addcontact — добавить контакт клиента
|
||||||
• /becomemaster — стать мастером
|
|
||||||
• /sharebot — поделиться ботом
|
• /sharebot — поделиться ботом
|
||||||
|
• /subscribe — оформить подписку
|
||||||
• /help — список команд
|
• /help — список команд
|
||||||
|
|
||||||
Откройте приложение кнопкой "Открыть", чтобы отредактировать свой профиль или создать запись
|
Откройте приложение кнопкой "Открыть", чтобы отредактировать свой профиль или создать запись
|
||||||
@ -75,9 +77,7 @@ msg-already-registered =
|
|||||||
msg-invalid-phone = ❌ Некорректный номер телефона
|
msg-invalid-phone = ❌ Некорректный номер телефона
|
||||||
|
|
||||||
# Сообщения о контактах
|
# Сообщения о контактах
|
||||||
msg-send-client-contact =
|
msg-send-client-contact = 👤 Отправьте контакт клиента, которого вы хотите добавить.
|
||||||
👤 Отправьте контакт клиента, которого вы хотите добавить.
|
|
||||||
Для отмены операции используйте команду /cancel
|
|
||||||
|
|
||||||
msg-send-contact = Пожалуйста, отправьте контакт клиента через кнопку Telegram
|
msg-send-contact = Пожалуйста, отправьте контакт клиента через кнопку Telegram
|
||||||
|
|
||||||
@ -102,4 +102,19 @@ err-generic = ⚠️ Что-то пошло не так. Попробуйте е
|
|||||||
err-banned = 🚫 Ваш аккаунт заблокирован
|
err-banned = 🚫 Ваш аккаунт заблокирован
|
||||||
err-with-details = ❌ Произошла ошибка
|
err-with-details = ❌ Произошла ошибка
|
||||||
{ $error }
|
{ $error }
|
||||||
err-limit-exceeded = 🚫 Слишком много запросов! Подождите немного
|
err-limit-exceeded = 🚫 Слишком много запросов! Подождите немного
|
||||||
|
|
||||||
|
msg-cancel-operation = Для отмены операции используйте команду /cancel
|
||||||
|
|
||||||
|
# Сообщения о подписке
|
||||||
|
msg-subscribe =
|
||||||
|
👑 Подписка Pro:
|
||||||
|
• Разблокирует неограниченное количество заказов
|
||||||
|
|
||||||
|
msg-subscribe-success =
|
||||||
|
✅ Платеж успешно обработан!
|
||||||
|
|
||||||
|
msg-subscribe-error =
|
||||||
|
❌ Произошла ошибка при обработке платежа
|
||||||
|
|
||||||
|
msg-subscribe-active-until = 📅 Ваша подписка активна до { $date }
|
||||||
@ -25,10 +25,12 @@
|
|||||||
"@repo/graphql": "workspace:*",
|
"@repo/graphql": "workspace:*",
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@repo/typescript-config": "workspace:*",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
|
"dayjs": "catalog:",
|
||||||
"grammy": "^1.38.1",
|
"grammy": "^1.38.1",
|
||||||
"ioredis": "^5.7.0",
|
"ioredis": "^5.7.0",
|
||||||
"pino": "^9.9.0",
|
"pino": "^9.9.0",
|
||||||
"pino-pretty": "^13.1.1",
|
"pino-pretty": "^13.1.1",
|
||||||
|
"radashi": "catalog:",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable id-length */
|
/* eslint-disable id-length */
|
||||||
import { type Context } from '@/bot/context';
|
import { type Context } from '@/bot/context';
|
||||||
import { KEYBOARD_REMOVE, KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
|
import { KEYBOARD_REMOVE, KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
|
||||||
|
import { combine } from '@/utils/messages';
|
||||||
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
|
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
|
||||||
import { type Conversation } from '@grammyjs/conversations';
|
import { type Conversation } from '@grammyjs/conversations';
|
||||||
import { CustomersService } from '@repo/graphql/api/customers';
|
import { CustomersService } from '@repo/graphql/api/customers';
|
||||||
@ -24,7 +25,11 @@ export async function addContact(conversation: Conversation<Context, Context>, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Просим отправить контакт клиента
|
// Просим отправить контакт клиента
|
||||||
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact')));
|
await ctx.reply(
|
||||||
|
await conversation.external(({ t }) =>
|
||||||
|
combine(t('msg-send-client-contact'), t('msg-cancel-operation')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Ждем любое сообщение от пользователя
|
// Ждем любое сообщение от пользователя
|
||||||
const waitCtx = await conversation.wait();
|
const waitCtx = await conversation.wait();
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from './add-contact';
|
export * from './add-contact';
|
||||||
|
export * from './subscription';
|
||||||
|
|||||||
101
apps/bot/src/bot/conversations/subscription.ts
Normal file
101
apps/bot/src/bot/conversations/subscription.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable id-length */
|
||||||
|
import { type Context } from '@/bot/context';
|
||||||
|
import { env } from '@/config/env';
|
||||||
|
import { formatMoney } from '@/utils/format';
|
||||||
|
import { combine } from '@/utils/messages';
|
||||||
|
import { type Conversation } from '@grammyjs/conversations';
|
||||||
|
import { fmt, i } from '@grammyjs/parse-mode';
|
||||||
|
import { SubscriptionsService } from '@repo/graphql/api/subscriptions';
|
||||||
|
import * as GQL from '@repo/graphql/types';
|
||||||
|
import { InlineKeyboard } from 'grammy';
|
||||||
|
import { sift } from 'radashi';
|
||||||
|
|
||||||
|
export async function subscription(conversation: Conversation<Context, Context>, ctx: Context) {
|
||||||
|
const telegramId = ctx.from?.id;
|
||||||
|
if (!telegramId) {
|
||||||
|
return replyError(ctx, conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionsService = new SubscriptionsService({ telegramId });
|
||||||
|
|
||||||
|
const hasUserTrial = await subscriptionsService.hasUserTrialSubscription();
|
||||||
|
|
||||||
|
const { subscriptionPrices } = await subscriptionsService.getSubscriptionPrices({
|
||||||
|
filters: {
|
||||||
|
isActive: {
|
||||||
|
eq: true,
|
||||||
|
},
|
||||||
|
period: {
|
||||||
|
ne: hasUserTrial ? GQL.Enum_Subscriptionprice_Period.Trial : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prices = sift(subscriptionPrices);
|
||||||
|
|
||||||
|
// строим клавиатуру
|
||||||
|
const keyboard = buildPricesKeyboard(prices);
|
||||||
|
|
||||||
|
// сообщение с выбором плана
|
||||||
|
const messageWithPrices = await ctx.reply(
|
||||||
|
combine(
|
||||||
|
await conversation.external(({ t }) =>
|
||||||
|
combine(t('msg-subscribe'), fmt`${i}${t('msg-cancel-operation')}${i}`.text),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
{ reply_markup: keyboard },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ждём выбора
|
||||||
|
const selectPlanWaitCtx = await conversation.wait();
|
||||||
|
|
||||||
|
// удаляем сообщение с выбором (не обязательно, но красивее)
|
||||||
|
try {
|
||||||
|
await ctx.api.deleteMessage(telegramId, messageWithPrices.message_id);
|
||||||
|
} catch {
|
||||||
|
/* игнорируем, если не удалось удалить */
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPeriod = selectPlanWaitCtx.callbackQuery?.data;
|
||||||
|
if (!selectedPeriod) return replyError(ctx, conversation);
|
||||||
|
|
||||||
|
const selectedPrice = prices.find((price) => price?.period === selectedPeriod);
|
||||||
|
if (!selectedPrice) return replyError(ctx, conversation);
|
||||||
|
|
||||||
|
// создаём invoice
|
||||||
|
return ctx.replyWithInvoice(
|
||||||
|
'Оплата подписки',
|
||||||
|
selectedPrice.description || '',
|
||||||
|
JSON.stringify({ period: selectedPrice.period }),
|
||||||
|
'RUB',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
amount: selectedPrice.amount * 100, // Telegram ждёт в копейках
|
||||||
|
label: selectedPrice.description || 'К оплате',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
provider_token: env.BOT_PROVIDER_TOKEN,
|
||||||
|
start_parameter: 'get_access',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
function buildPricesKeyboard(prices: GQL.SubscriptionPriceFieldsFragment[]) {
|
||||||
|
const keyboard = new InlineKeyboard();
|
||||||
|
for (const price of prices) {
|
||||||
|
keyboard.row({
|
||||||
|
callback_data: price.period,
|
||||||
|
pay: true,
|
||||||
|
text: `${price.description} (${formatMoney(price.amount)})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replyError(ctx: Context, conversation: Conversation<Context, Context>) {
|
||||||
|
return ctx.reply(await conversation.external(({ t }) => t('err-generic')));
|
||||||
|
}
|
||||||
@ -2,4 +2,5 @@ export * from './add-contact';
|
|||||||
export * from './help';
|
export * from './help';
|
||||||
export * from './registration';
|
export * from './registration';
|
||||||
export * from './share-bot';
|
export * from './share-bot';
|
||||||
|
export * from './subscription';
|
||||||
export * from './welcome';
|
export * from './welcome';
|
||||||
|
|||||||
44
apps/bot/src/bot/features/subscription.ts
Normal file
44
apps/bot/src/bot/features/subscription.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { type Context } from '@/bot/context';
|
||||||
|
import { logHandle } from '@/bot/helpers/logging';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
|
import { SubscriptionsService } from '@repo/graphql/api/subscriptions';
|
||||||
|
import { Composer } from 'grammy';
|
||||||
|
|
||||||
|
const composer = new Composer<Context>();
|
||||||
|
|
||||||
|
// Telegram требует отвечать на pre_checkout_query
|
||||||
|
composer.on('pre_checkout_query', logHandle('pre-checkout-query'), async (ctx) => {
|
||||||
|
await ctx.answerPreCheckoutQuery(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const feature = composer.chatType('private');
|
||||||
|
|
||||||
|
// команда для входа в flow подписки
|
||||||
|
feature.command('subscribe', logHandle('command-subscribe'), async (ctx) => {
|
||||||
|
await ctx.conversation.enter('subscription');
|
||||||
|
});
|
||||||
|
|
||||||
|
// успешная оплата
|
||||||
|
feature.on(':successful_payment', logHandle('successful-payment'), async (ctx) => {
|
||||||
|
const telegramId = ctx.from.id;
|
||||||
|
const subscriptionsService = new SubscriptionsService({ telegramId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawPayload = ctx.message?.successful_payment.invoice_payload;
|
||||||
|
if (!rawPayload) throw new Error('Missing invoice payload');
|
||||||
|
|
||||||
|
const payload = JSON.parse(rawPayload);
|
||||||
|
|
||||||
|
const { formattedDate } = await subscriptionsService.createOrUpdateSubscription(payload);
|
||||||
|
|
||||||
|
await ctx.reply(ctx.t('msg-subscribe-success'));
|
||||||
|
await ctx.reply(ctx.t('msg-subscribe-active-until', { date: formattedDate }));
|
||||||
|
} catch (error) {
|
||||||
|
await ctx.reply(ctx.t('msg-subscribe-error'));
|
||||||
|
logger.error(
|
||||||
|
'Failed to process subscription after successful payment\n' + (error as Error)?.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { composer as subscription };
|
||||||
@ -5,7 +5,7 @@ import { type LanguageCode } from '@grammyjs/types';
|
|||||||
import { type Api, type Bot, type RawApi } from 'grammy';
|
import { type Api, type Bot, type RawApi } from 'grammy';
|
||||||
|
|
||||||
export async function setCommands({ api }: Bot<Context, Api<RawApi>>) {
|
export async function setCommands({ api }: Bot<Context, Api<RawApi>>) {
|
||||||
const commands = createCommands(['start', 'addcontact', 'sharebot', 'help']);
|
const commands = createCommands(['start', 'addcontact', 'sharebot', 'help', 'subscribe']);
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
addLocalizations(command);
|
addLocalizations(command);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const envSchema = z.object({
|
export const envSchema = z.object({
|
||||||
|
BOT_PROVIDER_TOKEN: z.string(),
|
||||||
BOT_TOKEN: z.string(),
|
BOT_TOKEN: z.string(),
|
||||||
BOT_URL: z.string(),
|
BOT_URL: z.string(),
|
||||||
RATE_LIMIT: z
|
RATE_LIMIT: z
|
||||||
|
|||||||
4
apps/bot/src/utils/format.ts
Normal file
4
apps/bot/src/utils/format.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const formatMoney = Intl.NumberFormat('ru-RU', {
|
||||||
|
currency: 'RUB',
|
||||||
|
style: 'currency',
|
||||||
|
}).format;
|
||||||
@ -91,7 +91,7 @@ export default async function ProPage() {
|
|||||||
<Users className="size-5 text-purple-600 dark:text-purple-400" />
|
<Users className="size-5 text-purple-600 dark:text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-left text-base leading-relaxed text-gray-700 dark:text-gray-300">
|
<p className="text-left text-base leading-relaxed text-gray-700 dark:text-gray-300">
|
||||||
Ваш профиль доступен всем пользователям
|
Ваш профиль доступен всем пользователям в поиске
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ export default async function ProPage() {
|
|||||||
<Star className="size-5 text-purple-600 dark:text-purple-400" />
|
<Star className="size-5 text-purple-600 dark:text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-left text-base leading-relaxed text-gray-700 dark:text-gray-300">
|
<p className="text-left text-base leading-relaxed text-gray-700 dark:text-gray-300">
|
||||||
Профиль и аватар выделяются специальным цветом
|
Профиль и аватар выделяются цветом
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,6 +15,91 @@ export const ERRORS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class SubscriptionsService extends BaseService {
|
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>) {
|
async createSubscription(variables: VariablesOf<typeof GQL.CreateSubscriptionDocument>) {
|
||||||
await this.checkIsBanned();
|
await this.checkIsBanned();
|
||||||
|
|
||||||
@ -54,23 +139,17 @@ export class SubscriptionsService extends BaseService {
|
|||||||
const { customer } = await this.checkIsBanned();
|
const { customer } = await this.checkIsBanned();
|
||||||
|
|
||||||
// Проверяем, не использовал ли пользователь уже пробный период
|
// Проверяем, не использовал ли пользователь уже пробный период
|
||||||
const { subscription: existingSubscription } = await this.getSubscription({
|
const hasUserTrial = await this.hasUserTrialSubscription();
|
||||||
telegramId: customer?.telegramId,
|
if (hasUserTrial) throw new Error(ERRORS.TRIAL_PERIOD_ALREADY_USED);
|
||||||
});
|
|
||||||
if (existingSubscription) {
|
|
||||||
// Проверяем, есть ли в истории успешная пробная подписка
|
|
||||||
const hasUsedTrial = existingSubscription.subscriptionHistories?.some(
|
|
||||||
(item) =>
|
|
||||||
item?.period === GQL.Enum_Subscriptionhistory_Period.Trial &&
|
|
||||||
item?.state === GQL.Enum_Subscriptionhistory_State.Success,
|
|
||||||
);
|
|
||||||
if (hasUsedTrial) {
|
|
||||||
throw new Error(ERRORS.TRIAL_PERIOD_ALREADY_USED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем цены подписки для определения длительности пробного периода
|
// Получаем цены подписки для определения длительности пробного периода
|
||||||
const { subscriptionPrices } = await this.getSubscriptionPrices({ isActive: true });
|
const { subscriptionPrices } = await this.getSubscriptionPrices({
|
||||||
|
filters: {
|
||||||
|
isActive: {
|
||||||
|
eq: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
if (!subscriptionPrices) throw new Error(ERRORS.SUBSCRIPTION_PRICES_NOT_FOUND);
|
if (!subscriptionPrices) throw new Error(ERRORS.SUBSCRIPTION_PRICES_NOT_FOUND);
|
||||||
|
|
||||||
// Ищем пробный период
|
// Ищем пробный период
|
||||||
@ -86,7 +165,7 @@ export class SubscriptionsService extends BaseService {
|
|||||||
|
|
||||||
// Создаем пробную подписку
|
// Создаем пробную подписку
|
||||||
const subscriptionData = await this.createSubscription({
|
const subscriptionData = await this.createSubscription({
|
||||||
input: {
|
data: {
|
||||||
autoRenew: false,
|
autoRenew: false,
|
||||||
customer: customer?.documentId,
|
customer: customer?.documentId,
|
||||||
expiresAt: expiresAt.toISOString(),
|
expiresAt: expiresAt.toISOString(),
|
||||||
@ -102,13 +181,13 @@ export class SubscriptionsService extends BaseService {
|
|||||||
|
|
||||||
// Создаем запись в истории подписки
|
// Создаем запись в истории подписки
|
||||||
await this.createSubscriptionHistory({
|
await this.createSubscriptionHistory({
|
||||||
input: {
|
data: {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
description: `Пробный период на ${trialPeriodDays} дней`,
|
description: `Пробный период на ${trialPeriodDays} дней`,
|
||||||
endDate: expiresAt.toISOString(),
|
endDate: expiresAt.toISOString(),
|
||||||
period: GQL.Enum_Subscriptionhistory_Period.Trial,
|
period: GQL.Enum_Subscriptionhistory_Period.Trial,
|
||||||
source: GQL.Enum_Subscriptionhistory_Source.Promo,
|
source: GQL.Enum_Subscriptionhistory_Source.Trial,
|
||||||
startDate: now.toISOString(),
|
startDate: now.toISOString(),
|
||||||
state: GQL.Enum_Subscriptionhistory_State.Success,
|
state: GQL.Enum_Subscriptionhistory_State.Success,
|
||||||
subscription: subscription.documentId,
|
subscription: subscription.documentId,
|
||||||
@ -181,6 +260,27 @@ export class SubscriptionsService extends BaseService {
|
|||||||
return result.data;
|
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>) {
|
async updateSubscription(variables: VariablesOf<typeof GQL.UpdateSubscriptionDocument>) {
|
||||||
await this.checkIsBanned();
|
await this.checkIsBanned();
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,6 @@ fragment SubscriptionFields on Subscription {
|
|||||||
isActive
|
isActive
|
||||||
expiresAt
|
expiresAt
|
||||||
autoRenew
|
autoRenew
|
||||||
customer {
|
|
||||||
...CustomerFields
|
|
||||||
}
|
|
||||||
subscriptionHistories {
|
|
||||||
...SubscriptionHistoryFields
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment SubscriptionHistoryFields on SubscriptionHistory {
|
fragment SubscriptionHistoryFields on SubscriptionHistory {
|
||||||
@ -34,7 +28,7 @@ fragment SubscriptionPriceFields on SubscriptionPrice {
|
|||||||
documentId
|
documentId
|
||||||
period
|
period
|
||||||
days
|
days
|
||||||
price
|
amount
|
||||||
currency
|
currency
|
||||||
isActive
|
isActive
|
||||||
description
|
description
|
||||||
@ -46,9 +40,6 @@ fragment SubscriptionRewardFields on SubscriptionReward {
|
|||||||
expiresAt
|
expiresAt
|
||||||
activated
|
activated
|
||||||
description
|
description
|
||||||
subscription_history {
|
|
||||||
...SubscriptionHistoryFields
|
|
||||||
}
|
|
||||||
owner {
|
owner {
|
||||||
...CustomerFields
|
...CustomerFields
|
||||||
}
|
}
|
||||||
@ -69,23 +60,20 @@ query getSubscriptionSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query GetSubscriptionPrices($isActive: Boolean) {
|
query GetSubscriptionPrices($filters: SubscriptionPriceFiltersInput) {
|
||||||
subscriptionPrices(filters: { isActive: { eq: $isActive } }, sort: "price:asc") {
|
subscriptionPrices(filters: $filters, sort: "amount:asc") {
|
||||||
...SubscriptionPriceFields
|
...SubscriptionPriceFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query GetSubscriptionHistory($subscriptionId: ID!) {
|
query GetSubscriptionHistory($filters: SubscriptionHistoryFiltersInput) {
|
||||||
subscriptionHistories(
|
subscriptionHistories(filters: $filters) {
|
||||||
filters: { subscription: { documentId: { eq: $subscriptionId } } }
|
|
||||||
sort: "startDate:desc"
|
|
||||||
) {
|
|
||||||
...SubscriptionHistoryFields
|
...SubscriptionHistoryFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutation CreateSubscription($input: SubscriptionInput!) {
|
mutation CreateSubscription($data: SubscriptionInput!) {
|
||||||
createSubscription(data: $input) {
|
createSubscription(data: $data) {
|
||||||
...SubscriptionFields
|
...SubscriptionFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,8 +84,8 @@ mutation UpdateSubscription($documentId: ID!, $data: SubscriptionInput!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutation CreateSubscriptionHistory($input: SubscriptionHistoryInput!) {
|
mutation CreateSubscriptionHistory($data: SubscriptionHistoryInput!) {
|
||||||
createSubscriptionHistory(data: $input) {
|
createSubscriptionHistory(data: $data) {
|
||||||
...SubscriptionHistoryFields
|
...SubscriptionHistoryFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -141,6 +141,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 20.19.4
|
version: 20.19.4
|
||||||
|
dayjs:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.11.13
|
||||||
grammy:
|
grammy:
|
||||||
specifier: ^1.38.1
|
specifier: ^1.38.1
|
||||||
version: 1.38.1
|
version: 1.38.1
|
||||||
@ -153,6 +156,9 @@ importers:
|
|||||||
pino-pretty:
|
pino-pretty:
|
||||||
specifier: ^13.1.1
|
specifier: ^13.1.1
|
||||||
version: 13.1.1
|
version: 13.1.1
|
||||||
|
radashi:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 12.5.1
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.5.0
|
specifier: ^8.5.0
|
||||||
version: 8.5.0(jiti@2.4.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.0)
|
version: 8.5.0(jiti@2.4.1)(postcss@8.5.6)(tsx@4.19.2)(typescript@5.8.3)(yaml@2.8.0)
|
||||||
|
|||||||
@ -12,7 +12,8 @@
|
|||||||
"LOGIN_GRAPHQL",
|
"LOGIN_GRAPHQL",
|
||||||
"BOT_TOKEN",
|
"BOT_TOKEN",
|
||||||
"NEXTAUTH_SECRET",
|
"NEXTAUTH_SECRET",
|
||||||
"BOT_URL"
|
"BOT_URL",
|
||||||
|
"BOT_PROVIDER_TOKEN"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user