6f469fb13d
Денежная модель (куски 1-2): реклама в Телеграме (МТС Маркетолог, робот-в-браузере)
оплачивается с ОБЩЕГО баланса тенанта (как СМС), наценка 40%. Списание при запуске,
возврат при отказе модерации/сбое/отмене (сальдо-идемпотентность tg_ad_charge/tg_ad_refund).
Старый рекламный кошелёк-заморозка убран из потока кампаний (таблицы ad_wallets НЕ удалены).
Новый TelegramCampaignChargeService (зеркало SmsChargeService); F5 money-safety сохранён
(сбой с mts_campaign_id → needs_review без возврата). Уборщик добивает зависшие queued с возвратом.
Чистка СМС-наследия (аудит модуля построчно 5 проверяющими): модуль ресейлит ПОКАЗЫ рекламы,
а не рассылку сообщений — вырезаны выдуманные сущности, механически скопированные из СМС:
- «имя/бренд отправителя» + помесячная абонплата (в телеге МТС имени отправителя нет):
ChargeTgNameFeeJob, TelegramSenderService, SenderController, модели Sender/Setting,
фронт-панель TelegramSenderPanel, админ-карточка, updateTgSettings, тип tg_name_fee.
Таблицы client_tg_senders/_settings дропнуты.
- мёртвые таблицы client_tg_messages (доставка по номеру) и client_tg_templates (шаблоны
сообщений) — без модели/использования, дропнуты (миграция 000017, create-миграции удалены).
- client_tg_optouts переосмыслен как ручной список «не показывать этим номерам» (не «отписки»).
- лексика рассылки → рекламы: «Авторассылка»→«Авто-реклама», «кошелёк/заморожено»→«общий баланс».
Медиа объявления (реальный пробел показов): новый POST /api/telegram/campaigns/{id}/media
(картинка/видео к черновику, png/jpg/gif/mp4 ≤50 МБ, замена удаляет старый файл) + поле
загрузки в форме кабинета. Робот уже принимал media_path (RunTelegramCampaignJob → mediaFile
→ cabinet.js fillAdMedia) — не хватало только приёма файла от клиента.
Схема: db/CHANGELOG_schema.md v8.94 (CHECK += tg_ad_charge/tg_ad_refund) + v8.95 (дроп
СМС-таблиц, tg_name_fee убран). Тесты зелёные поштучно (квирк партиций — гонять по одному):
Schema/Models/AdminTg/CampaignCharge/CampaignApi/Audience/Media; фронт advertising-telegram-view
19 + admin-tg/auto-rule 24. pint/deptrac(0) чисто; larastan исключён (Pest-$this шум). Робот
bots/mts-telegram-ads НЕ трогали.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
4.3 KiB
Vue
126 lines
4.3 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* SaaS-admin → «Клиентская Telegram-реклама»: тарифная сетка (ступени ₽ за показ).
|
|
* План §Сессия 4, задача 4.4. Лаконичное зеркало AdminSmsView.
|
|
*/
|
|
import { onMounted, ref } from 'vue';
|
|
import { fetchTgTariffs, updateTgTariffs, type AdminTgTariff } from '../../api/admin';
|
|
|
|
const tariffs = ref<AdminTgTariff[]>([]);
|
|
|
|
const loadError = ref(false);
|
|
const tariffsSaving = ref(false);
|
|
const tariffsSaved = ref(false);
|
|
|
|
async function load(): Promise<void> {
|
|
loadError.value = false;
|
|
try {
|
|
const data = await fetchTgTariffs();
|
|
tariffs.value = data.tariffs;
|
|
} catch {
|
|
loadError.value = true;
|
|
}
|
|
}
|
|
|
|
function addRow(): void {
|
|
tariffs.value.push({ id: 0, min_qty: 1, price_rub: '0.00' });
|
|
}
|
|
|
|
function removeRow(index: number): void {
|
|
tariffs.value.splice(index, 1);
|
|
}
|
|
|
|
async function saveTariffs(): Promise<void> {
|
|
tariffsSaving.value = true;
|
|
tariffsSaved.value = false;
|
|
try {
|
|
const rows = tariffs.value.map((t) => ({
|
|
min_qty: Number(t.min_qty),
|
|
price_rub: String(t.price_rub),
|
|
}));
|
|
const res = await updateTgTariffs(rows);
|
|
tariffs.value = res.tariffs;
|
|
tariffsSaved.value = true;
|
|
} finally {
|
|
tariffsSaving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(load);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-tg">
|
|
<h1 class="text-h5 mb-4">Клиентская Telegram-реклама</h1>
|
|
|
|
<v-alert
|
|
v-if="loadError"
|
|
type="error"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
data-testid="tg-tariffs-error"
|
|
>
|
|
Не удалось загрузить тарифы. Обновите страницу.
|
|
</v-alert>
|
|
|
|
<!-- Тарифная сетка: ступени ₽ за показ по объёму. -->
|
|
<v-card class="mb-6" data-testid="tg-tariffs-card">
|
|
<v-card-title>Ступени тарифа (₽ за показ)</v-card-title>
|
|
<v-card-text>
|
|
<p class="text-body-2 text-medium-emphasis mb-3">
|
|
Итоговую стоимость показа считает кабинет МТС — эта сетка нужна для сметы
|
|
в интерфейсе клиента и лимита на объявление.
|
|
</p>
|
|
<div
|
|
v-for="(row, i) in tariffs"
|
|
:key="i"
|
|
class="d-flex align-center ga-3 mb-2"
|
|
:data-testid="`tg-tariff-row-${i}`"
|
|
>
|
|
<v-text-field
|
|
v-model.number="row.min_qty"
|
|
label="От объёма (шт.)"
|
|
type="number"
|
|
density="compact"
|
|
hide-details
|
|
style="max-width: 180px"
|
|
/>
|
|
<v-text-field
|
|
v-model="row.price_rub"
|
|
label="₽ за показ"
|
|
density="compact"
|
|
hide-details
|
|
style="max-width: 160px"
|
|
/>
|
|
<v-btn
|
|
icon="mdi-delete-outline"
|
|
variant="text"
|
|
size="small"
|
|
:data-testid="`tg-tariff-remove-${i}`"
|
|
@click="removeRow(i)"
|
|
/>
|
|
</div>
|
|
<v-btn
|
|
variant="text"
|
|
prepend-icon="mdi-plus"
|
|
data-testid="tg-tariff-add"
|
|
@click="addRow"
|
|
>
|
|
Добавить ступень
|
|
</v-btn>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-btn
|
|
color="primary"
|
|
:loading="tariffsSaving"
|
|
data-testid="tg-tariffs-save"
|
|
@click="saveTariffs"
|
|
>
|
|
Сохранить тарифы
|
|
</v-btn>
|
|
<span v-if="tariffsSaved" class="text-success text-body-2 ml-3">Сохранено</span>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</div>
|
|
</template>
|