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:
1 parent
ac786cc5cc
commit
0edc6432b7
34 files changed
+2704
-26
No files matched your search
@@ -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])
|
||||
}
|
||||
Reference in new issue
Block a user