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

+193
View File
@@ -0,0 +1,193 @@
import { afterAll, beforeEach, describe, expect, it } from 'vitest';
import { addMonths } from '@barev/core';
import {
applyAgencyCode,
assignmentCandidates,
companyAutoAssign,
completeWebRequest,
createOrder,
createWebRequest,
dispatcherAssign,
expireOffers,
giveConsent,
linkWebRequest,
markPaid,
offerOrder,
partnerAddStaff,
partnerCabinetData,
partnerDeclineOrder,
partnerUnassign,
respondToOffer,
startClient,
} from '../src/index.ts';
import { db, fixtures, makeCtx, NOW, slotAt, tgUser } from './helpers.ts';
let fx: Awaited<ReturnType<typeof fixtures>>;
beforeEach(async () => {
fx = await fixtures();
});
afterAll(() => db.$disconnect());
type C = ReturnType<typeof makeCtx>['ctx'];
async function order(ctx: C, tg: number, at = slotAt(2, 11), allowBusySlot = false) {
const s = await startClient(ctx, tgUser(tg));
await giveConsent(ctx, s.clientId);
return createOrder(ctx, { clientId: s.clientId, serviceCode: 'cleaning', params: { cleaningType: 'regular', rooms: 2 }, scheduledAt: at, addressText: 'Pushkin 10', districtId: fx.kentron.id, allowBusySlot });
}
const admin = () => db.adminUser.create({ data: { name: 'Op', roles: ['SUPPORT'] } });
const events = async (orderId: string) => (await db.orderStatusHistory.findMany({ where: { orderId }, orderBy: { createdAt: 'asc' } })).map((h) => h.event);
describe('партнёры услуг: оператор назначает', () => {
it('у каждого варианта видно, что остаётся нам; самый выгодный подсвечен', async () => {
const { ctx } = makeCtx(NOW);
await db.providerCompany.update({ where: { id: fx.company.id }, data: { deal: 'COMMISSION', commissionPct: 40 } });
const o = await order(ctx, 901);
const r = await assignmentCandidates(ctx, o.id);
const chisto = r.ranked.find((c) => c.kind === 'company')!;
expect(chisto.margin).toBe(Math.floor(o.totalPrice * 0.4));
const maria = r.ranked.find((c) => c.name === 'Maria')!;
expect(maria.margin).toBe(o.totalPrice - maria.cost);
const best = [...r.ranked].sort((a, b) => b.margin - a.margin)[0]!;
expect(r.bestId).toBe(`${best.kind}:${best.id}`);
});
it('партнёр отказался — заказ снова у оператора', async () => {
const { ctx, events: ev } = makeCtx(NOW);
const o = await order(ctx, 902);
const op = await admin();
await offerOrder(ctx, { orderId: o.id, adminId: op.id, target: { kind: 'company', id: fx.company.id } });
const offer = await db.orderOffer.findFirstOrThrow({ where: { orderId: o.id, companyId: fx.company.id, status: 'OFFERED' } });
expect(offer.expiresAt).not.toBeNull();
await partnerDeclineOrder(ctx, { orderId: o.id, companyId: fx.company.id, userId: fx.dispatcher.id });
expect((await db.order.findUniqueOrThrow({ where: { id: o.id } })).companyId).toBeNull();
expect(ev).toContainEqual({ type: 'partner.returned', orderId: o.id, companyId: fx.company.id, reason: 'declined' });
});
it('партнёр не назначил сотрудника вовремя — заказ возвращается оператору', async () => {
const { ctx, clock } = makeCtx(NOW);
const o = await order(ctx, 903);
await offerOrder(ctx, { orderId: o.id, adminId: (await admin()).id, target: { kind: 'company', id: fx.company.id } });
clock.now = new Date(NOW.getTime() + 61 * 60_000);
await expireOffers(ctx);
expect((await db.order.findUniqueOrThrow({ where: { id: o.id } })).companyId).toBeNull();
expect(await events(o.id)).toContain('partnerExpired');
});
});
describe('партнёры услуг: автораспределение по сотрудникам', () => {
it('заказ сам уходит сотруднику; отказ — следующему; никого нет — менеджеру', async () => {
const { ctx, events: ev } = makeCtx(NOW);
await db.providerCompany.update({ where: { id: fx.company.id }, data: { autoAssign: true } });
const added = await partnerAddStaff(ctx, {
companyId: fx.company.id,
userId: fx.dispatcher.id,
staff: { name: 'Gayane', languages: ['hy'], districtIds: [fx.kentron.id], schedule: [0, 1, 2, 3, 4, 5, 6].map((weekday) => ({ weekday, startMinute: 540, endMinute: 1200 })) },
});
// сотрудник ещё не вошёл в бот — автоматически ему не предлагаем
expect(added.inviteCode).toMatch(/^PRO-/);
const o = await order(ctx, 904);
await offerOrder(ctx, { orderId: o.id, adminId: (await admin()).id, target: { kind: 'company', id: fx.company.id } });
const first = await db.orderOffer.findFirstOrThrow({ where: { orderId: o.id, status: 'OFFERED', providerId: { not: null } } });
expect(first.providerId).toBe(fx.lusine.id);
await respondToOffer(ctx, { offerId: first.id, providerId: fx.lusine.id, accept: false });
// больше подходящих нет — заказ ждёт менеджера
expect(await db.orderOffer.count({ where: { orderId: o.id, status: 'OFFERED', providerId: null, companyId: fx.company.id } })).toBe(1);
expect(ev).toContainEqual({ type: 'company.noStaff', orderId: o.id, companyId: fx.company.id });
expect(await companyAutoAssign(ctx, o.id)).toEqual({ result: 'none' });
});
it('лимит заказов в день и перерыв на дорогу', async () => {
const { ctx } = makeCtx(NOW);
await db.providerCompany.update({ where: { id: fx.company.id }, data: { autoAssign: true, travelGapMinutes: 60 } });
// первый заказ Лусине: 11:00, уборка 2 комнат ~3 часа
const a = await order(ctx, 905, slotAt(2, 11));
await offerOrder(ctx, { orderId: a.id, adminId: (await admin()).id, target: { kind: 'company', id: fx.company.id } });
const offerA = await db.orderOffer.findFirstOrThrow({ where: { orderId: a.id, status: 'OFFERED', providerId: { not: null } } });
await respondToOffer(ctx, { offerId: offerA.id, providerId: fx.lusine.id, accept: true });
const end = new Date(a.scheduledAt.getTime() + a.durationMinutes * 60_000);
// сразу после окончания — нет времени доехать
const b = await order(ctx, 906, new Date(end.getTime() + 30 * 60_000), true);
const cab = await import('../src/servicePartners.ts');
const settings = await (await import('../src/settings.ts')).getSettings(db);
const tight = await cab.companyStaffCandidates(db, fx.company.id, b.id, settings);
expect(tight.rows.find((r) => r.id === fx.lusine.id)?.check).toBe('gap');
// лимит 1 заказ в день
await db.provider.update({ where: { id: fx.lusine.id }, data: { maxOrdersPerDay: 1 } });
const c = await order(ctx, 907, new Date(end.getTime() + 2 * 3600_000), true);
const limited = await cab.companyStaffCandidates(db, fx.company.id, c.id, settings);
expect(limited.rows.find((r) => r.id === fx.lusine.id)?.check).toBe('limit');
});
it('менеджер назначает вручную и снимает сотрудника', async () => {
const { ctx } = makeCtx(NOW);
const o = await order(ctx, 908);
await offerOrder(ctx, { orderId: o.id, adminId: (await admin()).id, target: { kind: 'company', id: fx.company.id } });
await dispatcherAssign(ctx, { orderId: o.id, companyId: fx.company.id, providerId: fx.lusine.id, dispatcherUserId: fx.dispatcher.id });
expect((await db.order.findUniqueOrThrow({ where: { id: o.id } })).providerId).toBe(fx.lusine.id);
await partnerUnassign(ctx, { orderId: o.id, companyId: fx.company.id, userId: fx.dispatcher.id });
const after = await db.order.findUniqueOrThrow({ where: { id: o.id } });
expect(after).toMatchObject({ providerId: null, companyId: fx.company.id, status: 'NEW' });
const cab = await partnerCabinetData(ctx, fx.company.id, fx.dispatcher.id);
expect(cab.pending.map((x) => x.id)).toContain(o.id);
});
});
describe('партнёры услуг: доход', () => {
it('«расходы наши»: при оплате фиксируется доля партнёра от прибыли', async () => {
const { ctx } = makeCtx(NOW);
await db.providerCompany.update({ where: { id: fx.company.id }, data: { deal: 'PROFIT_SHARE', profitSharePct: 30, fotPct: 60, taxPct: 5 } });
const o = await order(ctx, 910);
await offerOrder(ctx, { orderId: o.id, adminId: (await admin()).id, target: { kind: 'company', id: fx.company.id } });
await dispatcherAssign(ctx, { orderId: o.id, companyId: fx.company.id, providerId: fx.lusine.id, dispatcherUserId: fx.dispatcher.id });
await markPaid(ctx, { orderId: o.id, by: { type: 'PROVIDER', id: fx.lusine.id } });
const e = await db.partnerEarning.findUniqueOrThrow({ where: { orderId: o.id } });
const fot = Math.floor(o.totalPrice * 0.6);
const tax = Math.floor(o.totalPrice * 0.05);
expect(e).toMatchObject({ deal: 'PROFIT_SHARE', paid: o.totalPrice, fot, tax, amount: Math.floor(((o.totalPrice - fot - tax) * 30) / 100) });
const cab = await partnerCabinetData(ctx, fx.company.id, fx.dispatcher.id);
expect(cab.money.balance).toBe(e.amount);
});
it('чужой менеджер не видит кабинет', async () => {
const { ctx } = makeCtx(NOW);
await expect(partnerCabinetData(ctx, fx.company.id, (await db.user.create({ data: { telegramId: 99999n } })).id)).rejects.toMatchObject({ code: 'FORBIDDEN' });
});
});
describe('подарок агентства', () => {
it('уровень и срок задаются у агентства', async () => {
const { ctx } = makeCtx(NOW);
await db.agency.update({ where: { id: fx.agency.id }, data: { giftPlan: 'ELITE', giftMonths: 3 } });
const s = await startClient(ctx, tgUser(920));
await giveConsent(ctx, s.clientId);
const r = await applyAgencyCode(ctx, s.clientId, 'DOMIK');
expect(r.gift).toEqual({ plan: 'ELITE', endsAt: addMonths(NOW, 3) });
const sub = await db.subscription.findFirstOrThrow({ where: { clientId: s.clientId } });
expect(sub).toMatchObject({ plan: 'ELITE', agencyId: fx.agency.id, source: 'AGENCY_GIFT' });
});
it('агентство без подарка', async () => {
const { ctx } = makeCtx(NOW);
await db.agency.update({ where: { id: fx.agency.id }, data: { giftPlan: null } });
const s = await startClient(ctx, tgUser(921));
await giveConsent(ctx, s.clientId);
expect((await applyAgencyCode(ctx, s.clientId, 'DOMIK')).gift).toBeNull();
});
});
describe('заявка с сайта без времени и адреса', () => {
it('бот доспрашивает недостающее и оформляет заказ', async () => {
const { ctx } = makeCtx(NOW);
const req = await createWebRequest(ctx, { serviceCode: 'cleaning', params: { cleaningType: 'regular', rooms: 2, bathrooms: 1 }, phone: '+37491123456', locale: 'ru' });
expect(req).toMatchObject({ preferredAt: null, districtId: null, addressText: null });
const s = await startClient(ctx, tgUser(930));
const link = await linkWebRequest(ctx, req.token, s.clientId);
expect(link).toMatchObject({ kind: 'incomplete', missing: ['district', 'address', 'time'] });
if (link.kind !== 'incomplete') return;
expect(await completeWebRequest(ctx, { requestId: link.requestId, clientId: s.clientId, districtCode: 'kentron' })).toMatchObject({ kind: 'incomplete', missing: ['address', 'time'] });
expect(await completeWebRequest(ctx, { requestId: link.requestId, clientId: s.clientId, addressText: 'Abovyan 5' })).toMatchObject({ kind: 'incomplete', missing: ['time'] });
const done = await completeWebRequest(ctx, { requestId: link.requestId, clientId: s.clientId, preferredAt: slotAt(2, 12) });
expect(done).toMatchObject({ kind: 'ordered' });
expect((await db.webRequest.findUniqueOrThrow({ where: { id: req.id } })).status).toBe('ORDERED');
});
});