krona-front/src/features/__tests__/crmEndToEndIntegrity.test.tsx

216 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
});