Files
portal/app/tests/Frontend/SalesInvoices.spec.ts
T
Дмитрий 04d2346754 feat(sales): экран «Счета» — список + отметка оплаты (фронт)
Task 5.1b — SalesInvoicesView (начальник, #page-invoices) поверх готовых
эндпоинтов /api/sales/invoices.

- Таблица счетов (дата/номер/клиент/плательщик/сумма/статус/оплатить до),
  поиск с debounce + фильтр статуса, чипы статусов.
- Кнопка «Отметить оплаченным» (только issued/overdue) → диалог
  подтверждения → POST mark-paid → snackbar + перезагрузка. Зачисление
  баланса и Акт формирует бэк (InvoicePaymentService).
- api/sales.ts: listSalesInvoices/markSalesInvoicePaid. Роут /sales/invoices
  со заглушки на экран.

Vitest 5/5, фронт-набор 1066 без регрессий, ESLint чист.

Фаза 5 завершена: начальник отмечает оплату счёта прямо в портале продаж.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:45:17 +03:00

139 lines
5.6 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, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createPinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import SalesInvoicesView from '../../resources/js/views/sales/SalesInvoicesView.vue';
// ─── mock sales api ───────────────────────────────────────────────────────────
const listSalesInvoices = vi.fn();
const markSalesInvoicePaid = vi.fn();
vi.mock('../../resources/js/api/sales', () => ({
listSalesInvoices: (...args: unknown[]) => listSalesInvoices(...args),
markSalesInvoicePaid: (...args: unknown[]) => markSalesInvoicePaid(...args),
extractSalesErrorMessage: (_e: unknown, fallback = 'Ошибка') => fallback,
}));
function mockResponse() {
return {
data: [
{
id: 7,
invoice_number: 'СЧ-2026-0007',
amount_total: '30000',
status: 'issued',
issued_at: '2026-06-28T09:00:00+03:00',
expires_at: '2026-07-05T00:00:00+03:00',
tenant_id: 101,
tenant_name: 'Окна Москва ООО',
payer_name: 'ООО «Окна Москва»',
},
{
id: 4,
invoice_number: 'СЧ-2026-0004',
amount_total: '15000',
status: 'overdue',
issued_at: '2026-06-18T09:00:00+03:00',
expires_at: '2026-06-25T00:00:00+03:00',
tenant_id: 102,
tenant_name: 'Двери Премиум',
payer_name: 'ООО «Двери Премиум»',
},
{
id: 5,
invoice_number: 'СЧ-2026-0005',
amount_total: '25000',
status: 'paid',
issued_at: '2026-06-20T09:00:00+03:00',
expires_at: null,
tenant_id: 103,
tenant_name: 'Натяжные потолки СПб',
payer_name: 'ИП Иванов И.И.',
},
],
meta: { current_page: 1, last_page: 1, total: 3, per_page: 25 },
};
}
beforeEach(() => {
vi.clearAllMocks();
listSalesInvoices.mockResolvedValue(mockResponse());
markSalesInvoicePaid.mockResolvedValue({ status: 'ok' });
});
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/sales/invoices', name: 'sales-invoices', component: SalesInvoicesView },
{ path: '/sales', name: 'sales-overview', component: { template: '<div/>' } },
],
});
return router.push('/sales/invoices').then(() => router);
}
async function mountView() {
const router = await makeRouter();
const wrapper = mount(SalesInvoicesView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
return wrapper;
}
describe('SalesInvoicesView', () => {
it('вызывает listSalesInvoices при монтировании', async () => {
await mountView();
expect(listSalesInvoices).toHaveBeenCalled();
});
it('отрисовывает строки счетов из мока', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('СЧ-2026-0007');
expect(text).toContain('Окна Москва ООО');
expect(text).toContain('ООО «Окна Москва»');
expect(text).toContain('СЧ-2026-0004');
expect(text).toContain('Двери Премиум');
expect(text).toContain('СЧ-2026-0005');
});
it('показывает статус-чипы (Выставлен / Просрочен / Оплачен)', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Выставлен');
expect(text).toContain('Просрочен');
expect(text).toContain('Оплачен');
});
it('для issued/overdue есть кнопка «Отметить оплаченным», для paid — нет', async () => {
const wrapper = await mountView();
expect(wrapper.find('[data-testid="mark-paid-7"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="mark-paid-4"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="mark-paid-5"]').exists()).toBe(false);
});
it('клик «Отметить оплаченным» открывает диалог, подтверждение вызывает markSalesInvoicePaid с id + перезагрузку', async () => {
const wrapper = await mountView();
expect(listSalesInvoices).toHaveBeenCalledTimes(1);
await wrapper.find('[data-testid="mark-paid-7"]').trigger('click');
await flushPromises();
// диалог открыт
expect(wrapper.vm.confirmOpen).toBe(true);
// подтверждаем
await wrapper.vm.doMarkPaid();
await flushPromises();
expect(markSalesInvoicePaid).toHaveBeenCalledWith(7);
// перезагрузка списка после отметки оплаты
expect(listSalesInvoices).toHaveBeenCalledTimes(2);
});
});