import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { convertLead, DomainError, dueSalesTasks, moveLead, salesBonus } from '../src/index.ts'; import { db, fixtures, makeCtx, NOW } from './helpers.ts'; beforeEach(async () => { await fixtures(); await db.salesStage.createMany({ data: [ { id: 's_new', name: 'Новый', kind: 'OPEN', sortOrder: 0 }, { id: 's_meet', name: 'Встреча', kind: 'OPEN', sortOrder: 1 }, { id: 's_won', name: 'Подключён', kind: 'WON', sortOrder: 2 }, { id: 's_lost', name: 'Отказ', kind: 'LOST', sortOrder: 3 }, ], }); }); afterAll(() => db.$disconnect()); const code = (p: Promise) => p.then(() => 'OK', (e) => (e instanceof DomainError ? e.code : String(e))); const setting = (key: string, value: number) => db.setting.upsert({ where: { key }, create: { key, value }, update: { value } }); describe('CRM продажника', () => { it('этапы: «Подключён» только через подключение, «Отказ» — с причиной; всё пишется в историю', async () => { const { ctx } = makeCtx(NOW); const sales = await db.adminUser.create({ data: { name: 'Давид', roles: ['SALES'] } }); const lead = await db.salesLead.create({ data: { kind: 'VENUE', name: 'Lavash', stageId: 's_new', ownerId: sales.id } }); await moveLead(ctx, { leadId: lead.id, stageId: 's_meet', byAdminId: sales.id }); expect(await code(moveLead(ctx, { leadId: lead.id, stageId: 's_won', byAdminId: sales.id }))).toBe('LEAD_NOT_CONVERTED'); expect(await code(moveLead(ctx, { leadId: lead.id, stageId: 's_lost', byAdminId: sales.id }))).toBe('REASON_REQUIRED'); await moveLead(ctx, { leadId: lead.id, stageId: 's_lost', byAdminId: sales.id, lostReason: 'Дорого' }); const after = await db.salesLead.findUniqueOrThrow({ where: { id: lead.id }, include: { activities: { orderBy: { createdAt: 'asc' } } } }); expect(after).toMatchObject({ stageId: 's_lost', lostReason: 'Дорого' }); expect(after.activities.map((a) => a.text)).toEqual(['Встреча', 'Отказ: Дорого']); }); it('заведение «Подключён»: партнёр с продажником и одноразовая ссылка собственнику', async () => { const { ctx } = makeCtx(NOW); const sales = await db.adminUser.create({ data: { name: 'Давид', roles: ['SALES'] } }); const lead = await db.salesLead.create({ data: { kind: 'VENUE', name: 'Lavash', category: 'restaurant', address: 'Туманяна 21', lat: 40.18, lng: 44.51, stageId: 's_meet', ownerId: sales.id } }); const r = await convertLead(ctx, { leadId: lead.id, byAdminId: sales.id }); expect(r.kind).toBe('VENUE'); expect(r.url).toMatch(/\?start=po-/); const partner = await db.partner.findUniqueOrThrow({ where: { id: (r as { partnerId: string }).partnerId } }); expect(partner).toMatchObject({ name: 'Lavash', category: 'restaurant', salesId: sales.id, lat: 40.18 }); expect(partner.ownerInviteCode).toBeTruthy(); expect(await db.salesLead.findUniqueOrThrow({ where: { id: lead.id } })).toMatchObject({ stageId: 's_won', partnerId: partner.id }); expect(await code(convertLead(ctx, { leadId: lead.id, byAdminId: sales.id }))).toBe('ALREADY_CONVERTED'); }); it('агентство «Подключён»: код уникален, ссылка для клиентов, % по умолчанию из настроек', async () => { const { ctx } = makeCtx(NOW); const sales = await db.adminUser.create({ data: { name: 'Давид', roles: ['SALES'] } }); const lead = await db.salesLead.create({ data: { kind: 'AGENCY', name: 'Yerevan Homes', stageId: 's_new', ownerId: sales.id } }); expect(await code(convertLead(ctx, { leadId: lead.id, byAdminId: sales.id, agencyCode: 'домик' }))).toBe('AGENCY_CODE_FORMAT'); expect(await code(convertLead(ctx, { leadId: lead.id, byAdminId: sales.id, agencyCode: 'domik' }))).toBe('AGENCY_CODE_TAKEN'); const r = await convertLead(ctx, { leadId: lead.id, byAdminId: sales.id, agencyCode: 'yhomes' }); expect(r.url).toMatch(/\?start=a-YHOMES$/); expect(await db.agency.findUniqueOrThrow({ where: { code: 'YHOMES' } })).toMatchObject({ name: 'Yerevan Homes', commissionPct: 10, salesId: sales.id }); }); it('задачи: в срок — одно напоминание, выполненные и будущие — нет', async () => { const { ctx, events, clock } = makeCtx(NOW); const sales = await db.adminUser.create({ data: { name: 'Давид', roles: ['SALES'] } }); const due = await db.salesTask.create({ data: { assigneeId: sales.id, title: 'Позвонить', dueAt: new Date(NOW.getTime() - 60_000) } }); await db.salesTask.create({ data: { assigneeId: sales.id, title: 'Потом', dueAt: new Date(NOW.getTime() + 3600_000) } }); await db.salesTask.create({ data: { assigneeId: sales.id, title: 'Сделано', dueAt: new Date(NOW.getTime() - 60_000), doneAt: NOW } }); expect(await dueSalesTasks(ctx)).toBe(1); expect(events).toEqual([{ type: 'sales.taskDue', taskId: due.id }]); expect(await dueSalesTasks(ctx)).toBe(0); clock.now = new Date(NOW.getTime() + 2 * 3600_000); expect(await dueSalesTasks(ctx)).toBe(1); }); it('бонус: ставки за заведения и агентства + % выручки клиентов агентства в первые N месяцев', async () => { const { ctx } = makeCtx(NOW); const sales = await db.adminUser.create({ data: { name: 'Давид', roles: ['SALES'] } }); const from = new Date(NOW.getTime() - 10 * 86_400_000); const to = new Date(NOW.getTime() + 86_400_000); expect((await salesBonus(ctx, sales.id, from, to)).total).toBe(0); await setting('sales.bonusPerVenue', 10000); await setting('sales.bonusPerAgency', 20000); await setting('sales.bonusAgencyPct', 5); await db.partner.create({ data: { name: 'Cafe', category: 'cafe', offer: {}, salesId: sales.id, createdAt: NOW } }); const agency = await db.agency.create({ data: { code: 'NEWAG', name: 'New', salesId: sales.id, createdAt: new Date(NOW.getTime() - 86_400_000) } }); const user = await db.user.create({ data: { telegramId: 990001n } }); const client = await db.client.create({ data: { userId: user.id, cardCode: 'BONUS1', agencyId: agency.id, consentAt: NOW } }); await db.subscription.create({ data: { clientId: client.id, plan: 'BAREV_PLUS', source: 'PAID', amount: 40000, startsAt: NOW, endsAt: new Date(NOW.getTime() + 30 * 86_400_000), createdAt: NOW } }); const b = await salesBonus(ctx, sales.id, from, to); expect(b).toMatchObject({ venues: 1, agencies: 1, revenue: 40000, fromVenues: 10000, fromAgencies: 20000, fromRevenue: 2000, total: 32000 }); }); });