793b20a39c
Сверка прототипа с реализацией показала расхождения — закрыты по TDD (dev, фронт):
- F1: экран «Предложения» (FieldProposalsScreen) переписан под вид «Поля» —
карточки-плитки field-shared, тип+«предложение», крупная похожесть, Сайт +
Справочник 2ГИС·Яндекс, править/удалять в карточке, массовый перенос; кнопка
«Собрать конкурентов» открывает единое окно сбора 300 ₽ вместо старого autoform.
- F2: новый дружелюбный админ-экран AdminAutopodborPricingView (правка цен
доп.услуг через PUT /api/admin/system-settings/{key} с обоснованием для аудита,
сетка лидов для справки) + маршрут /admin/autopodbor-pricing + пункт меню.
- F3: колонка «когда списывается» в панели доп.услуг биллинга.
- M2: удалён мёртвый экран FieldManualCompetitorScreen (+ спека) — на него не
было переходов; ручное добавление живёт окном на «Поле».
Тесты автоподбор+админ 43/43 зелёные, продакшен-вёрстка eslint-чистая, vite build ✅.
НЕ на проде. M1 (18:00/21:00 МСК) — не баг, реальный инвариант продукта, не трогал.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
3.4 KiB
TypeScript
82 lines
3.4 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||
import { mount } from '@vue/test-utils';
|
||
import { createVuetify } from 'vuetify';
|
||
|
||
vi.mock('../../resources/js/api/admin');
|
||
import AdminAutopodborPricingView from '../../resources/js/views/admin/AdminAutopodborPricingView.vue';
|
||
import { listSystemSettings, updateSystemSetting, getPricingTiers } from '../../resources/js/api/admin';
|
||
|
||
const vuetify = createVuetify();
|
||
|
||
function settings(search = '300', study = '50') {
|
||
return [
|
||
{ key: 'autopodbor_price_search_rub', value: search, type: 'decimal', description: null, updated_at: '', updated_by: null },
|
||
{ key: 'autopodbor_price_study_rub', value: study, type: 'decimal', description: null, updated_at: '', updated_by: null },
|
||
{ key: 'other_key', value: '1', type: 'int', description: null, updated_at: '', updated_by: null },
|
||
];
|
||
}
|
||
function tiers() {
|
||
return {
|
||
active: [{ tier_no: 1, leads_in_tier: 100, price_per_lead_kopecks: 50000, effective_from: '2026-06-01' }],
|
||
scheduled: {},
|
||
};
|
||
}
|
||
|
||
function mountV() {
|
||
return mount(AdminAutopodborPricingView, { global: { plugins: [vuetify] } });
|
||
}
|
||
|
||
describe('AdminAutopodborPricingView', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
vi.mocked(listSystemSettings).mockResolvedValue(settings() as any);
|
||
vi.mocked(getPricingTiers).mockResolvedValue(tiers() as any);
|
||
vi.mocked(updateSystemSetting).mockResolvedValue({} as any);
|
||
});
|
||
|
||
it('грузит текущие тарифы доп.услуг из system-settings', async () => {
|
||
const w = mountV();
|
||
await new Promise((r) => setTimeout(r, 0));
|
||
expect(listSystemSettings).toHaveBeenCalled();
|
||
expect((w.vm as any).searchPrice).toBe('300');
|
||
expect((w.vm as any).studyPrice).toBe('50');
|
||
});
|
||
|
||
it('показывает сетку лидов для справки', async () => {
|
||
const w = mountV();
|
||
await new Promise((r) => setTimeout(r, 0));
|
||
expect(w.text()).toContain('Тариф на лиды');
|
||
expect(w.text()).toContain('500'); // 50000 коп = 500 ₽
|
||
});
|
||
|
||
it('сохранение изменённой цены зовёт updateSystemSetting с value и reason', async () => {
|
||
const w = mountV();
|
||
await new Promise((r) => setTimeout(r, 0));
|
||
(w.vm as any).searchPrice = '350';
|
||
await (w.vm as any).save();
|
||
expect(updateSystemSetting).toHaveBeenCalledWith(
|
||
'autopodbor_price_search_rub',
|
||
expect.objectContaining({ value: '350' }),
|
||
);
|
||
expect(updateSystemSetting).not.toHaveBeenCalledWith('autopodbor_price_study_rub', expect.anything());
|
||
});
|
||
|
||
it('причина короче 30 символов блокирует сохранение', async () => {
|
||
const w = mountV();
|
||
await new Promise((r) => setTimeout(r, 0));
|
||
(w.vm as any).searchPrice = '350';
|
||
(w.vm as any).reason = 'мало';
|
||
await (w.vm as any).save();
|
||
expect(updateSystemSetting).not.toHaveBeenCalled();
|
||
expect((w.vm as any).errorMessage).toContain('30');
|
||
});
|
||
|
||
it('без изменений не зовёт сохранение', async () => {
|
||
const w = mountV();
|
||
await new Promise((r) => setTimeout(r, 0));
|
||
await (w.vm as any).save();
|
||
expect(updateSystemSetting).not.toHaveBeenCalled();
|
||
expect((w.vm as any).errorMessage).toBeTruthy();
|
||
});
|
||
});
|