37493b7d02
Вкладка «Не писать этим» отдельной панелью SmsOptoutsPanel.vue: номер руками, пачкой из файла, удаление; непонятые строки показаны образцами, а не молча. Кнопка «Остановить» видна, пока рассылка в очереди, идёт или ждёт утра; после остановки строка показывает честный итог «Ушло N из M, списано X ₽» — деньги фактические, а не смета. Ключ заказа crypto.randomUUID() уходит с каждой отправкой и меняется после успеха. На ответ сервера «похоже, это повтор» экран задаёт вопрос словами сервера и повторяет только по согласию человека. В админке раздел «Общий стоп-лист»: список, внесение с причиной, удаление. Отказ получателя (страница по ссылке и приписка в тексте) в экранах отсутствует — отменён владельцем, см. В-30 приёмочного листа. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
8.5 KiB
TypeScript
203 lines
8.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { mount, flushPromises } from '@vue/test-utils';
|
||
import { createVuetify } from 'vuetify';
|
||
import AdminSmsView from '../../resources/js/views/admin/AdminSmsView.vue';
|
||
import type { AdminSmsSender, AdminSmsTariffsResponse } from '../../resources/js/api/admin';
|
||
|
||
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
|
||
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
|
||
return {
|
||
...orig,
|
||
fetchSmsTariffs: vi.fn(),
|
||
updateSmsTariffs: vi.fn(),
|
||
updateSmsSettings: vi.fn(),
|
||
fetchSmsSenders: vi.fn(),
|
||
approveSmsSender: vi.fn(),
|
||
rejectSmsSender: vi.fn(),
|
||
disableSmsSender: vi.fn(),
|
||
// Этап 1: общий стоп-лист портала (строки листа 1.4, 1.5).
|
||
fetchSmsGlobalOptouts: vi.fn(),
|
||
addSmsGlobalOptout: vi.fn(),
|
||
deleteSmsGlobalOptout: vi.fn(),
|
||
};
|
||
});
|
||
|
||
const adminApi = await import('../../resources/js/api/admin');
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
function makeTariffs(overrides: Partial<AdminSmsTariffsResponse> = {}): AdminSmsTariffsResponse {
|
||
return {
|
||
tariffs: [
|
||
{ id: 1, min_qty: 1, price_rub: '3.50' },
|
||
{ id: 2, min_qty: 1000, price_rub: '3.00' },
|
||
],
|
||
settings: { name_fee_rub_per_operator: '500.00', name_debt_grace_days: 5 },
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function makeSender(overrides: Partial<AdminSmsSender> = {}): AdminSmsSender {
|
||
return {
|
||
id: 1,
|
||
tenant_id: 5,
|
||
name: 'ОКНА МСК',
|
||
name_type: 'company',
|
||
status: 'pending',
|
||
monthly_fee_rub: '500.00',
|
||
note: null,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
const mountView = () =>
|
||
mount(AdminSmsView, {
|
||
global: { plugins: [createVuetify()] },
|
||
});
|
||
|
||
describe('AdminSmsView', () => {
|
||
it('на mount загружает тарифы, настройки и заявки на согласовании', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([makeSender()]);
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
expect(adminApi.fetchSmsTariffs).toHaveBeenCalledTimes(1);
|
||
expect(adminApi.fetchSmsSenders).toHaveBeenCalledWith('pending');
|
||
|
||
expect(wrapper.text()).toContain('ОКНА МСК');
|
||
expect(wrapper.find('[data-testid="sms-tariffs-save"]').exists()).toBe(true);
|
||
expect(wrapper.find('[data-testid="sms-settings-save"]').exists()).toBe(true);
|
||
});
|
||
|
||
it('«Сохранить тарифы» вызывает updateSmsTariffs с текущими строками', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
|
||
vi.mocked(adminApi.updateSmsTariffs).mockResolvedValueOnce({ tariffs: makeTariffs().tariffs });
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
await wrapper.find('[data-testid="sms-tariffs-save"]').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(adminApi.updateSmsTariffs).toHaveBeenCalledTimes(1);
|
||
const rows = vi.mocked(adminApi.updateSmsTariffs).mock.calls[0][0];
|
||
expect(rows).toEqual([
|
||
{ min_qty: 1, price_rub: '3.50' },
|
||
{ min_qty: 1000, price_rub: '3.00' },
|
||
]);
|
||
});
|
||
|
||
it('«Сохранить настройки» вызывает updateSmsSettings с полями', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
|
||
vi.mocked(adminApi.updateSmsSettings).mockResolvedValueOnce({
|
||
name_fee_rub_per_operator: '500.00',
|
||
name_debt_grace_days: 5,
|
||
});
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith('500.00', 5);
|
||
});
|
||
|
||
it('«Подтвердить» вызывает approveSmsSender(id) и перезагружает список', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders)
|
||
.mockResolvedValueOnce([makeSender({ id: 7 })])
|
||
.mockResolvedValueOnce([]);
|
||
vi.mocked(adminApi.approveSmsSender).mockResolvedValueOnce(makeSender({ id: 7, status: 'active' }));
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
await wrapper.find('[data-testid="sms-sender-approve-7"]').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(adminApi.approveSmsSender).toHaveBeenCalledWith(7);
|
||
expect(adminApi.fetchSmsSenders).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
// ─── Общий стоп-лист портала (строки листа 1.4, 1.5) ────────────────────
|
||
|
||
it('1.5 — показывает внесённые номера с причиной', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
|
||
vi.mocked(adminApi.fetchSmsGlobalOptouts).mockResolvedValueOnce([
|
||
{ phone: '79991234567', reason: 'жалоба в МТС', created_at: '2026-07-27T10:00:00Z' },
|
||
]);
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
expect(adminApi.fetchSmsGlobalOptouts).toHaveBeenCalledTimes(1);
|
||
expect(wrapper.text()).toContain('79991234567');
|
||
expect(wrapper.text()).toContain('жалоба в МТС');
|
||
});
|
||
|
||
it('1.4 — вносит номер с комментарием и перечитывает список', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
|
||
vi.mocked(adminApi.fetchSmsGlobalOptouts)
|
||
.mockResolvedValueOnce([])
|
||
.mockResolvedValueOnce([
|
||
{ phone: '79991234567', reason: 'жалоба в МТС', created_at: '2026-07-27T10:00:00Z' },
|
||
]);
|
||
vi.mocked(adminApi.addSmsGlobalOptout).mockResolvedValueOnce({
|
||
phone: '79991234567',
|
||
reason: 'жалоба в МТС',
|
||
created_at: '2026-07-27T10:00:00Z',
|
||
});
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
await wrapper.find('[data-testid="global-optout-phone"] input').setValue('79991234567');
|
||
await wrapper.find('[data-testid="global-optout-reason"] input').setValue('жалоба в МТС');
|
||
await wrapper.find('[data-testid="global-optout-add"]').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(adminApi.addSmsGlobalOptout).toHaveBeenCalledWith('79991234567', 'жалоба в МТС');
|
||
expect(adminApi.fetchSmsGlobalOptouts).toHaveBeenCalledTimes(2);
|
||
expect(wrapper.text()).toContain('79991234567');
|
||
});
|
||
|
||
it('1.5 — убирает номер из общего стоп-листа', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
|
||
vi.mocked(adminApi.fetchSmsGlobalOptouts)
|
||
.mockResolvedValueOnce([
|
||
{ phone: '79991234567', reason: null, created_at: '2026-07-27T10:00:00Z' },
|
||
])
|
||
.mockResolvedValueOnce([]);
|
||
vi.mocked(adminApi.deleteSmsGlobalOptout).mockResolvedValueOnce(undefined);
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
await wrapper.find('[data-testid="global-optout-delete-79991234567"]').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(adminApi.deleteSmsGlobalOptout).toHaveBeenCalledWith('79991234567');
|
||
expect(wrapper.text()).not.toContain('79991234567');
|
||
});
|
||
|
||
it('ошибка загрузки тарифов показывает человеческое сообщение', async () => {
|
||
vi.mocked(adminApi.fetchSmsTariffs).mockRejectedValueOnce(new Error('500'));
|
||
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
|
||
|
||
const wrapper = mountView();
|
||
await flushPromises();
|
||
|
||
expect(wrapper.find('[data-testid="sms-tariffs-error"]').exists()).toBe(true);
|
||
});
|
||
});
|