174dbae808
Backend TenantChargesController: - GET /api/billing/charges — paginated list, filters period (current_month / last_month / 90d) + charge_source. - POST /api/billing/charges/export — StreamedResponse CSV (BOM + UTF-8) с chunkById(500). - auth:sanctum + tenant middleware — RLS изолирует tenant_id. - 6 Pest integration tests (RLS isolation + filters + pagination + CSV export). Frontend ChargesTab.vue: - v-data-table-server с paginated load + period/charge_source filters. - CSV-download через blob → createObjectURL. - Forest-palette + JetBrains Mono tnum. BillingView.vue — добавлен tab «Списания» с импортом ChargesTab. ChargesTab.story.vue + 4 Vitest tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.7 KiB
TypeScript
97 lines
3.7 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { mount } from '@vue/test-utils';
|
|
import { createVuetify } from 'vuetify';
|
|
import axios from 'axios';
|
|
|
|
import ChargesTab from '../../resources/js/views/billing/ChargesTab.vue';
|
|
|
|
vi.mock('axios');
|
|
|
|
// vite-plugin-vuetify auto-import регистрируется через vitest.config.ts,
|
|
// поэтому createVuetify() без явного передачи components работает.
|
|
const vuetify = createVuetify();
|
|
|
|
const mockData = {
|
|
data: [
|
|
{
|
|
id: 1,
|
|
charged_at: '2026-05-01T10:00:00Z',
|
|
deal_id: 42,
|
|
tier_no: 1,
|
|
charge_source: 'rub',
|
|
price_per_lead_kopecks: 50000,
|
|
},
|
|
{
|
|
id: 2,
|
|
charged_at: '2026-05-01T11:00:00Z',
|
|
deal_id: 43,
|
|
tier_no: 1,
|
|
charge_source: 'prepaid',
|
|
price_per_lead_kopecks: 0,
|
|
},
|
|
],
|
|
meta: { current_page: 1, last_page: 1, total: 2, per_page: 20 },
|
|
};
|
|
|
|
describe('ChargesTab', () => {
|
|
beforeEach(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.get as any).mockResolvedValue({ data: mockData });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(axios.post as any).mockResolvedValue({ data: new Blob() });
|
|
globalThis.URL.createObjectURL = vi.fn(() => 'blob:url');
|
|
globalThis.URL.revokeObjectURL = vi.fn();
|
|
});
|
|
|
|
it('renders 2 charge rows from API', async () => {
|
|
const wrapper = mount(ChargesTab, {
|
|
global: { plugins: [vuetify], stubs: { RouterLink: true } },
|
|
});
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect((wrapper.vm as any).total).toBe(2);
|
|
});
|
|
|
|
it('period change triggers refetch', async () => {
|
|
const wrapper = mount(ChargesTab, {
|
|
global: { plugins: [vuetify], stubs: { RouterLink: true } },
|
|
});
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(wrapper.vm as any).period = 'last_month';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).refresh();
|
|
expect(axios.get).toHaveBeenLastCalledWith('/api/billing/charges', {
|
|
params: expect.objectContaining({ period: 'last_month' }),
|
|
});
|
|
});
|
|
|
|
it('charge_source filter is sent in params', async () => {
|
|
const wrapper = mount(ChargesTab, {
|
|
global: { plugins: [vuetify], stubs: { RouterLink: true } },
|
|
});
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(wrapper.vm as any).source = 'prepaid';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).refresh();
|
|
expect(axios.get).toHaveBeenLastCalledWith('/api/billing/charges', {
|
|
params: expect.objectContaining({ charge_source: 'prepaid' }),
|
|
});
|
|
});
|
|
|
|
it('exportCsv fires POST /export with blob response', async () => {
|
|
const wrapper = mount(ChargesTab, {
|
|
global: { plugins: [vuetify], stubs: { RouterLink: true } },
|
|
});
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await (wrapper.vm as any).exportCsv();
|
|
expect(axios.post).toHaveBeenCalledWith(
|
|
'/api/billing/charges/export',
|
|
expect.objectContaining({ period: 'current_month' }),
|
|
expect.objectContaining({ responseType: 'blob' }),
|
|
);
|
|
});
|
|
});
|