a56dcb06b2
Клиент сам выставляет PDF-счёт (TopupDialog вкладка «По счёту»), счета и акты — в отдельной вкладке «Счета». Админ (/admin/invoices) отмечает оплату одной кнопкой → атомарно зачисляет баланс (BillingTopupService), формирует Акт (без НДС, saas_upd_documents ДОП) и шлёт клиенту письмо «Счёт оплачен» с вложением PDF-акта. PDF открываются inline в браузере (ASCII-имя). - Сервисы InvoiceNumberGenerator/InvoiceService/ActService/InvoicePaymentService/PdfRenderer - Контроллеры InvoiceController (клиент) + AdminInvoiceController (список+mark-paid) - Модели SaasInvoice/SaasInvoiceItem/SaasUpdDocument; шаблоны pdf/invoice|act - Нумерация СЧ-ГГГГ-NNNNN (advisory-lock); просрочка invoices:expire (cron) - Наименование услуги: «Оплата генерации рекламных лидов» - Зависимость barryvdh/laravel-dompdf (default_font dejavu sans); схема БД не менялась - Этап 2 (автомат через ВТБ API) — отдельно, спека/план в docs/superpowers Тесты: счета 13, Billing 138, фронт зелёные; larastan baseline +6 (Pest false-pos). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { mount, flushPromises } from '@vue/test-utils';
|
|
import { createVuetify } from 'vuetify';
|
|
import AdminInvoicesView from '../../resources/js/views/admin/AdminInvoicesView.vue';
|
|
import * as adminApi from '../../resources/js/api/admin';
|
|
|
|
const oneIssued = {
|
|
data: [
|
|
{
|
|
id: 5,
|
|
invoice_number: 'СЧ-2026-00005',
|
|
amount_total: '700.00',
|
|
status: 'issued',
|
|
issued_at: '2026-06-29',
|
|
expires_at: null,
|
|
tenant_id: 2,
|
|
tenant_name: 'ООО Клиент',
|
|
payer_name: 'ООО Клиент',
|
|
},
|
|
],
|
|
meta: { total: 1, current_page: 1, last_page: 1, per_page: 25 },
|
|
};
|
|
|
|
describe('AdminInvoicesView', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('рендерит счёт и его статус', async () => {
|
|
vi.spyOn(adminApi, 'listAdminInvoices').mockResolvedValue(oneIssued);
|
|
const w = mount(AdminInvoicesView, { global: { plugins: [createVuetify()] } });
|
|
await flushPromises();
|
|
expect(w.text()).toContain('СЧ-2026-00005');
|
|
expect(w.text()).toContain('Выставлен');
|
|
});
|
|
|
|
it('«Отметить оплаченным» открывает диалог и зовёт markInvoicePaid после подтверждения', async () => {
|
|
vi.spyOn(adminApi, 'listAdminInvoices').mockResolvedValue(oneIssued);
|
|
const spy = vi.spyOn(adminApi, 'markInvoicePaid').mockResolvedValue();
|
|
|
|
const w = mount(AdminInvoicesView, {
|
|
global: {
|
|
plugins: [createVuetify()],
|
|
stubs: {
|
|
VDialog: {
|
|
template: '<div class="dialog-stub" v-if="modelValue"><slot /></div>',
|
|
props: ['modelValue'],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await flushPromises();
|
|
|
|
const btn = w.find('[data-testid="mark-paid-5"]');
|
|
expect(btn.exists()).toBe(true);
|
|
await btn.trigger('click');
|
|
await w.vm.$nextTick();
|
|
|
|
const confirm = w.findAll('button').find((b) => b.text().includes('Подтверждаю'));
|
|
expect(confirm).toBeTruthy();
|
|
await confirm!.trigger('click');
|
|
await flushPromises();
|
|
|
|
expect(spy).toHaveBeenCalledWith(5);
|
|
});
|
|
});
|