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:
zabudko-ruandClaude Opus 5.5 committed 2026-10-25 22:57:03 +04:00
1 parent dda917fc71
commit 45477fb018
94 files changed
+10955 -650

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
/**
* Локальная разработка: демо партнёра услуг (клининг) — заказы у партнёра, сотрудники, доход,
* и одноразовая ссылка на кабинет менеджера в браузере.
* pnpm --filter @barev/api dev:partners-demo
* Клиенты демо — telegramId 100003000+. Повторный запуск пересоздаёт их заказы.
*/
import { fromYerevan, toYerevan } from '@barev/core';
import { getDb } from '@barev/db';
import { createOrder, createProWebLogin, dispatcherAssign, giveConsent, markPaid, offerOrder, startClient, transitionOrder, type Ctx } from '@barev/services';
import { env } from '../src/env.ts';
if (env.isProd) throw new Error('Только для разработки');
const db = getDb();
const ctx: Ctx = { db, notify: { emit: async () => {} }, now: () => new Date() };
const TG_FROM = 100003000n;
const PRO_APP = process.env.PRO_WEB_URL ?? 'http://localhost:5187';
// ── чистим прошлое демо ──
const old = await db.user.findMany({ where: { telegramId: { gte: TG_FROM, lt: TG_FROM + 100n } }, include: { client: true } });
const oldClients = old.map((u) => u.client?.id).filter((x): x is string => !!x);
const oldOrders = (await db.order.findMany({ where: { clientId: { in: oldClients } }, select: { id: true } })).map((o) => o.id);
await db.partnerEarning.deleteMany({ where: { orderId: { in: oldOrders } } });
await db.agencyCommission.deleteMany({ where: { orderId: { in: oldOrders } } });
await db.order.deleteMany({ where: { id: { in: oldOrders } } });
let company = await db.providerCompany.findFirst({ where: { active: true, roles: { has: 'CLEANER' } } });
if (!company) throw new Error('Нет компании-партнёра — запустите seed');
company = await db.providerCompany.update({ where: { id: company.id }, data: { deal: 'PROFIT_SHARE', profitSharePct: 40, fotPct: null, taxPct: null } });
const manager = await db.companyDispatcher.findFirstOrThrow({ where: { companyId: company.id } });
const staff = await db.provider.findMany({ where: { companyId: company.id, active: true } });
// график всем сотрудникам: каждый день 9–20, лимит 3 заказа в день у первого
for (const [i, p] of staff.entries()) {
await db.providerSchedule.deleteMany({ where: { providerId: p.id } });
await db.providerSchedule.createMany({ data: [0, 1, 2, 3, 4, 5, 6].map((weekday) => ({ providerId: p.id, weekday, startMinute: 9 * 60, endMinute: 20 * 60 })) });
await db.provider.update({ where: { id: p.id }, data: { maxOrdersPerDay: i === 0 ? 3 : null, maxHoursPerDay: i === 0 ? 8 : null } });
}
const kentron = await db.district.findUniqueOrThrow({ where: { code: 'kentron' } });
const admin = await db.adminUser.findFirstOrThrow({ where: { email: 'owner@barev.local' } });
const at = (dayOffset: number, hour: number) => {
const p = toYerevan(new Date(Date.now() + dayOffset * 24 * 3600_000));
return fromYerevan(p.year, p.month, p.day, hour * 60);
};
const NAMES = ['Ольга', 'James', 'Ирина', 'Arpi', 'Лилит'];
async function demoOrder(i: number, when: Date, rooms: number) {
const s = await startClient(ctx, { id: Number(TG_FROM) + i, first_name: NAMES[i % NAMES.length]!, language_code: i % 2 ? 'en' : 'ru' });
await giveConsent(ctx, s.clientId);
return createOrder(ctx, { clientId: s.clientId, serviceCode: 'cleaning', params: { cleaningType: 'regular', rooms, bathrooms: 1 }, scheduledAt: when, addressText: `Абовяна ${10 + i}`, districtId: kentron.id, allowBusySlot: true });
}
// 1) два заказа ждут назначения у партнёра
for (const [i, d] of [
[0, 2],
[1, 3],
] as const) {
const o = await demoOrder(i, at(d, 11), 2 + i);
await offerOrder(ctx, { orderId: o.id, adminId: admin.id, target: { kind: 'company', id: company.id } });
}
// 2) один назначен на сотрудника
const assigned = await demoOrder(2, at(2, 15), 1);
await offerOrder(ctx, { orderId: assigned.id, adminId: admin.id, target: { kind: 'company', id: company.id } });
const linked = staff.find((p) => p.userId) ?? staff[0]!;
await dispatcherAssign(ctx, { orderId: assigned.id, companyId: company.id, providerId: linked.id, dispatcherUserId: manager.userId });
// 3) один выполнен и оплачен — доход партнёра
const done = await demoOrder(3, at(1, 10), 3);
await offerOrder(ctx, { orderId: done.id, adminId: admin.id, target: { kind: 'company', id: company.id } });
await dispatcherAssign(ctx, { orderId: done.id, companyId: company.id, providerId: linked.id, dispatcherUserId: manager.userId });
for (const to of ['EN_ROUTE', 'CLEANING'] as const) await transitionOrder(ctx, { orderId: done.id, to, actor: 'ADMIN', actorId: admin.id, event: 'demo' });
await db.orderChecklistItem.updateMany({ where: { orderId: done.id }, data: { checkedAt: new Date() } });
await db.order.update({ where: { id: done.id }, data: { status: 'DONE', completedAt: new Date() } });
await markPaid(ctx, { orderId: done.id, by: { type: 'ADMIN', id: admin.id } });
// 4) новый заказ без исполнителя — оператор выбирает, кому отдать (видно, что выгоднее)
const fresh = await demoOrder(4, at(4, 12), 2);
console.log(`Заказ для назначения: #${fresh.number} (${fresh.id})`);
const code = await createProWebLogin(ctx, manager.userId);
console.log(`Партнёр: ${company.name}`);
console.log(`Кабинет менеджера в браузере (ссылка одноразовая, 15 минут):\n${PRO_APP}/?login=${code}#/company`);
process.exit(0);
+46
View File
@@ -0,0 +1,46 @@
/**
* Локальная разработка: демо бариков у клиента «Анна» (telegramId 100000001):
* начисления (доступные и в ожидании), бонус, списание, открытый запрос и предложение системы.
* pnpm --filter @barev/api dev:points-demo
*/
import { DAY } from '@barev/core';
import { getDb } from '@barev/db';
import { accruePoints, redeemPoints, requestRedemption, type Ctx } from '@barev/services';
import { env } from '../src/env.ts';
if (env.isProd) throw new Error('Только для разработки');
const db = getDb();
const ctx: Ctx = { db, notify: { emit: async () => {} }, now: () => new Date() };
const OWNER = { adminId: null, owner: true };
const user = await db.user.findUniqueOrThrow({ where: { telegramId: 100000001n }, include: { client: true } });
const clientId = user.client!.id;
// чистим прошлое демо этого клиента
await db.pointsRedemptionItem.deleteMany({ where: { redemption: { clientId } } });
await db.pointsRedemption.deleteMany({ where: { clientId } });
await db.pointsReversal.deleteMany({ where: { clientId } });
await db.pointsSuggestion.deleteMany({ where: { clientId } });
await db.pointsAccrual.deleteMany({ where: { clientId } });
await db.client.update({ where: { id: clientId }, data: { pointsActive: 0, pointsPending: 0, pointsDebt: 0, streakCount: 0, streakLastAt: null, pointsActivityAt: null } });
const cat = (code: string) => db.pointsCategory.findUniqueOrThrow({ where: { code } });
const now = Date.now();
const ago = (d: number) => new Date(now - d * DAY);
const cleaning = await cat('cleaning');
for (const [i, d] of [50, 25, 3].entries()) {
await accruePoints(ctx, { clientId, kind: 'ORDER', categoryId: cleaning.id, orderAmount: 18_000 + i * 2000, serviceDate: ago(d), orderRef: `DEMO-C${i}`, channel: 'WEB' }, OWNER);
}
await accruePoints(ctx, { clientId, kind: 'ORDER', categoryId: (await cat('documents')).id, orderAmount: 45_000, excludedAmount: 15_000, serviceDate: ago(10), orderRef: 'DEMO-D1', channel: 'WEB' }, OWNER);
// ожидание 48 ч от даты услуги: старые уже доступны — активируем
const { activatePendingPoints } = await import('@barev/services');
await activatePendingPoints(ctx);
// свежая уборка — ещё в ожидании
await accruePoints(ctx, { clientId, kind: 'ORDER', categoryId: cleaning.id, orderAmount: 19_000, serviceDate: new Date(), orderRef: 'DEMO-C3', channel: 'AUTO' }, OWNER);
const bonus = await db.pointsBonusType.findUniqueOrThrow({ where: { code: 'housewarming' } });
await accruePoints(ctx, { clientId, kind: 'BONUS', bonusTypeId: bonus.id, amount: 5000, serviceDate: new Date(), channel: 'WEB', commentForClient: 'Yerevan Home' }, OWNER);
await redeemPoints(ctx, { clientId, amount: 2000, orderAmount: 20_000, orderRef: 'DEMO-R1', reason: 'оплата по звонку', channel: 'WEB' }, OWNER);
await requestRedemption(ctx, { clientId, amount: 1500, note: 'Уборка 30 октября, 18 000 ֏' });
await db.pointsSuggestion.create({ data: { clientId, mechanic: 'first_month_route', dedupeKey: `route:${clientId}`, payload: { categories: ['cleaning', 'documents', 'learning'] }, suggestedAmount: 3000 } });
const c = await db.client.findUniqueOrThrow({ where: { id: clientId } });
console.log(`Анна: доступно ${c.pointsActive}, в ожидании ${c.pointsPending}, серия ${c.streakCount}`);
process.exit(0);