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
@@ -0,0 +1,60 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BroadcastStatus" AS ENUM ('SENDING', 'DONE', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BroadcastRecipientStatus" AS ENUM ('PENDING', 'SENT', 'BLOCKED', 'FAILED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "PromoCode" ADD COLUMN "categories" "CategoryCode"[] DEFAULT ARRAY[]::"CategoryCode"[],
|
||||
ADD COLUMN "note" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TextOverride" (
|
||||
"key" TEXT NOT NULL,
|
||||
"locale" "Locale" NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"updatedById" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TextOverride_pkey" PRIMARY KEY ("key","locale")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Broadcast" (
|
||||
"id" TEXT NOT NULL,
|
||||
"texts" JSONB NOT NULL,
|
||||
"appButton" BOOLEAN NOT NULL DEFAULT true,
|
||||
"filters" JSONB NOT NULL,
|
||||
"status" "BroadcastStatus" NOT NULL DEFAULT 'SENDING',
|
||||
"total" INTEGER NOT NULL DEFAULT 0,
|
||||
"sent" INTEGER NOT NULL DEFAULT 0,
|
||||
"blocked" INTEGER NOT NULL DEFAULT 0,
|
||||
"failed" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Broadcast_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BroadcastRecipient" (
|
||||
"id" TEXT NOT NULL,
|
||||
"broadcastId" TEXT NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"status" "BroadcastRecipientStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"error" TEXT,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "BroadcastRecipient_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "BroadcastRecipient_broadcastId_status_idx" ON "BroadcastRecipient"("broadcastId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BroadcastRecipient_broadcastId_clientId_key" ON "BroadcastRecipient"("broadcastId", "clientId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BroadcastRecipient" ADD CONSTRAINT "BroadcastRecipient_broadcastId_fkey" FOREIGN KEY ("broadcastId") REFERENCES "Broadcast"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -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