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)', () => {