Bariki cashback, order edit, and current work in progress
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dda917fc71
commit
45477fb018
94 files changed
+10955
-650
No files matched your search
@@ -14,6 +14,11 @@ import {
|
||||
giveConsent,
|
||||
rateOrder,
|
||||
linkWebRequest,
|
||||
clientPoints,
|
||||
pointsHistory,
|
||||
completeWebRequest,
|
||||
webRequestSlots,
|
||||
type LinkResult,
|
||||
setClientChannel,
|
||||
setLocale,
|
||||
startClient,
|
||||
@@ -41,6 +46,8 @@ const bot = new Bot(token || 'disabled:token');
|
||||
|
||||
/** Ожидаем ввод кода агентства от этих чатов */
|
||||
const awaitingCode = new Set<number>();
|
||||
/** Заявка с сайта без адреса: следующий текст — адрес (chatId → заявка) */
|
||||
const awaitingAddress = new Map<number, string>();
|
||||
/** Низкая оценка: следующий текст — комментарий к ней (chatId → заказ и оценка) */
|
||||
const awaitingRatingComment = new Map<number, { orderId: string; score: number }>();
|
||||
|
||||
@@ -75,9 +82,11 @@ async function askConsent(c: Context, locale: Locale) {
|
||||
await c.reply(t('bot.client.consentText', { url: `${LANDING}/${locale}/privacy` }), { reply_markup: kb, parse_mode: 'HTML', link_preview_options: { is_disabled: true } });
|
||||
}
|
||||
|
||||
async function showMain(c: Context, locale: Locale, gift?: { endsAt: Date } | null) {
|
||||
const planName = (plan: string) => (plan === 'ELITE' ? 'Barev Elite' : 'Barev+');
|
||||
|
||||
async function showMain(c: Context, locale: Locale, gift?: { endsAt: Date; plan: string } | null) {
|
||||
const t = createT(locale);
|
||||
if (gift) await c.reply(t('bot.client.giftActivated', { date: formatDateTime(locale, gift.endsAt, { dateStyle: 'long' }) }));
|
||||
if (gift) await c.reply(t('bot.client.giftActivated', { plan: planName(gift.plan), date: formatDateTime(locale, gift.endsAt, { dateStyle: 'long' }) }));
|
||||
await c.reply(t('bot.client.welcome'), { reply_markup: mainKeyboard(locale) });
|
||||
}
|
||||
|
||||
@@ -87,19 +96,107 @@ async function continueOnboarding(c: Context, locale: Locale) {
|
||||
return showMain(c, locale);
|
||||
}
|
||||
|
||||
// ───── заявка с сайта: доспросить район, адрес и время ─────
|
||||
|
||||
async function webRequestDone(c: Context, locale: Locale, link: LinkResult) {
|
||||
const t = createT(locale);
|
||||
if (link.kind === 'ordered') {
|
||||
const date = formatDateTime(locale, link.scheduledAt, { weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' });
|
||||
await c.reply(t('bot.client.webRequestOrdered', { request: link.requestNumber, order: link.orderNumber, date }));
|
||||
} else if (link.kind === 'linked' || link.kind === 'incomplete') {
|
||||
await c.reply(t('bot.client.webRequestLinked', { request: link.requestNumber }));
|
||||
}
|
||||
}
|
||||
|
||||
/** Следующий вопрос по заявке: сначала район, потом адрес, потом день и время */
|
||||
async function webRequestStep(c: Context, locale: Locale, clientId: string, link: Extract<LinkResult, { kind: 'incomplete' }>, first = false) {
|
||||
const t = createT(locale);
|
||||
const chatId = c.chat!.id;
|
||||
if (first) await c.reply(t('bot.client.wr.intro', { request: link.requestNumber }));
|
||||
const next = link.missing[0];
|
||||
if (next === 'district') {
|
||||
const kb = new InlineKeyboard();
|
||||
const districts = await db.district.findMany({ orderBy: { sortOrder: 'asc' } });
|
||||
districts.forEach((d, i) => {
|
||||
kb.text(t(`districts.${d.code}`), `wrd:${link.requestId}:${d.code}`);
|
||||
if (i % 2 === 1) kb.row();
|
||||
});
|
||||
await c.reply(t('bot.client.wr.askDistrict'), { reply_markup: kb });
|
||||
return;
|
||||
}
|
||||
if (next === 'address') {
|
||||
awaitingAddress.set(chatId, link.requestId);
|
||||
await c.reply(t('bot.client.wr.askAddress'));
|
||||
return;
|
||||
}
|
||||
const days = await webRequestSlots(ctx, { requestId: link.requestId, clientId });
|
||||
if (!days.length) {
|
||||
await c.reply(t('bot.client.wr.noSlots'));
|
||||
return showMain(c, locale);
|
||||
}
|
||||
const kb = new InlineKeyboard();
|
||||
days.slice(0, 8).forEach((d, i) => {
|
||||
kb.text(formatDateTime(locale, d.starts[0]!, { weekday: 'short', day: 'numeric', month: 'short' }), `wrday:${link.requestId}:${d.dateKey}`);
|
||||
if (i % 2 === 1) kb.row();
|
||||
});
|
||||
await c.reply(t('bot.client.wr.askDay'), { reply_markup: kb });
|
||||
}
|
||||
|
||||
async function webRequestAnswer(c: Context, input: { requestId: string; districtCode?: string; addressText?: string; preferredAt?: Date }) {
|
||||
const client = await getClientByTelegramId(db, BigInt(c.from!.id));
|
||||
if (!client) return;
|
||||
const locale = client.user.locale as Locale;
|
||||
try {
|
||||
const res = await completeWebRequest(ctx, { ...input, clientId: client.id });
|
||||
if (res.kind === 'incomplete') return webRequestStep(c, locale, client.id, res);
|
||||
await webRequestDone(c, locale, res);
|
||||
return showMain(c, locale);
|
||||
} catch (e) {
|
||||
if (e instanceof DomainError) return void (await c.reply(createT(locale)(`errors.${e.code}`)));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
bot.callbackQuery(/^wrd:(\w+):(\w+)$/, async (c) => {
|
||||
await c.answerCallbackQuery();
|
||||
await c.editMessageReplyMarkup().catch(() => undefined);
|
||||
await webRequestAnswer(c, { requestId: c.match[1]!, districtCode: c.match[2]! });
|
||||
});
|
||||
|
||||
bot.callbackQuery(/^wrday:(\w+):(\d{4}-\d{2}-\d{2})$/, async (c) => {
|
||||
await c.answerCallbackQuery();
|
||||
const client = await getClientByTelegramId(db, BigInt(c.from.id));
|
||||
if (!client) return;
|
||||
const locale = client.user.locale as Locale;
|
||||
const t = createT(locale);
|
||||
const [day] = await webRequestSlots(ctx, { requestId: c.match[1]!, clientId: client.id, dateKey: c.match[2]! }).catch(() => []);
|
||||
if (!day) return void (await c.reply(t('bot.client.wr.dayFull')));
|
||||
const kb = new InlineKeyboard();
|
||||
day.starts.slice(0, 24).forEach((s, i) => {
|
||||
kb.text(formatDateTime(locale, s, { hour: '2-digit', minute: '2-digit' }), `wrt:${c.match[1]}:${Math.round(s.getTime() / 60_000).toString(36)}`);
|
||||
if (i % 4 === 3) kb.row();
|
||||
});
|
||||
await c.editMessageText(t('bot.client.wr.askTime', { day: formatDateTime(locale, day.starts[0]!, { weekday: 'long', day: 'numeric', month: 'long' }) }), { reply_markup: kb }).catch(() => c.reply(t('bot.client.wr.askTime'), { reply_markup: kb }));
|
||||
});
|
||||
|
||||
bot.callbackQuery(/^wrt:(\w+):([0-9a-z]+)$/, async (c) => {
|
||||
await c.answerCallbackQuery();
|
||||
await c.editMessageReplyMarkup().catch(() => undefined);
|
||||
await webRequestAnswer(c, { requestId: c.match[1]!, preferredAt: new Date(parseInt(c.match[2]!, 36) * 60_000) });
|
||||
});
|
||||
|
||||
bot.command('start', async (c) => {
|
||||
const res = await startClient(ctx, c.from!, c.match || undefined);
|
||||
// пришёл с сайта после заявки: язык и согласие уже есть — сразу говорим, что с заявкой
|
||||
const token = decodeStartPayload(c.match).token;
|
||||
const link = token ? await linkWebRequest(ctx, token, res.clientId) : null;
|
||||
if (link && link.kind !== 'none') {
|
||||
const t = createT(res.locale);
|
||||
if (link.kind === 'ordered') {
|
||||
const date = formatDateTime(res.locale, link.scheduledAt, { weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' });
|
||||
await c.reply(t('bot.client.webRequestOrdered', { request: link.requestNumber, order: link.orderNumber, date }));
|
||||
} else {
|
||||
await c.reply(t('bot.client.webRequestLinked', { request: link.requestNumber }));
|
||||
if (link.kind === 'incomplete') {
|
||||
const gift = link.gift ?? res.gift;
|
||||
if (gift) await c.reply(createT(res.locale)('bot.client.giftActivated', { plan: planName(gift.plan), date: formatDateTime(res.locale, gift.endsAt, { dateStyle: 'long' }) }));
|
||||
return webRequestStep(c, res.locale, res.clientId, link, true);
|
||||
}
|
||||
await webRequestDone(c, res.locale, link);
|
||||
return showMain(c, res.locale, link.gift ?? res.gift);
|
||||
}
|
||||
if (res.needsLocale) {
|
||||
@@ -158,6 +255,41 @@ bot.command('app', async (c) => {
|
||||
|
||||
bot.command('help', async (c) => c.reply(createT(await localeOf(c.from))('bot.client.help')));
|
||||
|
||||
// ───── барики ─────
|
||||
|
||||
const fmtPts = (locale: Locale, n: number) => new Intl.NumberFormat(locale === 'zh' ? 'zh-CN' : locale).format(n);
|
||||
const pointsKb = (locale: Locale) => (APP_URL.startsWith('https://') ? new InlineKeyboard().webApp(createT(locale)('bot.client.points.open'), `${APP_URL}?r=/points`) : undefined);
|
||||
|
||||
bot.command('balance', async (c) => {
|
||||
const client = await getClientByTelegramId(db, BigInt(c.from!.id));
|
||||
const locale = await localeOf(c.from);
|
||||
if (!client) return;
|
||||
const t = createT(locale);
|
||||
const p = await clientPoints(ctx, client.id);
|
||||
const lines = [t('bot.client.points.balance', { balance: fmtPts(locale, p.balance) })];
|
||||
if (p.pending) lines.push(t('bot.client.points.pending', { pending: fmtPts(locale, p.pending), date: p.pendingNextAt ? formatDateTime(locale, p.pendingNextAt, { day: 'numeric', month: 'long' }) : '—' }));
|
||||
if (p.burnAt) lines.push(t('bot.client.points.burn', { date: formatDateTime(locale, p.burnAt, { day: 'numeric', month: 'long' }) }));
|
||||
await c.reply(lines.join('\n'), { reply_markup: pointsKb(locale) });
|
||||
});
|
||||
|
||||
bot.command('history', async (c) => {
|
||||
const client = await getClientByTelegramId(db, BigInt(c.from!.id));
|
||||
const locale = await localeOf(c.from);
|
||||
if (!client) return;
|
||||
const t = createT(locale);
|
||||
const rows = await pointsHistory(db, client.id, { take: 10 });
|
||||
if (!rows.length) return void (await c.reply(t('bot.client.points.empty')));
|
||||
const name = (m: Record<string, string> | null) => (m ? m[locale] || m.ru || '' : '');
|
||||
const text = rows
|
||||
.map((r) => {
|
||||
const sign = r.amount > 0 ? '+' : '−';
|
||||
const what = r.type === 'bonus' ? name(r.bonus) : r.type === 'accrual' ? name(r.category) : t(`bot.client.points.type.${r.type}`);
|
||||
return `${formatDateTime(locale, r.at, { day: '2-digit', month: '2-digit' })} ${sign}${fmtPts(locale, Math.abs(r.amount))} · ${what}${r.orderRef ? ` ${r.orderRef}` : ''} · ${t(`bot.client.points.status.${r.status}`)}`;
|
||||
})
|
||||
.join('\n');
|
||||
await c.reply(text, { reply_markup: pointsKb(locale) });
|
||||
});
|
||||
|
||||
/** Сообщение в чат команды: подписчик пишет консьержу, остальные — поддержке. Ответ придёт сюда же */
|
||||
async function toTeam(c: Context, text: string, file?: ChatFile) {
|
||||
const locale = await localeOf(c.from);
|
||||
@@ -228,6 +360,12 @@ bot.on('message:text', async (c) => {
|
||||
await rateOrder(ctx, { orderId: pending.orderId, clientId: client.id, score: pending.score, comment: c.message.text.slice(0, 1000) }).catch(() => undefined);
|
||||
return toTeam(c, c.message.text);
|
||||
}
|
||||
// адрес для заявки с сайта
|
||||
const requestId = awaitingAddress.get(c.chat.id);
|
||||
if (requestId && client) {
|
||||
awaitingAddress.delete(c.chat.id);
|
||||
return webRequestAnswer(c, { requestId, addressText: c.message.text });
|
||||
}
|
||||
if (!awaitingCode.has(c.chat.id)) return toTeam(c, c.message.text);
|
||||
if (!client) return;
|
||||
try {
|
||||
@@ -256,6 +394,8 @@ async function main() {
|
||||
{ command: 'start', description: t('bot.client.cmdStart') },
|
||||
{ command: 'app', description: t('bot.client.cmdApp') },
|
||||
{ command: 'code', description: t('bot.client.cmdCode') },
|
||||
{ command: 'balance', description: t('bot.client.cmdBalance') },
|
||||
{ command: 'history', description: t('bot.client.cmdHistory') },
|
||||
{ command: 'support', description: t('bot.client.cmdSupport') },
|
||||
{ command: 'concierge', description: t('bot.client.cmdConcierge') },
|
||||
{ command: 'language', description: t('bot.client.cmdLanguage') },
|
||||
|
||||
Reference in new issue
Block a user