630 lines
36 KiB
Markdown
630 lines
36 KiB
Markdown
# سناریو: اشتراک پشتیبانی کاربر (پرداخت مستقیم درگاه — بدون کسبوکار و بدون کیفپول)
|
||
|
||
## ۱. خلاصه اجرایی
|
||
|
||
بخش تیکتهای پشتیبانی امروز برای همهٔ کاربران (در صورت فعال بودن سوییچ سیستم) **رایگان** است و هیچ مدل قیمتگذاری، اشتراک، صورتحساب یا گزارش درآمد ندارد.
|
||
|
||
این سناریو لایهٔ **اشتراک پشتیبانی در سطح کاربر (User)** را اضافه میکند تا:
|
||
|
||
- پشتیبانی بتواند **رایگان**، **پولی**، یا **ترکیبی (سهمیه رایگان + پولی)** باشد.
|
||
- کاربر بتواند پلنهای **۱ ماهه / ۳ ماهه / ۶ ماهه / ۱۲ ماهه** بخرد.
|
||
- مدیر سیستم قیمتها، پلنها و قوانین دسترسی را کنترل کند.
|
||
- پرداخت **مستقیم از درگاه بانکی ثبتشده در تنظیمات سیستم** انجام شود (هدایت کاربر به سایت بانک/درگاه).
|
||
- **سوابق صورتحساب** برای کاربر قابل مشاهده باشد.
|
||
- در پنل مدیریت، **گزارش آماری** درآمد و اشتراک اضافه شود.
|
||
- ماژول تیکت فعلی (پیام، SLA، اپراتور، CSAT، پیوست، realtime) دست نخورده بماند و فقط **گیت دسترسی** روی آن سوار شود.
|
||
|
||
### اصول قطعی (غیرقابل مذاکره در این سناریو)
|
||
|
||
| اصل | توضیح |
|
||
|-----|--------|
|
||
| بدون کسبوکار | هیچ `business_id` در خرید، اشتراک، صورتحساب، callback یا entitlement وجود ندارد. |
|
||
| بدون کیفپول | هیچ شارژ/برداشت از `Wallet`، `WalletTransaction` نوع top-up/service، یا موجودی کسبوکار انجام نمیشود. |
|
||
| پرداخت مستقیم | پس از انتخاب پلن، بلافاصله درخواست پرداخت روی درگاه سیستم ساخته میشود و کاربر به URL درگاه هدایت میشود. |
|
||
| مالکیت کاربر | اشتراک و صورتحساب متعلق به `user_id` است (همان کاربری که تیکت میسازد). |
|
||
| درگاه سیستم | فقط از `payment_gateways` فعال در مدیریت سیستم استفاده میشود؛ `business_payment_gateways` اصلاً دخیل نیست. |
|
||
|
||
---
|
||
|
||
## ۲. اهداف و غیرهدفها
|
||
|
||
### اهداف
|
||
|
||
1. کنترل حالت پشتیبانی (رایگان / پولی / ترکیبی) توسط ادمین.
|
||
2. CRUD پلنهای پشتیبانی با مدت و قیمت.
|
||
3. خرید مستقیم با درگاه (زرینپال / پارسیان / بیتپی — همان ارائهدهندگان فعلی سیستم).
|
||
4. فعالسازی اشتراک فقط پس از تأیید موفق callback درگاه.
|
||
5. نمایش و دانلود/جزئیات سوابق صورتحساب برای کاربر.
|
||
6. مسدودسازی ایجاد/پاسخ تیکت وقتی entitlement فعال نیست (بسته به حالت سیستم).
|
||
7. گزارش آماری مدیریتی (درآمد، اشتراک فعال، تبدیل، انقضا، CSAT تفکیکشده).
|
||
8. اعلانهای انقضا، grace period، تمدید، و تمایز SLA/اولویت بر اساس پلن (پیشنهادی ضروری).
|
||
|
||
### غیرهدفها (عمداً خارج از اسکوپ)
|
||
|
||
- اتصال به کیفپول کسبوکار یا فاکتور حسابداری کسبوکار.
|
||
- فروش پشتیبانی بهصورت لایسنس سازمانی چندکاربره (میتواند فاز بعد باشد).
|
||
- تغییر هستهٔ اپراتور/SLA/پیامرسانی تیکت مگر برای گیت entitlement و متادیتای پلن.
|
||
- پرداخت چندمرحلهای اقساطی.
|
||
|
||
---
|
||
|
||
## ۳. واژهنامه
|
||
|
||
| اصطلاح | معنی |
|
||
|--------|------|
|
||
| **حالت پشتیبانی (Support Mode)** | تنظیم سراسری: `free` / `paid` / `hybrid` |
|
||
| **پلن پشتیبانی (Support Plan)** | بستهٔ قابل فروش با `period_months` و `price` |
|
||
| **اشتراک (Subscription)** | entitlement زمانی کاربر روی یک پلن (`starts_at` … `ends_at`) |
|
||
| **صورتحساب پشتیبانی (Support Invoice)** | سند مالی کاربر برای یک خرید/تمدید؛ مستقل از فاکتور حسابداری کسبوکار |
|
||
| **نشست پرداخت (Payment Session)** | رکورد موقت/دائمی برای ردیابی درخواست درگاه تا verify |
|
||
| **سهمیه رایگان (Free Quota)** | در حالت hybrid: تعداد تیکت قابل ایجاد در بازهٔ زمانی بدون اشتراک |
|
||
| **Grace Period** | مهلت کوتاه بعد از انقضا که هنوز دسترسی خواندن/پاسخ محدود برقرار است |
|
||
| **Entitlement** | نتیجهٔ محاسبه: آیا کاربر الان حق ایجاد/پاسخ تیکت دارد یا نه |
|
||
|
||
---
|
||
|
||
## ۴. حالتهای سیستم (ادمین)
|
||
|
||
کلید جدید در تنظیمات سیستم (کنار `support_tickets_enabled`):
|
||
|
||
### ۴.۱. `support_billing_mode`
|
||
|
||
| مقدار | رفتار |
|
||
|-------|--------|
|
||
| `free` | مثل امروز: همه با تیکتینگ فعال میتوانند تیکت بسازند/پاسخ دهند. پلنها فقط برای نمایش/آینده اختیاریاند؛ خرید اجباری نیست. |
|
||
| `paid` | بدون اشتراک فعال (یا grace معتبر)، ایجاد تیکت و پاسخ کاربر **ممنوع**. مشاهده تیکتهای قبلی مجاز است (read-only). |
|
||
| `hybrid` | تا سقف سهمیه رایگان ماهانه میتوان تیکت ساخت؛ بیش از سقف یا برای ویژگیهای پلن پولی، نیاز به اشتراک. |
|
||
|
||
### ۴.۲. سایر تنظیمات سراسری
|
||
|
||
| کلید | نوع | پیشفرض پیشنهادی | توضیح |
|
||
|------|-----|------------------|--------|
|
||
| `support_tickets_enabled` | bool | موجود | اگر خاموش باشد، کل تیکتینگ کاربر بسته است (اپراتور باز میماند). |
|
||
| `support_tickets_disabled_message` | string | موجود | پیام نمایش به کاربر. |
|
||
| `support_billing_mode` | enum | `free` | بالا. |
|
||
| `support_free_quota_per_month` | int | `2` | فقط در `hybrid`: تعداد تیکت جدید قابل ایجاد در هر ماه تقویمی شمسی/میلادی (قابل انتخاب؛ پیشفرض میلادی UTC یا تقویم سیستم). |
|
||
| `support_grace_period_days` | int | `3` | بعد از `ends_at`؛ در این بازه پاسخ به تیکتهای باز مجاز، ایجاد تیکت جدید خیر (قابل تنظیم دقیقتر در بخش قوانین). |
|
||
| `support_allow_read_without_subscription` | bool | `true` | در حالت paid/hybrid بدون اشتراک، لیست و جزئیات تیکتهای قبلی قابل مشاهده باشد. |
|
||
| `support_require_subscription_to_reply` | bool | `true` | در paid: بدون اشتراک حتی پاسخ هم بسته شود. در hybrid: اگر سهمیه تمام شده و اشتراک ندارد، پاسخ بسته شود (قابل پیکربندی). |
|
||
| `support_default_gateway_id` | int\|null | اولین درگاه فعال سیستم | درگاه پیشفرض برای خرید پشتیبانی. اگر null و چند درگاه فعال باشد، کاربر از بین درگاههای سیستم انتخاب میکند. |
|
||
| `support_invoice_prefix` | string | `SUP` | پیشوند کد صورتحساب (`SUP-1405-000123`). |
|
||
| `support_expiry_notify_days` | int[] | `[7, 3, 1]` | روزهای قبل از انقضا برای اعلان. |
|
||
| `support_paid_priority_boost` | bool | `true` | تیکت کاربران دارای اشتراک پولی، در صف اپراتور با نشان/اولویت نسبی بالاتر نمایش داده شود (بدون شکستن SLA policy موجود مگر پلن صریحاً SLA داشته باشد). |
|
||
|
||
**نکته مهاجرت:** مقدار پیشفرض `support_billing_mode = free` است تا رفتار فعلی سیستم بعد از دیپلوی تغییر نکند.
|
||
|
||
---
|
||
|
||
## ۵. مدل داده
|
||
|
||
همه جداول جدید **بدون FK به businesses**.
|
||
|
||
### ۵.۱. `support_plans`
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|------|-----|--------|
|
||
| `id` | PK | |
|
||
| `name` | string(200) | نام نمایشی |
|
||
| `code` | string(100) unique | مثلاً `support_1m`, `support_3m` |
|
||
| `period_months` | int | فقط یکی از: `1`, `3`, `6`, `12` (در فاز ۱) |
|
||
| `price` | numeric(18,2) | مبلغ قابل پرداخت به ریال/واحد ارز سیستم |
|
||
| `currency_id` | FK currencies | اجباری |
|
||
| `is_free` | bool | پلن رایگان (قیمت ۰)؛ فعالسازی بدون درگاه |
|
||
| `is_active` | bool | قابل خرید بودن |
|
||
| `sort_order` | int | ترتیب نمایش کارتها |
|
||
| `description` | text\|null | توضیح کوتاه برای کاربر |
|
||
| `features_json` | JSON\|null | لیست بولت ویژگیها برای UI |
|
||
| `max_open_tickets` | int\|null | سقف تیکت باز همزمان؛ null = نامحدود |
|
||
| `sla_policy_id` | FK\|null | اختیاری: اتصال به SLA خاص این پلن |
|
||
| `priority_weight` | int | پیشفرض `0`؛ برای boost در لیست اپراتور |
|
||
| `includes_priority_support` | bool | نشان «پشتیبانی اولویتدار» در UI |
|
||
| `trial_days` | int | پیشفرض `0`؛ اگر >0 و کاربر هرگز اشتراک نداشته، میتواند trial بگیرد (پیشنهادی) |
|
||
| `created_at` / `updated_at` | datetime | |
|
||
|
||
**Seed پیشنهادی اولیه (غیرفعال تا ادمین قیمت بگذارد یا با قیمت ۰ و is_free):**
|
||
|
||
- پشتیبانی ۱ ماهه (`period_months=1`)
|
||
- پشتیبانی ۳ ماهه (`period_months=3`)
|
||
- پشتیبانی ۶ ماهه (`period_months=6`)
|
||
- پشتیبانی ۱۲ ماهه (`period_months=12`)
|
||
|
||
### ۵.۲. `support_subscriptions`
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|------|-----|--------|
|
||
| `id` | PK | |
|
||
| `user_id` | FK users CASCADE | مالک |
|
||
| `plan_id` | FK support_plans RESTRICT | پلن خریداریشده |
|
||
| `status` | string | `pending` / `active` / `grace` / `expired` / `cancelled` / `replaced` |
|
||
| `starts_at` | datetime | |
|
||
| `ends_at` | datetime\|null | برای پلنهای مدتدار اجباری |
|
||
| `grace_ends_at` | datetime\|null | `ends_at + grace_period_days` |
|
||
| `auto_renew` | bool | پیشفرض `false`؛ فاز ۱ میتواند UI داشته باشد ولی تمدید خودکار فقط اگر درگاه توکن ذخیره کند — **در فاز ۱ توصیه: فقط تمدید دستی** |
|
||
| `source_invoice_id` | FK support_invoices\|null | صورتحسابی که این اشتراک را فعال کرد |
|
||
| `cancelled_at` | datetime\|null | |
|
||
| `cancel_reason` | string\|null | |
|
||
| `created_at` / `updated_at` | datetime | |
|
||
|
||
**قواعد:**
|
||
|
||
- در هر لحظه حداکثر **یک** اشتراک `active` (یا `grace`) برای هر کاربر.
|
||
- خرید جدید در حین اشتراک فعال = **تمدید/ارتقا** (بخش ۷.۴).
|
||
- `pending` فقط اگر بخواهیم قبل از پرداخت رکورد بسازیم؛ ترجیح: اول invoice + payment_session، بعد از verify اشتراک `active` ساخته/تمدید شود.
|
||
|
||
### ۵.۳. `support_invoices`
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|------|-----|--------|
|
||
| `id` | PK | |
|
||
| `user_id` | FK users | |
|
||
| `plan_id` | FK support_plans | |
|
||
| `code` | string(50) unique | مثلاً `SUP-20260721-000042` |
|
||
| `invoice_type` | string | `purchase` / `renewal` / `upgrade` / `trial` |
|
||
| `amount` | numeric(18,2) | مبلغ نهایی پرداختشده/قابل پرداخت |
|
||
| `discount_amount` | numeric(18,2) | پیشفرض ۰ |
|
||
| `currency_id` | FK | |
|
||
| `status` | string | `draft` / `awaiting_payment` / `paid` / `failed` / `expired` / `void` / `refunded` |
|
||
| `issued_at` | datetime | |
|
||
| `due_at` | datetime\|null | مهلت پرداخت نشست (مثلاً ۳۰ دقیقه) |
|
||
| `paid_at` | datetime\|null | |
|
||
| `payment_session_id` | FK\|null | |
|
||
| `gateway_id` | int\|null | درگاه استفادهشده |
|
||
| `gateway_provider` | string\|null | zarinpal/parsian/bitpay |
|
||
| `gateway_ref` | string\|null | Authority/Token |
|
||
| `gateway_trace` | string\|null | RefId بانکی |
|
||
| `period_months` | int | اسنپشات مدت در زمان خرید |
|
||
| `coverage_starts_at` | datetime\|null | بازه پوشش بعد از پرداخت |
|
||
| `coverage_ends_at` | datetime\|null | |
|
||
| `promo_code_id` | FK\|null | اختیاری فاز ۱.۵ |
|
||
| `extra_info` | JSON\|null | جزئیات خام verify، IP، user-agent، … |
|
||
| `created_at` / `updated_at` | datetime | |
|
||
|
||
### ۵.۴. `support_payment_sessions`
|
||
|
||
جایگزین کیفپول برای ردیابی درگاه — **کاملاً مستقل از `wallet_transactions`.**
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|------|-----|--------|
|
||
| `id` | PK | |
|
||
| `user_id` | FK users | |
|
||
| `invoice_id` | FK support_invoices | |
|
||
| `gateway_id` | FK payment_gateways | درگاه سیستم |
|
||
| `amount` | numeric(18,2) | |
|
||
| `currency_id` | FK | |
|
||
| `status` | string | `created` / `redirected` / `verifying` / `paid` / `failed` / `cancelled` / `expired` |
|
||
| `external_ref` | string\|null | Authority/Token |
|
||
| `provider_payload` | JSON\|null | پاسخ خام initiate (بدون secret) |
|
||
| `verify_payload` | JSON\|null | پاسخ خام verify |
|
||
| `client_return_path` | string\|null | مسیر برگشت فرانت (`/user/profile/support/billing?…`) |
|
||
| `expires_at` | datetime | انقضای نشست پرداخت |
|
||
| `paid_at` | datetime\|null | |
|
||
| `created_at` / `updated_at` | datetime | |
|
||
|
||
### ۵.۵. `support_usage_counters` (برای hybrid)
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|------|-----|--------|
|
||
| `id` | PK | |
|
||
| `user_id` | FK | |
|
||
| `period_key` | string | مثلاً `2026-07` (ماه میلادی) یا کلید شمسی |
|
||
| `tickets_created` | int | شمار ایجاد تیکت در آن دوره |
|
||
| `updated_at` | datetime | |
|
||
|
||
Unique(`user_id`, `period_key`).
|
||
|
||
### ۵.۶. `support_promo_codes` (پیشنهادی — فاز ۱.۵، غنای فروش)
|
||
|
||
| فیلد | توضیح |
|
||
|------|--------|
|
||
| `code`, `percent_off` یا `amount_off`, `valid_from/to`, `max_redemptions`, `per_user_limit`, `applicable_plan_ids`, `is_active` | |
|
||
|
||
در فاز ۱ میتوان جدول را ساخت ولی UI ادمین را ساده نگه داشت؛ یا کامل در همان فاز اول اگر زمان اجازه دهد.
|
||
|
||
---
|
||
|
||
## ۶. لایه پرداخت (مستقیم به بانک)
|
||
|
||
### ۶.۱. انتخاب درگاه
|
||
|
||
1. خواندن `support_default_gateway_id` از تنظیمات سیستم.
|
||
2. اگر معتبر و `is_active` باشد → همان.
|
||
3. وگرنه: لیست `payment_gateways` با `is_active=true` (سطح سیستم).
|
||
4. اگر هیچ درگاه فعالی نباشد → خطا با پیام واضح به کاربر و ادمین: «درگاه پرداخت سیستم پیکربندی نشده است.»
|
||
5. **هرگز** از `business_payment_gateways` استفاده نشود.
|
||
|
||
### ۶.۲. جریان خرید (Happy Path)
|
||
|
||
```
|
||
کاربر → انتخاب پلن
|
||
→ (اختیاری) وارد کردن کد تخفیف
|
||
→ POST ایجاد صورتحساب + نشست پرداخت
|
||
→ Backend: initiate روی درگاه سیستم (بدون business_id)
|
||
→ پاسخ: payment_url
|
||
→ فرانت: باز کردن/هدایت به payment_url (سایت بانک/درگاه)
|
||
→ کاربر پرداخت میکند
|
||
→ درگاه → Callback سرور حسابیکس
|
||
→ Verify
|
||
→ invoice=paid + subscription active/extended
|
||
→ Redirect کاربر به صفحه نتیجه در UI پشتیبانی
|
||
```
|
||
|
||
### ۶.۳. تغییرات فنی لازم روی سرویس پرداخت
|
||
|
||
امروز `initiate_payment(..., business_id, tx_id, ...)` و callback عمدتاً به `WalletTransaction` و `confirm_top_up` گرهخورده است.
|
||
|
||
برای پشتیبانی باید یکی از این دو مسیر (ترجیح: A):
|
||
|
||
**مسیر A — تعمیم کنترلشده (توصیهشده)**
|
||
|
||
- افزودن تابعهای موازی:
|
||
- `initiate_system_payment(db, *, gateway_id, amount, callback_kind, internal_id, description, metadata)`
|
||
- `verify` موجود بماند ولی بعد از verify بهجای `confirm_top_up`، بر اساس `callback_kind` / رجیستری handler، `confirm_support_payment(session_id)` صدا زده شود.
|
||
- Callback جدید یا پارامتر `kind=support` روی callbackهای موجود:
|
||
- مثلاً `/api/v1/payments/callback/{provider}?kind=support&session_id=…`
|
||
- یا مسیر اختصاصی: `/api/v1/support/payments/callback/{provider}`
|
||
- `callback_url` درگاه برای پشتیبانی باید در config درگاه یا بهصورت override در initiate به URL پشتیبانی اشاره کند (یا همان URL عمومی با `kind`).
|
||
|
||
**مسیر B — کپی حداقلی**
|
||
|
||
- سرویس جدا `support_payment_service.py` که همان پروتکل زرینپال/پارسیان/بیتپی را صدا میزند ولی به `support_payment_sessions` مینویسد.
|
||
- خطر: دوبارهکاری؛ فقط اگر تعمیم payment_service پرریسک باشد.
|
||
|
||
**الزام:** در هر دو مسیر، مبلغ، `session_id`، و `user_id` قبل از redirect در DB قفل شوند؛ بعد از verify مبلغ درگاه با مبلغ نشست **باید** برابر باشد وگرنه `failed` و اشتراک فعال نشود.
|
||
|
||
### ۶.۴. نتیجه برگشت به فرانت
|
||
|
||
صفحهٔ نتیجه داخل پروفایل پشتیبانی:
|
||
|
||
- موفقیت: «اشتراک شما تا {ends_at} فعال شد» + دکمه رفتن به تیکتها / صدور تیکت.
|
||
- شکست: دلیل خلاصه + دکمه تلاش مجدد (ساخت نشست جدید روی همان invoice اگر هنوز `awaiting_payment` و منقضی نشده؛ در غیر این صورت invoice جدید).
|
||
- در وبویو موبایل/دسکتاپ: همان الگوی `detect_source` و صفحات HTML موفقیت/شکست فعلی قابل الهام است، ولی مقصد نهایی UX باید داخل اپ پشتیبانی باشد نه wallet.
|
||
|
||
### ۶.۵. Idempotency و امنیت پرداخت
|
||
|
||
- Verify تکراری برای یک `session` که قبلاً `paid` است → no-op موفق (اشتراک دوباره ساخته نشود).
|
||
- نشست منقضی (`expires_at`) بعد از بازگشت دیرهنگام → `failed/expired`؛ کاربر باید دوباره بخرد.
|
||
- امضای/Authority باید به همان `session_id` و `amount` بخورد.
|
||
- کاربر فقط به invoice/session خودش دسترسی دارد.
|
||
- ادمین میتواند invoice را `void` کند ولی refund بانکی دستی/خارج از باند است مگر بعداً API استرداد اضافه شود.
|
||
- حداقل مبلغ درگاه (مثلاً بیتپی ۵۰۰۰ ریال) قبل از initiate چک شود؛ پلن با قیمت کمتر از حداقل، در حالت غیررایگان قابل خرید نباشد یا ادمین هشدار ببیند.
|
||
|
||
### ۶.۶. پلن رایگان (`is_free` یا price=0)
|
||
|
||
- بدون redirect به بانک.
|
||
- صورتحساب با `status=paid`, `amount=0`, `invoice_type=purchase|trial`.
|
||
- اشتراک فوراً فعال میشود.
|
||
- محدودیت: هر کاربر فقط یکبار پلن رایگان غیرtrial، یا طبق سیاست ادمین (`max_free_activations_per_user`).
|
||
|
||
---
|
||
|
||
## ۷. قوانین Entitlement (دسترسی تیکت)
|
||
|
||
تابع مرکزی پیشنهادی:
|
||
|
||
`get_support_entitlement(db, user_id) -> SupportEntitlement`
|
||
|
||
خروجی منطقی:
|
||
|
||
```text
|
||
mode: free|paid|hybrid
|
||
can_create_ticket: bool
|
||
can_reply: bool
|
||
can_read: bool
|
||
reason_code: ok|billing_disabled|no_subscription|quota_exceeded|grace_reply_only|…
|
||
subscription: {plan, status, ends_at, grace_ends_at} | null
|
||
quota: {used, limit, period_key} | null # فقط hybrid
|
||
upsell_required: bool
|
||
```
|
||
|
||
### ۷.۱. ماتریس رفتار
|
||
|
||
| حالت | اشتراک فعال | Grace | سهمیه hybrid باقی | ایجاد تیکت | پاسخ | خواندن |
|
||
|------|-------------|-------|-------------------|------------|------|--------|
|
||
| tickets disabled | — | — | — | خیر | خیر | خیر (یا پیام disabled) |
|
||
| `free` | — | — | — | بله | بله | بله |
|
||
| `paid` | بله | — | — | بله* | بله | بله |
|
||
| `paid` | خیر | بله | — | خیر | بله (اگر تنظیم اجازه دهد) | بله |
|
||
| `paid` | خیر | خیر | — | خیر | خیر/بسته به تنظیم | بله اگر allow_read |
|
||
| `hybrid` | بله | — | — | بله* | بله | بله |
|
||
| `hybrid` | خیر | — | بله | بله (از سهمیه) | بله | بله |
|
||
| `hybrid` | خیر | — | خیر | خیر | خیر/بسته به تنظیم | بله اگر allow_read |
|
||
|
||
\* علاوه بر سقف `max_open_tickets` پلن.
|
||
|
||
### ۷.۲. نقاط اعمال گیت
|
||
|
||
- `POST /api/v1/support` (ایجاد)
|
||
- `POST /api/v1/support/{id}/messages` وقتی `sender_type=user`
|
||
- `PUT .../reopen` (بازگشایی = نیازمند entitlement ایجاد/پاسخ)
|
||
- فرانت: قبل از نمایش فرم ایجاد، banner وضعیت + CTA خرید
|
||
|
||
اپراتور و AI اپراتور **تحت این گیت نیستند**.
|
||
|
||
### ۷.۳. شمارش سهمیه hybrid
|
||
|
||
- فقط روی **ایجاد موفق** تیکت اینکریمنت شود.
|
||
- تیکت حذفشده توسط ادمین سهمیه را برنمیگرداند مگر سیاست خلاف آن تعریف شود (پیشفرض: برنمیگردد).
|
||
- کاربر دارای اشتراک فعال از سهمیه رایگان مصرف نمیکند (یا مصرف میکند ولی بیاثر است — ترجیح: مصرف نکند تا آمار سهمیه تمیز بماند).
|
||
|
||
### ۷.۴. تمدید و ارتقا
|
||
|
||
| سناریو | رفتار |
|
||
|--------|--------|
|
||
| تمدید همان پلن قبل از انقضا | بعد از پرداخت موفق: `ends_at = max(now, ends_at) + period_months` (stack) |
|
||
| تمدید بعد از انقضا (خارج grace) | `starts_at=now`, `ends_at=now+period` |
|
||
| ارتقا به پلن گرانتر/طولانیتر وسط دوره | فاز ۱ پیشنهادی: پرداخت کامل پلن جدید + باقیمانده روزهای قبلی بهصورت اعتبار روز به انتهای جدید اضافه شود **یا** سادهتر: فقط stack مدت بدون proration. **توصیه فاز ۱: بدون proration مبلغی؛ فقط افزودن مدت به انتهای اشتراک فعلی** تا پیچیدگی مالی کم شود. |
|
||
| خرید همزمان دو نشست باز | فقط یک `awaiting_payment` فعال per user؛ نشست قبلی `cancelled/expired` شود. |
|
||
|
||
### ۷.۵. انقضا و Grace
|
||
|
||
Job پسزمینه (مشابه jobs پشتیبانی موجود):
|
||
|
||
1. `active` و `now > ends_at` و `now <= grace_ends_at` → `grace`
|
||
2. `now > grace_ends_at` → `expired`
|
||
3. ارسال اعلانهای `support.subscription_expiring` در روزهای تنظیمشده
|
||
4. اعلان `support.subscription_expired` و `support.subscription_grace_ended`
|
||
|
||
---
|
||
|
||
## ۸. تجربه کاربری (Flutter)
|
||
|
||
### ۸.۱. ساختار صفحه پشتیبانی کاربر
|
||
|
||
تبها / بخشهای پیشنهادی در `/user/profile/support`:
|
||
|
||
1. **تیکتها** (موجود)
|
||
2. **اشتراک من** — وضعیت فعلی، تاریخ پایان، پلن، دکمه تمدید
|
||
3. **خرید پشتیبانی** — کارت پلنهای فعال ۱/۳/۶/۱۲
|
||
4. **صورتحسابها** — لیست invoiceها با فیلتر وضعیت
|
||
|
||
Header مشترک: چیپ وضعیت (`رایگان سیستمی` / `فعال تا …` / `منقضی` / `سهمیه: ۲/۲`).
|
||
|
||
### ۸.۲. جریان UI خرید
|
||
|
||
1. کارت پلن → جزئیات ویژگیها → «پرداخت و فعالسازی»
|
||
2. دیالوگ تأیید مبلغ و مدت (بدون انتخاب کسبوکار)
|
||
3. اگر چند درگاه سیستم فعال و default مشخص نیست → انتخاب درگاه
|
||
4. Loading «در حال اتصال به درگاه…»
|
||
5. External browser / WebView / launchUrl به `payment_url`
|
||
6. بازگشت به اپ → صفحه نتیجه با poll کوتاه وضعیت invoice (در صورتی که کاربر دستی برگشت قبل از callback)
|
||
|
||
### ۸.۳. Empty / Block states
|
||
|
||
- حالت paid بدون اشتراک: بهجای فرم ایجاد تیکت، کارت upsell با پلنهای پیشنهادی.
|
||
- خطای درگاه پیکربندینشده: پیام غیرمقصرانه برای کاربر + لاگ برای ادمین.
|
||
- تیکتینگ کلاً disabled: همان پیام فعلی.
|
||
|
||
### ۸.۴. صورتحسابها
|
||
|
||
ستونها: کد، نوع، پلن، مبلغ، وضعیت، تاریخ صدور، تاریخ پرداخت، شماره پیگیری درگاه.
|
||
|
||
جزئیات: اسنپشات پلن + بازه پوشش + دکمه «پرداخت مجدد» اگر `awaiting_payment` و معتبر.
|
||
|
||
---
|
||
|
||
## ۹. پنل مدیریت سیستم
|
||
|
||
### ۹.۱. صفحه تنظیمات پشتیبانی (گسترش system configuration یا صفحه اختصاصی)
|
||
|
||
- فعال/غیرفعال تیکتینگ کاربر (موجود)
|
||
- حالت صورتحساب: free / paid / hybrid
|
||
- سهمیه رایگان، grace، اعلان انقضا
|
||
- درگاه پیشفرض پشتیبانی
|
||
- پیشوند صورتحساب
|
||
|
||
### ۹.۲. صفحه «پلنهای پشتیبانی»
|
||
|
||
CRUD مشابه `storage_plans_admin_page`:
|
||
|
||
- نام، کد، مدت (dropdown ۱/۳/۶/۱۲)، قیمت، ارز، فعال، رایگان، ترتیب، توضیحات، ویژگیها، سقف تیکت باز، وزن اولویت، SLA اختیاری
|
||
|
||
اعتبارسنجی: اگر `is_free` → `price` باید ۰ باشد.
|
||
|
||
### ۹.۳. صفحه/تب «صورتحسابهای پشتیبانی»
|
||
|
||
- جستجو بر اساس کد، کاربر، وضعیت، بازه تاریخ
|
||
- جزئیات verify درگاه (فقط ادمین)
|
||
- عملیات: void (قبل از پرداخت)، علامتگذاری دستی paid فقط برای سوپرادمین با audit log (موارد استثنایی پشتیبانی)
|
||
|
||
### ۹.۴. گزارش آماری (ابزار جدید)
|
||
|
||
داشبورد جدا یا تب در operator/admin:
|
||
|
||
**KPIهای ضروری**
|
||
|
||
| شاخص | تعریف |
|
||
|------|--------|
|
||
| درآمد دوره | sum(invoice.amount) where paid در بازه |
|
||
| تعداد پرداخت موفق / ناموفق | |
|
||
| نرخ تبدیل | کاربران visit-plans یا blocked-create → paid (تقریبی با event) |
|
||
| اشتراک فعال الان | count status in (active, grace) |
|
||
| تفکیک پلن | pie/bar بر اساس period_months |
|
||
| MRR تقریبی | نرمالسازی مبلغ به ماه: `amount / period_months` برای invoiceهای paid در دوره |
|
||
| در شرف انقضا ۷ روز | |
|
||
| انقضاشده دوره | |
|
||
| میانگین CSAT | کاربران با اشتراک فعال vs بدون/رایگان |
|
||
| تیکت بهازای مشترک | |
|
||
| سهمیه hybrid مصرفشده | متوسط used/limit |
|
||
|
||
**فیلترها:** از–تا، پلن، درگاه، وضعیت.
|
||
|
||
**خروجی:** جدول + نمودار ساده (الهام از `support_activity_chart`) + خروجی CSV برای ادمین.
|
||
|
||
### ۹.۵. اپراتور
|
||
|
||
- نشان کوچک روی تیکت: «مشترک» / «اولویتدار» اگر `includes_priority_support`.
|
||
- فیلتر لیست: فقط مشترکان.
|
||
- در جزئیات تیکت: پلن فعال کاربر و تاریخ پایان (فقط خواندنی).
|
||
|
||
---
|
||
|
||
## ۱۰. API (پیشنهادی)
|
||
|
||
### کاربر
|
||
|
||
| Method | Path | توضیح |
|
||
|--------|------|--------|
|
||
| GET | `/api/v1/support/billing/entitlement` | وضعیت دسترسی فعلی |
|
||
| GET | `/api/v1/support/billing/plans` | پلنهای فعال قابل خرید |
|
||
| GET | `/api/v1/support/billing/subscription` | اشتراک جاری |
|
||
| GET | `/api/v1/support/billing/invoices` | لیست صورتحسابهای کاربر |
|
||
| GET | `/api/v1/support/billing/invoices/{id}` | جزئیات |
|
||
| POST | `/api/v1/support/billing/checkout` | body: `{plan_id, gateway_id?, promo_code?}` → `{invoice, payment_url?}` |
|
||
| POST | `/api/v1/support/billing/invoices/{id}/retry-payment` | نشست جدید |
|
||
| GET | `/api/v1/support/billing/gateways` | درگاههای سیستم مجاز برای پشتیبانی (بدون secret) |
|
||
|
||
### Callback
|
||
|
||
| Method | Path | توضیح |
|
||
|--------|------|--------|
|
||
| GET/POST | `/api/v1/support/payments/callback/{provider}` | verify و فعالسازی |
|
||
|
||
### ادمین
|
||
|
||
| Method | Path | توضیح |
|
||
|--------|------|--------|
|
||
| CRUD | `/api/v1/admin/support-plans` | |
|
||
| GET | `/api/v1/admin/support-invoices` | جستجو |
|
||
| POST | `/api/v1/admin/support-invoices/{id}/void` | |
|
||
| GET | `/api/v1/admin/support-billing/stats` | خلاصه KPI |
|
||
| GET | `/api/v1/admin/support-billing/stats/timeseries` | سری زمانی درآمد/خرید |
|
||
| GET | `/api/v1/admin/support-billing/stats/export.csv` | |
|
||
| PATCH | تنظیمات سیستم موجود | فیلدهای بخش ۴ |
|
||
|
||
پرمیشنها: ادمین سیستم / سوپرادمین؛ آمار میتواند به `support_operator` فقطخواندنی هم داده شود (اختیاری؛ پیشفرض فقط ادمین مالی/سیستم).
|
||
|
||
---
|
||
|
||
## ۱۱. اعلانها (Event Keys پیشنهادی)
|
||
|
||
| Event | زمان | گیرنده |
|
||
|-------|------|--------|
|
||
| `support.subscription_activated` | پرداخت موفق / فعالسازی رایگان | کاربر |
|
||
| `support.subscription_renewed` | تمدید موفق | کاربر |
|
||
| `support.subscription_expiring` | N روز قبل از ends_at | کاربر |
|
||
| `support.subscription_expired` | ورود به expired | کاربر |
|
||
| `support.subscription_grace` | ورود به grace | کاربر |
|
||
| `support.payment_failed` | verify ناموفق | کاربر |
|
||
| `support.quota_exhausted` | hybrid سهمیه تمام | کاربر |
|
||
| `support.billing_blocked_create` | تلاش ایجاد بدون entitlement (اختیاری، برای آنالیتیکس) | — یا کاربر راهنما |
|
||
|
||
کانالها: inapp + email حداقل؛ SMS/تلگرام مطابق زیرساخت ناتیفیکیشن موجود.
|
||
|
||
---
|
||
|
||
## ۱۲. موارد پیشنهادی غنیسازی (اولویتبندی)
|
||
|
||
### ضروری در همان نسخهٔ اول (P0)
|
||
|
||
1. حالتهای `free` / `paid` / `hybrid`
|
||
2. پلنهای ۱/۳/۶/۱۲ + قیمت ادمین
|
||
3. Checkout مستقیم درگاه سیستم + callback + فعالسازی
|
||
4. صورتحساب و تاریخچه کاربر
|
||
5. گیت create/reply
|
||
6. Grace period + اعلان انقضا
|
||
7. داشبورد آماری ادمین (KPIهای اصلی)
|
||
8. Idempotent verify و یک نشست باز همزمان
|
||
9. Seed امن با `billing_mode=free` تا رفتار فعلی نشکند
|
||
|
||
### بسیار توصیهشده همزمان یا بلافاصله بعد (P1)
|
||
|
||
10. نشان «مشترک/اولویتدار» برای اپراتور + `priority_weight`
|
||
11. سقف `max_open_tickets` بر اساس پلن
|
||
12. اتصال اختیاری پلن به `sla_policy`
|
||
13. کد تخفیف
|
||
14. Retry پرداخت روی صورتحساب awaiting
|
||
15. خروجی CSV گزارش
|
||
16. Trial چندروزه یکبارمصرف
|
||
17. صفحه نتیجه پرداخت داخل مسیر پشتیبانی (نه wallet)
|
||
|
||
### فاز بعد (P2)
|
||
|
||
18. Auto-renew واقعی (نیازمند پرداخت خودکار/توکن درگاه — فعلاً اکثر درگاههای داخلی محدودند)
|
||
19. Proration دقیق ارتقا
|
||
20. استرداد آنلاین
|
||
21. اشتراک سازمانی چندکاربره روی یک پرداخت
|
||
22. مالیات/عوارض جدا روی صورتحساب
|
||
23. وبهوک حسابداری خارجی
|
||
|
||
---
|
||
|
||
## ۱۳. سازگاری با وضعیت فعلی
|
||
|
||
| مورد | تصمیم |
|
||
|------|--------|
|
||
| کاربران فعلی | با `billing_mode=free` بدون تغییر کار میکنند |
|
||
| تیکتهای باز | همیشه خواندنی طبق تنظیم؛ با سوییچ به `paid` برای ادامه پاسخ نیاز به خرید دارند |
|
||
| اپراتورها | بدون تغییر دسترسی |
|
||
| کیفپول / کسبوکار | صفر تداخل؛ جداول و سرویسهای جدید |
|
||
| درگاههای ثبتشده ادمین | همان رکوردهای `payment_gateways`؛ فقط مصرفکننده جدید |
|
||
|
||
---
|
||
|
||
## ۱۴. موارد لبهای و خطاها
|
||
|
||
| وضعیت | رفتار |
|
||
|-------|--------|
|
||
| کاربر پرداخت را در بانک لغو کند | session/invoice → failed؛ اشتراک تغییر نکند |
|
||
| Dual-tab دوبار checkout | نشست قبلی cancel؛ فقط آخرین معتبر |
|
||
| قیمت پلن وسط خرید توسط ادمین عوض شود | مبلغ از اسنپشات invoice؛ نه قیمت لحظهای پلن بعد از صدور |
|
||
| پلن وسط خرید غیرفعال شود | checkout جدید ممنوع؛ نشست جاری تا انقضا قابل تکمیل است |
|
||
| کاربر حذفشده | cascade منطقی؛ گزارشها anonymize اختیاری |
|
||
| اختلاف مبلغ verify | failed + هشدار امنیتی در لاگ |
|
||
| درگاه sandbox | فقط اگر `is_sandbox` و محیط اجازه دهد؛ در UI ادمین برچسب واضح |
|
||
| ساعت سرور / timezone | ذخیره UTC؛ نمایش محلی کاربر در UI |
|
||
| همزمانی verify و retry | قفل سطری روی payment_session |
|
||
|
||
---
|
||
|
||
## ۱۵. معیارهای پذیرش (Acceptance Criteria)
|
||
|
||
1. با `billing_mode=free` رفتار تیکت دقیقاً مثل قبل است.
|
||
2. با `paid` و بدون اشتراک، ایجاد تیکت API و UI مسدود و پیام upsell نشان داده میشود.
|
||
3. ادمین میتواند ۴ پلن با قیمتهای مختلف بسازد/ویرایش کند.
|
||
4. کاربر با انتخاب پلن به URL درگاه سیستم میرود؛ **هیچ انتخاب کسبوکار یا کیفپولی در UI نیست.**
|
||
5. پس از پرداخت موفق، اشتراک فعال و صورتحساب `paid` با `gateway_trace` ثبت میشود.
|
||
6. کاربر لیست صورتحسابهای خودش را میبیند و به دیگران دسترسی ندارد.
|
||
7. ادمین در گزارش، درآمد بازه و تعداد اشتراک فعال را میبیند.
|
||
8. انقضای اشتراک پس از job پسزمینه وضعیت را عوض میکند و اعلان ارسال میشود.
|
||
9. در کل جریان خرید/callback هیچ نوشتن روی جداول wallet/business برای این محصول رخ نمیدهد.
|
||
10. Verify تکراری اشتراک دوم نمیسازد.
|
||
|
||
---
|
||
|
||
## ۱۶. ترتیب پیادهسازی پیشنهادی
|
||
|
||
1. Migration جداول + تنظیمات سیستم + seed پلنها
|
||
2. سرویس entitlement + گیت API تیکت
|
||
3. Admin CRUD پلنها و تنظیمات billing
|
||
4. Checkout + payment session + initiate درگاه (بدون business)
|
||
5. Callback verify + فعالسازی اشتراک
|
||
6. UI کاربر: اشتراک / خرید / صورتحساب / block states
|
||
7. اعلان انقضا + job وضعیت
|
||
8. آمار ادمین + نشان اپراتور
|
||
9. Promo/trial در صورت باقی ماندن ظرفیت
|
||
|
||
---
|
||
|
||
## ۱۷. جمعبندی تصمیم معماری
|
||
|
||
```text
|
||
[User] --checkout--> [Support Invoice + Payment Session]
|
||
|
|
||
v
|
||
[System PaymentGateway]
|
||
|
|
||
v
|
||
[Bank / PSP Web]
|
||
|
|
||
v
|
||
[Support Callback Verify]
|
||
|
|
||
v
|
||
[User Subscription Active]
|
||
|
|
||
v
|
||
[Ticket Entitlement Gate]
|
||
```
|
||
|
||
**کیفپول و کسبوکار در این گراف وجود ندارند.**
|
||
تیکتها همچنان متعلق به کاربرند؛ فقط حق استفاده از کانال پشتیبانی با اشتراک/سهمیه کنترل میشود.
|