Files
portal/app/tests/Frontend/AdminPricingTiersView.spec.ts
T
Дмитрий ed5e3f495d feat(admin): Plan 4 Task 9 — AdminPricingTiersController + AdminPricingTiersView (CRUD 7-tier + audit)
Backend AdminPricingTiersController:
- GET /api/admin/pricing-tiers — active + scheduled.
- POST — create 7-tier set с effective_from=DATE_TRUNC('month', NOW()+1 month).
- DELETE /scheduled/{date} — отмена будущей сетки.
- Validation: ровно 7 tier_no 1..7 unique, tier 7 leads_in_tier=null, price>=0.
- Audit trail saas_admin_audit_log на POST + DELETE (через SaasAdminAuditLog
  model: payload_before/after, NOT NULL admin_user_id резолвится через стаб
  system-pricing@liderra.local + ip_address из $request->ip()).
- 8 Pest integration tests.

Frontend AdminPricingTiersView (Vue 3 + Vuetify 3):
- v-data-table активной сетки + scheduled groups + dialog editor.
- Forest-palette + JetBrains Mono для tnum-цифр.
- 5 Vitest unit tests (tests/Frontend/, авто-импорт Vuetify через vite-plugin).
- Histoire story для preview.

Router /admin/pricing-tiers route (layout 'admin', requiresAuth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:18:01 +03:00

81 lines
4.0 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 AdminPricingTiersView from '../../resources/js/views/admin/AdminPricingTiersView.vue';
vi.mock('axios');
// Auto-импорт компонентов/директив Vuetify подхватывает vite-plugin-vuetify
// из vitest.config.ts (см. AdminBillingView.spec.ts).
const vuetify = createVuetify();
const mockTiers = [
{ tier_no: 1, leads_in_tier: 100, price_per_lead_kopecks: 50000, effective_from: '1970-01-01' },
{ tier_no: 2, leads_in_tier: 200, price_per_lead_kopecks: 45000, effective_from: '1970-01-01' },
{ tier_no: 3, leads_in_tier: 400, price_per_lead_kopecks: 40000, effective_from: '1970-01-01' },
{ tier_no: 4, leads_in_tier: 800, price_per_lead_kopecks: 35000, effective_from: '1970-01-01' },
{ tier_no: 5, leads_in_tier: 1500, price_per_lead_kopecks: 30000, effective_from: '1970-01-01' },
{ tier_no: 6, leads_in_tier: 3000, price_per_lead_kopecks: 27000, effective_from: '1970-01-01' },
{ tier_no: 7, leads_in_tier: null, price_per_lead_kopecks: 25000, effective_from: '1970-01-01' },
];
describe('AdminPricingTiersView', () => {
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(axios.get as any).mockResolvedValue({ data: { data: { active: mockTiers, scheduled: {} } } });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(axios.post as any).mockResolvedValue({ data: { effective_from: '2026-06-01' } });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(axios.delete as any).mockResolvedValue({ data: { ok: true } });
});
it('renders 7 tier rows from /api/admin/pricing-tiers', async () => {
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
await new Promise((r) => setTimeout(r, 50));
expect(wrapper.text()).toContain('500.00');
expect(wrapper.text()).toContain('250.00');
});
it('shows "все свыше" for tier 7 with leads_in_tier=null', async () => {
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
await new Promise((r) => setTimeout(r, 50));
expect(wrapper.text()).toContain('все свыше');
});
it('opens editor dialog on button click', async () => {
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
await new Promise((r) => setTimeout(r, 50));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((wrapper.vm as any).editorOpen).toBe(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(wrapper.vm as any).editorOpen = true;
await wrapper.vm.$nextTick();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((wrapper.vm as any).editorOpen).toBe(true);
});
it('submits POST with editor payload', async () => {
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
await new Promise((r) => setTimeout(r, 50));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (wrapper.vm as any).submit();
expect(axios.post).toHaveBeenCalledWith(
'/api/admin/pricing-tiers',
expect.objectContaining({
tiers: expect.arrayContaining([expect.objectContaining({ tier_no: 7, leads_in_tier: null })]),
}),
);
});
it('confirmDelete triggers DELETE to /scheduled/{date}', async () => {
window.confirm = vi.fn(() => true);
const wrapper = mount(AdminPricingTiersView, { global: { plugins: [vuetify] } });
await new Promise((r) => setTimeout(r, 50));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (wrapper.vm as any).confirmDelete('2026-06-01');
expect(axios.delete).toHaveBeenCalledWith('/api/admin/pricing-tiers/scheduled/2026-06-01');
});
});