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:
hamed
2026-06-14 21:15:40 +03:30
parent a72a6da621
commit b58aacc37f
28 changed files with 3169 additions and 0 deletions
@@ -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
```