From 03f202260fc2fce6bb67de7d6bfab07ba56efff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 25 Jul 2026 21:07:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(=D1=80=D0=B5=D0=BA=D0=BB=D0=B0=D0=BC=D0=B0?= =?UTF-8?q?):=20=D0=BF=D0=BE=D0=BB=D0=B5=20=C2=AB=D0=A6=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=20=D0=BA=D0=BB=D0=B8=D0=BA=C2=BB=20=D0=B2=20?= =?UTF-8?q?=D0=BC=D0=B0=D1=81=D1=82=D0=B5=D1=80=D0=B5=20=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD=D0=B8=D0=B8=20(T13)=20+=20=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=BC=D1=91=D1=80=D1=82=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Шаг 3 «Бюджет» получил поле цены за клик (дефолт 10.00₽, как на бэке в CampaignLauncher), значение уходит в PATCH при сохранении бюджета и префиллится при открытии мастера на правку — иначе правка бюджета у существующей кампании тихо сбрасывала бы её цену за клик на дефолт. Сводка шага 4 показывает цену за клик и ссылку первого объявления. Убран мёртвый блок «Бюджет в день» — поле упразднено ранее, а условие на него всё ещё висело в шаблоне. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/resources/js/api/advertising.ts | 1 + .../components/advertising/CampaignWizard.vue | 30 +++++++++-- .../advertising-campaign-wizard.spec.ts | 54 +++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/app/resources/js/api/advertising.ts b/app/resources/js/api/advertising.ts index 784aebdc..9bb6d45f 100644 --- a/app/resources/js/api/advertising.ts +++ b/app/resources/js/api/advertising.ts @@ -34,6 +34,7 @@ export interface Campaign { */ use_uploaded_list?: boolean; daily_budget_rub?: string | null; + click_bid_rub?: string | null; } /** Ответ GET /api/advertising/campaigns/{id}/audience-size. */ diff --git a/app/resources/js/components/advertising/CampaignWizard.vue b/app/resources/js/components/advertising/CampaignWizard.vue index 3ab2400a..a4230407 100644 --- a/app/resources/js/components/advertising/CampaignWizard.vue +++ b/app/resources/js/components/advertising/CampaignWizard.vue @@ -72,6 +72,7 @@ const form = reactive({ use_uploaded_list: false, weekly_budget_rub: '1000.00', daily_budget_rub: null, + click_bid_rub: '10.00', ads: [], }); @@ -140,6 +141,7 @@ async function loadForEdit(id: number): Promise { form.use_uploaded_list = detail.campaign.use_uploaded_list ?? false; form.weekly_budget_rub = detail.campaign.weekly_budget_rub; form.daily_budget_rub = detail.campaign.daily_budget_rub ?? null; + form.click_bid_rub = detail.campaign.click_bid_rub ?? '10.00'; form.ads = detail.ads ?? []; draftId.value = detail.campaign.id; await refreshAudienceSize(); @@ -200,6 +202,7 @@ async function saveBudgetStep(): Promise { weekly_budget_rub: form.weekly_budget_rub, audience_days: form.audience_days, use_uploaded_list: form.use_uploaded_list, + click_bid_rub: form.click_bid_rub, }); return true; } catch (e) { @@ -404,6 +407,19 @@ defineExpose({ form, draftId, audienceSize, canLaunch, refreshAudienceSize, budg data-testid="budget-weekly" /> + +

{{ budgetError }}

@@ -430,9 +446,17 @@ defineExpose({ form, draftId, audienceSize, canLaunch, refreshAudienceSize, budg formatPlain(Number(form.weekly_budget_rub)) }} - - Бюджет в день - {{ form.daily_budget_rub }} ₽ + + Цена за клик + {{ + formatPlain(Number(form.click_bid_rub)) + }} + + + Сайт + {{ + form.ads[0]?.href + }} diff --git a/app/tests/Frontend/advertising-campaign-wizard.spec.ts b/app/tests/Frontend/advertising-campaign-wizard.spec.ts index a5ab2204..ec2d8974 100644 --- a/app/tests/Frontend/advertising-campaign-wizard.spec.ts +++ b/app/tests/Frontend/advertising-campaign-wizard.spec.ts @@ -226,6 +226,44 @@ describe('CampaignWizard.vue (B2-7 — шаг 3 «Бюджет»)', () => { expect(note.exists()).toBe(true); expect(note.text()).toContain('340'); }); + + it('T13 — на шаге 3 есть поле «Цена за клик» с дефолтом 10.00', async () => { + createCampaignMock.mockResolvedValue(DRAFT_CAMPAIGN); + fetchAudienceSizeMock.mockResolvedValue({ size: 120, min: 100, enough: true, hint: null }); + + const w = mount(CampaignWizard, { global: { plugins: [vuetify] } }); + await flush(); + await w.vm.$nextTick(); + + await gotoStep3(w); + + const clickBidInput = w.find('[data-testid="budget-click-bid"] input'); + expect(clickBidInput.exists()).toBe(true); + expect((clickBidInput.element as HTMLInputElement).value).toBe('10.00'); + }); + + it('T13 — правка цены за клик и «Далее» на шаге 3 уходит в patchCampaign click_bid_rub', async () => { + createCampaignMock.mockResolvedValue(DRAFT_CAMPAIGN); + fetchAudienceSizeMock.mockResolvedValue({ size: 120, min: 100, enough: true, hint: null }); + patchCampaignMock.mockResolvedValue({ ...DRAFT_CAMPAIGN, click_bid_rub: '15.00' }); + + const w = mount(CampaignWizard, { global: { plugins: [vuetify] } }); + await flush(); + await w.vm.$nextTick(); + + await gotoStep3(w); + + const clickBidInput = w.find('[data-testid="budget-click-bid"] input'); + await clickBidInput.setValue('15'); + await w.vm.$nextTick(); + + await w.find('[data-testid="wizard-next"]').trigger('click'); + await flush(); + await w.vm.$nextTick(); + + expect(patchCampaignMock).toHaveBeenCalledWith(99, expect.objectContaining({ click_bid_rub: '15' })); + expect(w.find('[data-testid="wizard-step-4"]').exists()).toBe(true); + }); }); describe('CampaignWizard.vue (B2-8 — шаг 4 «Проверка и запуск»)', () => { @@ -330,6 +368,22 @@ describe('CampaignWizard.vue (B2-8 — шаг 4 «Проверка и запус expect(errorEl.text()).toContain('Аудитория слишком мала для запуска.'); expect(w.emitted('launched')).toBeFalsy(); }); + + it('T13 — сводка шага 4 показывает цену за клик', async () => { + createCampaignMock.mockResolvedValue(DRAFT_CAMPAIGN); + fetchAudienceSizeMock.mockResolvedValue({ size: 120, min: 100, enough: true, hint: null }); + patchCampaignMock.mockResolvedValue({ ...DRAFT_CAMPAIGN, weekly_budget_rub: '1000.00' }); + + const w = mount(CampaignWizard, { global: { plugins: [vuetify] } }); + await flush(); + await w.vm.$nextTick(); + + await gotoStep4(w); + + const summary = w.find('[data-testid="launch-summary"]'); + expect(summary.text()).toContain('Цена за клик'); + expect(summary.text()).toContain('10'); + }); }); describe('CampaignWizard.vue (B2-9 — правка на ходу, Р30)', () => {