docs: spec SMS campaign people/leads tabs and batch send

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-17 20:47:48 +03:30
co-authored by Cursor
parent f7b07f6b7a
commit 32c722f42a
@@ -0,0 +1,84 @@
# پیامک گروهی: تب اشخاص/لیدها، فیلتر کامل، انتخاب‌همه، بچ ۵۰
تاریخ: ۲۰۲۶-۱۰-۱۷
وضعیت: تأیید شده؛ آمادهٔ پلن پیاده‌سازی.
## هدف
صفحهٔ پیامک گروهی بتواند مخاطب را از **اشخاص** یا **لیدها** (جدا از هم) با فیلترهای واقعی (از جمله استان/شهر) انتخاب کند، همهٔ نتایج فیلتر را یکجا انتخاب کند، متن را با فیلدهای پویا بنویسد، و ساخت/ارسال را در تکه‌های ۵۰تایی انجام دهد.
## تصمیم‌های تأییدشده
| موضوع | تصمیم |
| --- | --- |
| نوع مخاطب در یک کمپین | فقط اشخاص **یا** فقط لیدها (تب جدا؛ نه مخلوط) |
| انتخاب‌همه | همهٔ نتایج مطابق فیلتر در کل دیتابیس (با تأیید تعداد) |
| بچ | ارسال سرور ۵۰تایی؛ ساخت کمپین هم در صورت نیاز تکه‌تکهٔ ۵۰تایی |
| فیلتر مکان اشخاص | از `company.province` / `company.city` + جستجو در آدرس‌ها (بدون ستون استان روی Person) |
| فیلتر مکان لیدها | `city` و `region` (+ بقیهٔ فیلترهای موجود لیست لید) |
| فیلدهای پویا | همان توکن‌های اتوماسیون پیام: `{نام}`، `{شماره}`، `{تاریخ}`، `{عنوان}`، `{شرکت}` |
## UX
1. دو تب روی فرم کمپین جدید: **اشخاص** | **لیدها**.
2. تعویض تب با انتخاب موجود → تأیید و پاک شدن انتخاب.
3. لیست صفحه‌بندی‌شده با جستجو و فیلترهای هم‌سطح صفحات اصلی هر بخش.
4. نمایش تعداد انتخاب‌شده؛ دکمهٔ «انتخاب همهٔ نتایج فیلتر» → دریافت `total`/`ids` → تأیید → جایگزینی مجموعهٔ انتخاب.
5. بالای textarea دکمهٔ **فیلدهای پویا** (الگوی UI اتوماسیون پیام) برای درج توکن در متن.
6. پس از ذخیره/صف، لیست کمپین‌ها مثل قبل؛ گزارش کلیک اگر از قبل برای لید/شخص آماده است همان مسیر را می‌رود.
## API
### فیلتر لیست
- گسترش `GET /people` با فیلترهای کمپین: حداقل `province`, `city` (از شرکت)، و فیلترهای موجود (`q`, `mood`, `importSource`, …).
- اطمینان از پوشش فیلترهای لید برای کمپین: `q`, `city`, `region`, `stageId`, و فیلترهای موجود دیگر لیست لید.
### شناسه‌های فیلترشده
- `GET /people/ids` — همان queryهای لیست؛ پاسخ `{ ids: string[], total: number }`.
- `GET /leads/ids` — همان برای لید.
- هر دو با `ownerWhere` / scope بازیگر.
- سقف سخت برای جلوگیری از پاسخ غول‌آسا (مثلاً ۵۰٬۰۰۰)؛ بیش از سقف → خطای فارسی با پیشنهاد محدود کردن فیلتر.
### ساخت و تکمیل کمپین
- `CreateSmsCampaignRequest`:
- `name`, `body`, `queueNow?`, `scheduledAt?`
- دقیقاً یکی از `personIds` یا `leadIds` (آرایهٔ غیرخالی).
- ساخت اولیه حداکثر ۵۰ گیرنده؛ اگر فرانت بیشتر دارد، بعد از ساخت:
- `POST /sms-campaigns/:id/recipients` با `{ personIds }` یا `{ leadIds }` هم‌نوع با کمپین؛ هر درخواست حداکثر ۵۰ id.
- تا پایان تکه‌ها کمپین `draft` می‌ماند؛ سپس در صورت `queueNow` صف می‌شود (یا کاربر دستی queue می‌زند).
- گیرنده بدون موبایل → `skipped`؛ شمارنده‌ها به‌روز می‌شوند.
### ارسال
- در `processCampaign` مقدار `take` بچ از ۴۰ به **۵۰**.
- قبل از `sendSmsDetailed`: اعمال `renderSmsText` روی body با متغیرهای گیرنده (شخص یا لید)، سپس `personalizeSmsBody` برای لینک یکتا (ترتیب: ابتدا توکن‌های پویا، بعد لینک‌ها).
## داده
- مدل `SmsCampaignRecipient` از قبل `personId` و `leadId` دارد؛ migration جدید لازم نیست مگر برای ایندکس/قید اختیاری.
- کمپین صریحاً audience را از روی گیرندگان مشخص می‌کند؛ نیازی به ستون `audienceType` نیست اگر validation در create/add-recipients نوع را یکدست نگه دارد.
## خطاها
- هر دو `personIds` و `leadIds` با هم → `400`.
- افزودن نوع مخالف به کمپین موجود → `400`.
- قطع میانه‌راه تکه‌ها → کمپین `draft` با گیرندگان اضافه‌شده؛ پیام تعداد موفق/ناموفق.
- ماژول غیرفعال → همان `403` فعلی.
## خارج از محدوده
- کمپین ترکیبی اشخاص+لیدها
- ستون استان/شهر مستقیم روی Person
- ویرایش متن بعد از شروع ارسال
- زمان‌بندی تقویمی جدید فراتر از `scheduledAt` فعلی
## معیار پذیرش
1. تب لید و تب شخص جدا؛ یک کمپین فقط یکی را می‌پذیرد.
2. فیلتر استان/شهر (و فیلترهای کلیدی دیگر) کار می‌کند؛ انتخاب‌همه همهٔ نتایج فیلتر را می‌گیرد نه فقط صفحه.
3. فیلدهای پویا در UI درج می‌شوند و در SMS واقعی جایگزین می‌گردند.
4. ساخت لیست بزرگ در تکه‌های ۵۰؛ ارسال صف هم ۵۰تایی است.
)