Files
portal/app/tests/Frontend/SalesProspectDialog.spec.ts
T
Дмитрий 6ceec17439 feat(sales): Этап 3 — автожизнь воронки (регистрация + джоба стадий по деньгам)
Действие «Зарегистрировался» (PATCH action=registered): email→tenant, привязка
SalesClientAssignment со снимком тарифа, linked_tenant_id+stage=registered; клиент
занят другим → 422. Диалог карточки: пункт «Зарегистрировался» + поле e-mail.
SalesProspectsAdvanceJob (каждые 15 мин, pgsql_admin): по balance_transactions
считает стадию — Σtopup≥30000→user, >0→topped_up, есть расход при 0 topup→testing,
иначе registered; ручные/отказные стадии не трогает. Схема НЕ меняется.
Гейты: бэк 19/19, фронт 9/9, Larastan 0. 🪤 property $connection конфликтовал с
трейтом Queueable → переименовал в $dbConnection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:00:16 +03:00

144 lines
6.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 { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { describe, expect, it } from 'vitest';
import SalesProspectDialog from '../../resources/js/components/sales/SalesProspectDialog.vue';
import type { SalesProspect } from '../../resources/js/api/sales';
const vuetify = createVuetify();
function prospect(overrides: Partial<SalesProspect> = {}): SalesProspect {
return {
id: 1,
sales_user_id: 1,
stage: 'new',
firm_name: 'ООО Тест',
city: 'Ростов',
phone: '+7800',
site: 'test.ru',
inn: '6161',
rating_label: 'горячая',
payload: {},
next_call_at: null,
reason: null,
registered_email: null,
notes: null,
...overrides,
};
}
interface DialogVm {
action: string;
reason: string;
nextCallAt: string;
email: string;
siteHref: string | null;
availableActions: { value: string; title: string }[];
infoRows: { label: string; value: string; href?: string }[];
submit: () => void;
}
function factory(p: SalesProspect) {
return mount(SalesProspectDialog, {
props: { modelValue: true, prospect: p },
global: { plugins: [vuetify] },
});
}
describe('SalesProspectDialog', () => {
it('siteHref добавляет https к домену', () => {
const w = factory(prospect());
expect((w.vm as unknown as DialogVm).siteHref).toBe('https://test.ru');
});
it('для stage=user действие «Отказ» недоступно', () => {
const w = factory(prospect({ stage: 'user' }));
const values = (w.vm as unknown as DialogVm).availableActions.map((a) => a.value);
expect(values).not.toContain('rejected');
});
it('для обычной стадии «Отказ» доступен', () => {
const w = factory(prospect({ stage: 'negotiation' }));
const values = (w.vm as unknown as DialogVm).availableActions.map((a) => a.value);
expect(values).toContain('rejected');
});
it('«Переговоры» без даты — submit не эмитит save', () => {
const w = factory(prospect());
(w.vm as unknown as DialogVm).action = 'negotiation';
(w.vm as unknown as DialogVm).submit();
expect(w.emitted('save')).toBeFalsy();
});
it('«Отказ» с причиной — эмитит save с action=rejected', () => {
const w = factory(prospect({ stage: 'negotiation' }));
(w.vm as unknown as DialogVm).action = 'rejected';
(w.vm as unknown as DialogVm).reason = 'дорого';
(w.vm as unknown as DialogVm).submit();
expect(w.emitted('save')?.[0]?.[0]).toMatchObject({ action: 'rejected', reason: 'дорого' });
});
it('infoRows показывает ВСЕ данные поиска из payload', () => {
const w = factory(
prospect({
inn: '6161000000',
payload: {
legal_name: 'ООО «Тест»',
ogrn: '1234567890123',
address: 'г. Ростов, ул. Ленина 1',
legal_status: 'действующая',
director: 'Иванов Иван Иванович',
director_post: 'директор',
director_is_ip: false,
director_inn: '616100000000',
contact_phone: '+79001234567',
contact_email: 'ivan@test.ru',
direct_budget_min: 80000,
direct_budget: 250000,
direct_keys: 1200,
direct_ads: 30,
channels: ['Яндекс.Директ', 'коллтрекинг'],
},
}),
);
const rows = (w.vm as unknown as DialogVm).infoRows;
const text = rows.map((r) => `${r.label}: ${r.value}`).join(' | ');
expect(text).toContain('Иванов Иван Иванович'); // директор
expect(text).toContain('директор'); // должность
expect(text).toContain('616100000000'); // личный ИНН руководителя (ПДн)
expect(text).toContain('+79001234567'); // тел. директора
expect(text).toContain('ivan@test.ru'); // почта директора
expect(text).toContain('коллтрекинг'); // каналы
expect(text).toContain('1234567890123'); // ОГРН
expect(text).toContain('6161000000'); // ИНН фирмы
expect(text).toContain('г. Ростов, ул. Ленина 1'); // адрес
expect(text).toMatch(/80 000.*250 000/); // вилка бюджета Директа
expect(text).toContain('1 200'); // запросы в Директе (разряды пробелом)
});
it('«Зарегистрировался» доступен на обычной стадии, скрыт на user', () => {
const a = (w: ReturnType<typeof factory>) => (w.vm as unknown as DialogVm).availableActions.map((x) => x.value);
expect(a(factory(prospect({ stage: 'negotiation' })))).toContain('registered');
expect(a(factory(prospect({ stage: 'user' })))).not.toContain('registered');
expect(a(factory(prospect({ stage: 'testing' })))).not.toContain('registered');
});
it('«Зарегистрировался» без e-mail — save не эмитится; с e-mail — эмитит {action:registered,email}', () => {
const w = factory(prospect({ stage: 'negotiation' }));
(w.vm as unknown as DialogVm).action = 'registered';
(w.vm as unknown as DialogVm).submit();
expect(w.emitted('save')).toBeFalsy();
(w.vm as unknown as DialogVm).email = 'client@example.com';
(w.vm as unknown as DialogVm).submit();
expect(w.emitted('save')?.[0]?.[0]).toMatchObject({ action: 'registered', email: 'client@example.com' });
});
it('infoRows пропускает пустые поля payload', () => {
const w = factory(prospect({ payload: {}, inn: null }));
const labels = (w.vm as unknown as DialogVm).infoRows.map((r) => r.label);
expect(labels).not.toContain('ОГРН');
expect(labels).not.toContain('Директор');
});
});