import { describe, it, expect, beforeEach } from 'vitest'; import { resetDatabase } from '../../services/storage/indexedDb'; import { getServices } from '../../services/mock'; import { OrderItem } from '../../entities/commerce/order'; describe('CRM End-to-End Business Scenario Integrity Suite (F08)', () => { const { crm, commerce } = getServices(); beforeEach(async () => { await resetDatabase(); }); it('1. Executes full end-to-end flow: Lead -> Conversion -> Contact + Company + Deal -> Won -> Order with specification and two-way link', async () => { // 1. Create a product in commerce const product = await commerce.products.create({ sku: 'KRN-BARISTA-01', name: 'Кофемашина Krona Pro Barista Dual', categoryId: 'cat_equipment', price: { minor: 25000000, currency: 'RUB' }, costPrice: { minor: 15000000, currency: 'RUB' }, unit: 'шт', isService: false, trackInventory: true, status: 'active', }); // 2. Create an incoming Lead const lead = await crm.leads.create({ title: 'Поставка оборудования для кофейни «Арома»', contactName: 'Михаил Соколов', companyName: 'ООО Арома Групп', phone: '+7 (926) 444-55-66', email: 'mikhail@aroma-group.ru', source: 'website', status: 'new', estimatedValue: { minor: 50000000, currency: 'RUB' }, assignedTo: 'Менеджер Елена', score: 85, }); expect(lead.status).toBe('new'); // 3. Convert Lead to Deal const conversionResult = await crm.convertLeadToDeal( lead.id, 'Сделка: Оснащение кофейни Арома Групп' ); const { deal, contact } = conversionResult; expect(deal).toBeDefined(); expect(contact).toBeDefined(); // Verify Contact fields and company linkage expect(contact!.firstName).toBe('Михаил'); expect(contact!.lastName).toBe('Соколов'); expect(contact!.email).toBe('mikhail@aroma-group.ru'); expect(contact!.phone).toBe('+7 (926) 444-55-66'); expect(contact!.assignedTo).toBe('Менеджер Елена'); expect(contact!.roles).toContain('client'); expect(contact!.companyId).toBeDefined(); // Verify Company was auto-created from companyName const company = await crm.companies.getById(contact!.companyId!); expect(company).toBeDefined(); expect(company!.name).toBe('ООО Арома Групп'); // Verify Deal details expect(deal.title).toBe('Сделка: Оснащение кофейни Арома Групп'); expect(deal.stage).toBe('proposal'); expect(deal.contactId).toBe(contact!.id); expect(deal.companyId).toBe(company!.id); expect(deal.amount.minor).toBe(50000000); expect(deal.assignedTo).toBe('Менеджер Елена'); // Verify Lead state is converted with references const updatedLead = await crm.leads.getById(lead.id); expect(updatedLead!.status).toBe('converted'); expect(updatedLead!.convertedContactId).toBe(contact!.id); expect(updatedLead!.convertedDealId).toBe(deal.id); // 4. Progress Deal through stages to 'won' and attach real specification items await crm.deals.update(deal.id, { stage: 'won', items: [ { id: 'deal_item_1', productId: product.id, sku: product.sku, name: product.name, quantity: 2, unitPrice: { minor: 25000000, currency: 'RUB' }, totalPrice: { minor: 50000000, currency: 'RUB' }, }, ], }); const wonDeal = (await crm.deals.getById(deal.id))!; expect(wonDeal.stage).toBe('won'); expect(wonDeal.items).toHaveLength(1); // 5. Create Order from Won Deal with real specification (no placeholders like 'prd_coffee_beans' or 'COF-100') const orderItems: OrderItem[] = wonDeal.items!.map((it) => ({ id: `ord_item_${it.id}`, productId: it.productId, sku: it.sku || 'SKU-UNKNOWN', name: it.name, quantity: it.quantity, unitPrice: it.unitPrice, discountMinor: 0, totalPrice: it.totalPrice, })); const order = await commerce.orders.create({ orderNumber: 'ORD-CRM-TEST-001', customerType: 'company', customerId: wonDeal.companyId!, customerName: company!.name, status: 'draft', paymentStatus: 'unpaid', fulfillmentStatus: 'unfulfilled', items: orderItems, subtotal: wonDeal.amount, discountMinor: 0, taxMinor: Math.round(wonDeal.amount.minor * 0.2), shippingMinor: 0, total: wonDeal.amount, channel: 'b2b', warehouseId: 'wh_main', dealId: wonDeal.id, // Two-way link: Order -> Deal }); // Update Deal with Order reference (Two-way link: Deal -> Order) await crm.deals.update(wonDeal.id, { orderId: order.id }); // 6. Verify Two-Way Link const finalizedOrder = await commerce.orders.getById(order.id); const finalizedDeal = await crm.deals.getById(wonDeal.id); expect(finalizedOrder!.dealId).toBe(wonDeal.id); expect(finalizedDeal!.orderId).toBe(order.id); // Verify that NO placeholder values were used expect(finalizedOrder!.items[0].productId).toBe(product.id); expect(finalizedOrder!.items[0].productId).not.toBe('prd_coffee_beans'); expect(finalizedOrder!.items[0].sku).toBe('KRN-BARISTA-01'); expect(finalizedOrder!.items[0].sku).not.toBe('COF-100'); expect(finalizedOrder!.items[0].unitPrice.minor).toBe(25000000); // 7. Verify Timeline includes linked deal and order events const timeline = await crm.getTimeline('deal', wonDeal.id); expect(timeline.some((t) => t.type === 'order_created')).toBe(true); }); it('2. Idempotency: Repeating convertLeadToDeal on an already converted lead returns existing deal and contact without duplication', async () => { const lead = await crm.leads.create({ title: 'Лид для проверки повторной конвертации', contactName: 'Ольга Николаева', companyName: 'ЗАО Технопром', phone: '+7 (911) 777-88-99', email: 'olga@technoprom.ru', source: 'referral', status: 'new', }); const firstRun = await crm.convertLeadToDeal(lead.id, 'Первая конвертация'); expect(firstRun.deal).toBeDefined(); expect(firstRun.contact).toBeDefined(); const dealsBefore = await crm.deals.list({ pageSize: 100 }); const contactsBefore = await crm.contacts.list({ pageSize: 100 }); // Second call on already converted lead const secondRun = await crm.convertLeadToDeal(lead.id, 'Повторная попытка конвертации'); expect(secondRun.deal.id).toBe(firstRun.deal.id); expect(secondRun.contact?.id).toBe(firstRun.contact?.id); const dealsAfter = await crm.deals.list({ pageSize: 100 }); const contactsAfter = await crm.contacts.list({ pageSize: 100 }); // No duplicate records created! expect(dealsAfter.items.length).toBe(dealsBefore.items.length); expect(contactsAfter.items.length).toBe(contactsBefore.items.length); }); it('3. Existing Contact Matching: Converts new lead for an existing contact by email or phone without creating duplicate contact', async () => { // Pre-existing contact const existingContact = await crm.contacts.create({ firstName: 'Сергей', lastName: 'Васильев', phone: '+7 (905) 123-45-67', email: 'sergey.vasiliev@corp.ru', roles: ['client'], status: 'active', }); // New lead with matching email and formatted phone const newLead = await crm.leads.create({ title: 'Второй запрос от существующего клиента', contactName: 'Сергей Васильев', phone: '+7-905-123-45-67', email: 'SERGEY.VASILIEV@CORP.RU', // case-insensitive check source: 'phone', status: 'new', }); const conversion = await crm.convertLeadToDeal(newLead.id, 'Вторая сделка с Сергеем'); expect(conversion.contact?.id).toBe(existingContact.id); const allContacts = await crm.contacts.list({ pageSize: 100 }); const matchingContacts = allContacts.items.filter( (c) => c.email?.toLowerCase() === 'sergey.vasiliev@corp.ru' ); expect(matchingContacts).toHaveLength(1); }); });