Files
portal/app/resources/js/views/admin/AdminTgView.vue
T
Дмитрий 6f469fb13d feat(телеграм-реклама): деньги на общем балансе + чистка СМС-фантазий + медиа объявления
Денежная модель (куски 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>
2026-07-29 06:26:07 +03:00

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>