Backoffice stage 5: products, broadcasts, activity log, settings

- Products: services with full setup — content (name, description, extras names
  in 5 languages via text overrides), price tiers and extras, booking window,
  late-cancel fee, on/off, checklist; category discounts by membership level;
  membership plan prices; promo codes; relocation steps and documents.
- Text overrides: TextOverride table on top of the i18n files; API, bots and the
  worker reload them every minute, mini apps fetch public.texts at start.
- Promo codes applied to orders (admin new-order form and client API): not
  stacked with membership discount — the larger wins; once per client; validity,
  limit and categories.
- Broadcasts to the client bot by any client filter, per-language texts with
  Russian fallback, test to yourself, ~25 msg/s, per-recipient status, resume after
  worker restart, history with progress and stop.
- Activity log (who/when/what, filters, details) and Settings page with all
  owner settings grouped, plus payment methods.
- Tests for promo codes and text overrides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 03:17:56 +04:00
1 parent ac786cc5cc
commit 0edc6432b7
34 files changed
+2704 -26

No files matched your search

+66
View File
@@ -706,6 +706,10 @@ model PromoCode {
validFrom DateTime?
validUntil DateTime?
active Boolean @default(true)
/// для команды: откуда код (рассылка, блогер, партнёр)
note String?
/// на какие направления действует; пусто — на все
categories CategoryCode[] @default([])
createdAt DateTime @default(now())
uses PromoCodeUse[]
@@ -1369,3 +1373,65 @@ model SalesTask {
@@index([assigneeId, doneAt, dueAt])
}
// ───────────── Продукты, рассылки (этап 5) ─────────────
/// Текст, изменённый в админке поверх переводов из packages/i18n (название и описание услуги, пункт чек-листа…).
/// Пустой — не храним: действует стандартный перевод
model TextOverride {
key String
locale Locale
value String
updatedById String?
updatedAt DateTime @updatedAt
@@id([key, locale])
}
enum BroadcastStatus {
SENDING
DONE
CANCELLED
}
enum BroadcastRecipientStatus {
PENDING
SENT
/// заблокировал бота или ни разу его не открывал
BLOCKED
FAILED
}
/// Рассылка клиентам в бот по сегменту (фильтры списка клиентов). Текст — на языке клиента, фолбэк — русский
model Broadcast {
id String @id @default(cuid())
/// { ru, en, zh, hy, es }
texts Json
/// кнопка «Открыть приложение» под сообщением
appButton Boolean @default(true)
/// фильтры списка клиентов, по которым собрали получателей
filters Json
status BroadcastStatus @default(SENDING)
total Int @default(0)
sent Int @default(0)
blocked Int @default(0)
failed Int @default(0)
createdById String?
createdAt DateTime @default(now())
finishedAt DateTime?
recipients BroadcastRecipient[]
}
model BroadcastRecipient {
id String @id @default(cuid())
broadcastId String
broadcast Broadcast @relation(fields: [broadcastId], references: [id], onDelete: Cascade)
clientId String
status BroadcastRecipientStatus @default(PENDING)
error String?
sentAt DateTime?
@@unique([broadcastId, clientId])
@@index([broadcastId, status])
}