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
|
||||
```
|
||||
Reference in New Issue
Block a user