Implement SMS panel user flow and patient records system; add wallet charging, automatic reminders, and patient session management with detailed database schema and user flows.
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# معماری — تسک ۱۱: پنل اشتراکی (Subscription Tiers)
|
||||
|
||||
## ساختار فایلها
|
||||
|
||||
```
|
||||
src/Subscription/
|
||||
├── Controller/
|
||||
│ └── SubscriptionController.php
|
||||
├── Entity/
|
||||
│ ├── SubscriptionPlan.php
|
||||
│ ├── SubscriptionPeriod.php
|
||||
│ └── ClinicSubscription.php
|
||||
├── Repository/
|
||||
│ ├── SubscriptionPlanRepository.php
|
||||
│ ├── SubscriptionPeriodRepository.php
|
||||
│ └── ClinicSubscriptionRepository.php
|
||||
└── Service/
|
||||
└── SubscriptionService.php
|
||||
```
|
||||
|
||||
**فایلهایی که تغییر میکنند:**
|
||||
- `src/Payment/Controller/PaymentController.php` — متد `subscriptionCallback()` باید پس از تأیید پرداخت، `ClinicSubscription` بسازد
|
||||
|
||||
## Entity: SubscriptionPlan
|
||||
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'subscription_plans')]
|
||||
class SubscriptionPlan
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 30)]
|
||||
private string $name; // 'free' | 'basic' | 'professional'
|
||||
|
||||
#[ORM\Column(type: 'smallint')]
|
||||
private int $level; // 0 | 1 | 2
|
||||
|
||||
#[ORM\Column(type: 'smallint')]
|
||||
private int $maxSecretaries; // 1 | 2 | 5
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $features; // {"patient_records": bool, "services": bool, "sms_panel": bool}
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
// OneToMany → SubscriptionPeriod
|
||||
}
|
||||
```
|
||||
|
||||
## Entity: SubscriptionPeriod
|
||||
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'subscription_periods')]
|
||||
class SubscriptionPeriod
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: SubscriptionPlan::class)]
|
||||
#[ORM\JoinColumn(name: 'plan_id', nullable: false, onDelete: 'CASCADE')]
|
||||
private SubscriptionPlan $plan;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 50)]
|
||||
private string $label; // '۶ ماهه', 'تریال ۱ ماهه'
|
||||
|
||||
#[ORM\Column(type: 'smallint')]
|
||||
private int $durationMonths;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $priceRials; // 0 برای تریال
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isTrial = false;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: 'smallint')]
|
||||
private int $sortOrder = 0;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
## Entity: ClinicSubscription
|
||||
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'clinic_subscriptions')]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id', 'expires_at'], name: 'idx_clinic_sub_entity')]
|
||||
class ClinicSubscription
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 10)]
|
||||
private string $entityType; // 'doctor' | 'clinic'
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $entityId;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: SubscriptionPlan::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private SubscriptionPlan $plan;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: SubscriptionPeriod::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private SubscriptionPeriod $period;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: \App\Payment\Entity\Payment::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?\App\Payment\Entity\Payment $payment = null; // null برای تریال
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isTrial = false;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $startsAt;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
private ?int $expiresAt = null; // null = بینهایت (Free)
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $createdAt;
|
||||
}
|
||||
```
|
||||
|
||||
## Service: SubscriptionService
|
||||
|
||||
```php
|
||||
class SubscriptionService
|
||||
{
|
||||
public function getActiveSubscription(string $entityType, int $entityId): ?ClinicSubscription
|
||||
{
|
||||
// آخرین رکورد که expires_at > time() OR expires_at IS NULL
|
||||
}
|
||||
|
||||
public function hasFeature(string $entityType, int $entityId, string $feature): bool
|
||||
{
|
||||
$sub = $this->getActiveSubscription($entityType, $entityId);
|
||||
if (!$sub) return false;
|
||||
return $sub->getPlan()->getFeatures()[$feature] ?? false;
|
||||
}
|
||||
|
||||
public function getSecretaryLimit(string $entityType, int $entityId): int
|
||||
{
|
||||
$sub = $this->getActiveSubscription($entityType, $entityId);
|
||||
return $sub ? $sub->getPlan()->getMaxSecretaries() : 1; // Free = 1
|
||||
}
|
||||
|
||||
public function hasUsedTrial(string $entityType, int $entityId): bool
|
||||
{
|
||||
return $this->subscriptionRepo->existsTrial($entityType, $entityId);
|
||||
}
|
||||
|
||||
public function activateTrial(string $entityType, int $entityId): ClinicSubscription
|
||||
{
|
||||
// بررسی: آیا قبلاً تریال استفاده شده؟ → throw AppException
|
||||
// بررسی: آیا trial_enabled در SiteConfig فعال است؟
|
||||
// ساخت ClinicSubscription با is_trial=true, payment=null
|
||||
}
|
||||
|
||||
public function calculateExpiresAt(?int $currentExpiresAt, int $durationMonths): int
|
||||
{
|
||||
$base = max($currentExpiresAt ?? 0, time());
|
||||
return $base + ($durationMonths * 30 * 86400);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## وابستگی PaymentController
|
||||
|
||||
```php
|
||||
// src/Payment/Controller/PaymentController.php
|
||||
// متد subscriptionCallback() — بعد از تأیید پرداخت:
|
||||
|
||||
$subscriptionService->createFromPayment($payment, $periodUuid, $entityType, $entityId);
|
||||
|
||||
// این متد:
|
||||
// 1. Period را پیدا میکند
|
||||
// 2. expires_at را محاسبه میکند (در صورت تمدید از expires_at قبلی)
|
||||
// 3. ClinicSubscription جدید میسازد
|
||||
// 4. persist/flush
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# پایگاه داده — تسک ۱۱: پنل اشتراکی (Subscription Tiers)
|
||||
|
||||
## جدول: subscription_plans
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | |
|
||||
| name | VARCHAR(30) NOT NULL | `'free'` \| `'basic'` \| `'professional'` |
|
||||
| level | TINYINT NOT NULL | 0, 1, یا 2 |
|
||||
| max_secretaries | TINYINT NOT NULL | 1, 2، یا 5 |
|
||||
| features | JSON NOT NULL | `{"patient_records": bool, "services": bool, "sms_panel": bool}` |
|
||||
| active | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
| created_at | INT NOT NULL | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | Unix timestamp |
|
||||
|
||||
## جدول: subscription_periods
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | |
|
||||
| plan_id | INT NOT NULL FK→subscription_plans.id ON DELETE CASCADE | |
|
||||
| label | VARCHAR(50) NOT NULL | مثال: `'۶ ماهه'`, `'تریال ۱ ماهه'` |
|
||||
| duration_months | TINYINT NOT NULL | مدت به ماه |
|
||||
| price_rials | INT NOT NULL | `0` برای تریال |
|
||||
| is_trial | TINYINT(1) NOT NULL DEFAULT 0 | |
|
||||
| active | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
| sort_order | TINYINT NOT NULL DEFAULT 0 | ترتیب نمایش |
|
||||
| created_at | INT NOT NULL | |
|
||||
| updated_at | INT NOT NULL | |
|
||||
|
||||
## جدول: clinic_subscriptions
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | |
|
||||
| entity_type | VARCHAR(10) NOT NULL | `'doctor'` \| `'clinic'` |
|
||||
| entity_id | INT NOT NULL | id دکتر یا کلینیک |
|
||||
| plan_id | INT NOT NULL FK→subscription_plans.id | |
|
||||
| period_id | INT NOT NULL FK→subscription_periods.id | |
|
||||
| payment_id | INT NULL FK→payments.id ON DELETE SET NULL | `NULL` برای تریال |
|
||||
| is_trial | TINYINT(1) NOT NULL DEFAULT 0 | |
|
||||
| starts_at | INT NOT NULL | Unix timestamp |
|
||||
| expires_at | INT NULL | `NULL` = بینهایت (Free) |
|
||||
| created_at | INT NOT NULL | |
|
||||
|
||||
## ایندکسها
|
||||
|
||||
```sql
|
||||
-- جستجوی سریع اشتراک فعال
|
||||
CREATE INDEX idx_clinic_subscriptions_entity
|
||||
ON clinic_subscriptions(entity_type, entity_id, expires_at);
|
||||
|
||||
-- جلوگیری از استفاده مکرر از تریال
|
||||
-- توجه: این constraint روی is_trial=1 کار میکند چون is_trial=0 میتواند تکراری باشد
|
||||
-- بنابراین در application check میکنیم نه UNIQUE index
|
||||
CREATE INDEX idx_clinic_subscriptions_trial
|
||||
ON clinic_subscriptions(entity_type, entity_id, is_trial);
|
||||
```
|
||||
|
||||
## Seed Data (Migration اولیه)
|
||||
|
||||
```sql
|
||||
-- سه پنل پایه — باید در migration ایجاد شوند
|
||||
INSERT INTO subscription_plans (uuid, name, level, max_secretaries, features, active, created_at, updated_at) VALUES
|
||||
(UUID(), 'free', 0, 1, '{"patient_records":false,"services":false,"sms_panel":false}', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
(UUID(), 'basic', 1, 2, '{"patient_records":true,"services":true,"sms_panel":false}', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
(UUID(), 'professional', 2, 5, '{"patient_records":true,"services":true,"sms_panel":true}', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
|
||||
|
||||
-- دورههای نمونه برای basic (ادمین بعداً قیمتها را ویرایش میکند)
|
||||
-- plan_id=2 = basic
|
||||
INSERT INTO subscription_periods (uuid, plan_id, label, duration_months, price_rials, is_trial, active, sort_order, created_at, updated_at) VALUES
|
||||
(UUID(), 2, 'تریال ۱ ماهه', 1, 0, 1, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
(UUID(), 2, '۱ ماهه', 1, 250000, 0, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
(UUID(), 2, '۳ ماهه', 3, 690000, 0, 1, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
(UUID(), 2, '۶ ماهه', 6, 1200000, 0, 1, 3, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
(UUID(), 2, '۱۲ ماهه', 12, 2000000, 0, 1, 4, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
|
||||
```
|
||||
|
||||
## SiteConfig key های جدید
|
||||
|
||||
| کلید | نوع | مقدار پیشفرض | توضیح |
|
||||
|------|-----|--------------|-------|
|
||||
| `trial_enabled` | string `'1'`\|`'0'` | `'1'` | ادمین میتواند تریال را غیرفعال کند |
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- `expires_at = NULL` فقط برای رکوردهای Free plan است — اینها بینهایت معتبرند
|
||||
- query اشتراک فعال: `WHERE entity_type=? AND entity_id=? AND (expires_at IS NULL OR expires_at > UNIX_TIMESTAMP()) ORDER BY id DESC LIMIT 1`
|
||||
- تریال check در application: `SELECT COUNT(*) FROM clinic_subscriptions WHERE entity_type=? AND entity_id=? AND is_trial=1`
|
||||
@@ -0,0 +1,160 @@
|
||||
# تسک ۱۱: پنل اشتراکی (Subscription Tiers)
|
||||
|
||||
## توضیح
|
||||
پیادهسازی سیستم پنلهای اشتراکی سهسطحی. ادمین دورهها و قیمتها را تعریف میکند.
|
||||
این تسک **gate check** برای تسکهای ۱۲، ۱۳، ۱۵ فراهم میکند.
|
||||
|
||||
**نکته مهم:** `/api/v1/subscription-payment` و callback آن از قبل در `PaymentController` موجود است.
|
||||
در این تسک فقط باید بعد از callback موفق، `ClinicSubscription` ساخته شود + endpoint های Subscription خودش.
|
||||
|
||||
## سطوح پنل
|
||||
|
||||
| Level | Name | منشی | patient_records | services |
|
||||
|-------|------|------|----------------|---------|
|
||||
| 0 | free | ۱ | ❌ | ❌ |
|
||||
| 1 | basic | ۲ | ✅ | ✅ |
|
||||
| 2 | professional | ۵ | ✅ | ✅ |
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | Permission | توضیح |
|
||||
|-----|------|-----------|-------|
|
||||
| GET | `/api/v1/subscription/plans` | public | لیست پنلها + دورهها + قیمت |
|
||||
| GET | `/api/v1/subscription/my` | doctor/clinic | اشتراک فعال + `used_trial` |
|
||||
| POST | `/api/v1/subscription/trial` | doctor/clinic | فعالسازی تریال Basic (یکبار) |
|
||||
| GET | `/api/v1/admin/subscription/plans` | ROLE_ADMIN | لیست مدیریت پنلها |
|
||||
| POST | `/api/v1/admin/subscription/plan` | ROLE_ADMIN | ایجاد پنل |
|
||||
| PATCH | `/api/v1/admin/subscription/plan/{uuid}` | ROLE_ADMIN | ویرایش پنل |
|
||||
| POST | `/api/v1/admin/subscription/period` | ROLE_ADMIN | افزودن دوره |
|
||||
| PATCH | `/api/v1/admin/subscription/period/{uuid}` | ROLE_ADMIN | ویرایش دوره/قیمت |
|
||||
| DELETE | `/api/v1/admin/subscription/period/{uuid}` | ROLE_ADMIN | غیرفعالسازی دوره |
|
||||
| GET | `/api/v1/admin/subscription/report` | ROLE_ADMIN | گزارش فروش + تریالها |
|
||||
|
||||
**موجود (تغییر نمیکند):**
|
||||
- `POST /api/v1/subscription-payment` — شروع پرداخت (body: `{ period_uuid }`)
|
||||
- `GET|POST /api/v1/subscription-payment/callback/{gateway}` — callback gateway
|
||||
|
||||
## پیشنیازها
|
||||
- تسک ۰۲ (Auth/JWT)
|
||||
- تسک ۱۵-payment (PaymentController — موجود)
|
||||
|
||||
## زمان تخمینی
|
||||
۱۴ تا ۱۶ ساعت
|
||||
|
||||
## نمونه Request
|
||||
|
||||
### POST /api/v1/subscription/trial
|
||||
```json
|
||||
{}
|
||||
```
|
||||
(body خالی — entity از JWT گرفته میشود)
|
||||
|
||||
### POST /api/v1/admin/subscription/period
|
||||
```json
|
||||
{
|
||||
"plan_uuid": "uuid-of-basic-plan",
|
||||
"label": "۶ ماهه",
|
||||
"duration_months": 6,
|
||||
"price_rials": 1200000,
|
||||
"is_trial": false,
|
||||
"sort_order": 3
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/subscription-payment (موجود)
|
||||
```json
|
||||
{
|
||||
"period_uuid": "uuid-of-selected-period",
|
||||
"gateway": "mellat"
|
||||
}
|
||||
```
|
||||
|
||||
## نمونه Response
|
||||
|
||||
### GET /api/v1/subscription/plans
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"name": "basic",
|
||||
"level": 1,
|
||||
"max_secretaries": 2,
|
||||
"features": { "patient_records": true, "services": true, "sms_panel": false },
|
||||
"periods": [
|
||||
{ "uuid": "...", "label": "تریال ۱ ماهه", "duration_months": 1, "price_rials": 0, "is_trial": true },
|
||||
{ "uuid": "...", "label": "۱ ماهه", "duration_months": 1, "price_rials": 250000, "is_trial": false },
|
||||
{ "uuid": "...", "label": "۶ ماهه", "duration_months": 6, "price_rials": 1200000, "is_trial": false }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/subscription/my
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"plan": { "name": "basic", "level": 1, "features": { "patient_records": true, "services": true } },
|
||||
"period": { "label": "۶ ماهه", "duration_months": 6 },
|
||||
"is_trial": false,
|
||||
"starts_at": 1718000000,
|
||||
"expires_at": 1733360000,
|
||||
"used_trial": true,
|
||||
"days_remaining": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
وقتی اشتراک فعال ندارد:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"plan": { "name": "free", "level": 0 },
|
||||
"expires_at": null,
|
||||
"used_trial": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/subscription/trial (موفق)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"plan": "basic",
|
||||
"starts_at": 1718000000,
|
||||
"expires_at": 1720678400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/subscription/trial (خطا — قبلاً استفاده شده)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"errors": [{ "code": "ERR_TRIAL_ALREADY_USED", "message": "قبلاً از تریال استفاده کردهاید" }]
|
||||
}
|
||||
```
|
||||
|
||||
## قوانین تریال
|
||||
- فقط برای پنل **basic** (level=1)
|
||||
- هر entity یکبار — constraint `UNIQUE(entity_type, entity_id, is_trial)` + مقدار `is_trial=1` در DB
|
||||
- بدون پرداخت — `payment_id = null`
|
||||
- بعد از انقضا → برگشت به Free (دادهها حفظ میشوند)
|
||||
- ادمین میتواند تریال را کلاً غیرفعال کند: `SiteConfig.trial_enabled = false`
|
||||
|
||||
## قانون تمدید
|
||||
```
|
||||
expires_at جدید = max(expires_at فعلی, time()) + duration_months × 30 × 86400
|
||||
```
|
||||
یعنی اگر اشتراک هنوز منقضی نشده، تمدید از تاریخ انقضا محاسبه میشود (نه از now).
|
||||
|
||||
## gate check در تسکهای بعدی
|
||||
```php
|
||||
// SubscriptionService::hasFeature('patient_records') → bool
|
||||
// false → $this->error(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, '...', 403)
|
||||
```
|
||||
@@ -0,0 +1,126 @@
|
||||
# جریان کاربری — تسک ۱۱: پنل اشتراکی
|
||||
|
||||
## جریان مشاهده پنلها و خرید
|
||||
|
||||
```
|
||||
کاربر وارد صفحه اشتراک میشود
|
||||
│
|
||||
▼
|
||||
GET /api/v1/subscription/my
|
||||
→ نمایش پنل فعلی + تاریخ انقضا (شمسی) + days_remaining
|
||||
→ اگر used_trial=false و trial_enabled=true → بنر تریال نمایش داده میشود
|
||||
│
|
||||
▼
|
||||
GET /api/v1/subscription/plans
|
||||
→ جدول مقایسه پنلها (free | basic | professional)
|
||||
→ هر پنل: لیست دورهها با قیمت — دروه تریال جدا نمایش داده میشود
|
||||
│
|
||||
├─► کاربر «تریال» انتخاب میکند:
|
||||
│ │
|
||||
│ ▼
|
||||
│ POST /api/v1/subscription/trial
|
||||
│ ├─► 200: فعال شد → صفحه refresh
|
||||
│ └─► 400: قبلاً استفاده شده → toast خطا
|
||||
│
|
||||
└─► کاربر یک دوره پولی انتخاب میکند:
|
||||
│
|
||||
▼
|
||||
POST /api/v1/subscription-payment (موجود)
|
||||
{ period_uuid, gateway: 'mellat' }
|
||||
│
|
||||
▼
|
||||
redirect به gateway بانک
|
||||
│
|
||||
▼
|
||||
GET|POST /api/v1/subscription-payment/callback/{gateway}
|
||||
│
|
||||
▼
|
||||
SubscriptionService::createFromPayment()
|
||||
→ ClinicSubscription ساخته میشود
|
||||
│
|
||||
▼
|
||||
redirect به /admin/subscription?success=1
|
||||
```
|
||||
|
||||
## جریان تمدید اشتراک
|
||||
|
||||
```
|
||||
اشتراک ۶ ماهه فعال است (expires_at = آینده)
|
||||
│
|
||||
▼
|
||||
کاربر دوره جدید ۶ ماهه انتخاب میکند و پرداخت میکند
|
||||
│
|
||||
▼
|
||||
calculateExpiresAt():
|
||||
base = max(expires_at_فعلی, now) ← از تاریخ انقضا (نه الان)
|
||||
new_expires = base + 6 × 30 × 86400
|
||||
│
|
||||
▼
|
||||
اشتراک ۶ ماه دیگر تمدید میشود بدون اتلاف زمان باقیمانده
|
||||
```
|
||||
|
||||
## جریان gate check در تسکهای بعدی
|
||||
|
||||
```
|
||||
کاربر Free تلاش میکند پرونده بیمار بسازد
|
||||
│
|
||||
▼
|
||||
POST /api/v1/patient
|
||||
│
|
||||
▼
|
||||
PatientController::create():
|
||||
$hasFeature = $subscriptionService->hasFeature($entityType, $entityId, 'patient_records')
|
||||
│
|
||||
├─► false → 403 ERR_SUBSCRIPTION_REQUIRED
|
||||
│ { "errors": [{ "code": "ERR_SUBSCRIPTION_REQUIRED",
|
||||
│ "message": "این قابلیت نیاز به پنل Basic یا بالاتر دارد" }] }
|
||||
│
|
||||
└─► true → ادامه پردازش
|
||||
```
|
||||
|
||||
## جریان مدیریت پنلها توسط ادمین
|
||||
|
||||
```
|
||||
ادمین وارد صفحه مدیریت اشتراک میشود
|
||||
│
|
||||
├─► تب «پنلها»:
|
||||
│ GET /api/v1/admin/subscription/plans
|
||||
│ → جدول پنلها با دورهها و قیمتها
|
||||
│ → PATCH /api/v1/admin/subscription/period/{uuid} ← ویرایش inline قیمت
|
||||
│ → POST /api/v1/admin/subscription/period ← افزودن دوره جدید
|
||||
│
|
||||
└─► تب «گزارش»:
|
||||
GET /api/v1/admin/subscription/report?from=UNIX&to=UNIX
|
||||
→ تعداد خریدها + مجموع درآمد + تعداد تریالها
|
||||
→ breakdown بر اساس plan و period
|
||||
```
|
||||
|
||||
## هشدار انقضا (frontend)
|
||||
|
||||
```
|
||||
در هر بار لود صفحه:
|
||||
اگر days_remaining <= 7 و expires_at != null:
|
||||
→ نمایش banner هشدار: «اشتراک شما X روز دیگر منقضی میشود — تمدید کنید»
|
||||
```
|
||||
|
||||
## نمایش در Frontend (SubscriptionPage.tsx)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 🎁 یک ماه تریال رایگان — فعالسازی │ ← فقط اگر used_trial=false
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ پنل فعلی: Basic | انقضا: ۱۴۰۵/۰۶/۲۳ | ۴۲ روز │
|
||||
├───────────┬──────────────────┬─────────────────────┤
|
||||
│ Free │ Basic ★ │ Professional │
|
||||
│ رایگان │ پرونده بیمار ✅ │ پرونده بیمار ✅ │
|
||||
│ ۱ منشی │ سرویسها ✅ │ سرویسها ✅ │
|
||||
│ │ ۲ منشی │ ۵ منشی │
|
||||
│ │ ┌──────────┐ │ │
|
||||
│ │ │ ۱ ماهه │ │ [انتخاب دوره ▼] │
|
||||
│ │ │ ۲۵۰,۰۰۰ │ │ │
|
||||
│ │ │ ۶ ماهه │ │ │
|
||||
│ │ │ ۱,۲۰۰,۰۰۰│ │ │
|
||||
│ │ └──────────┘ │ │
|
||||
│ │ [خرید اشتراک] │ [خرید اشتراک] │
|
||||
└───────────┴──────────────────┴─────────────────────┘
|
||||
```
|
||||
Reference in New Issue
Block a user