6.0 KiB
6.0 KiB
معماری — تسک ۱۱: پنل اشتراکی (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
#[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
#[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
#[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
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
// src/Payment/Controller/PaymentController.php
// متد subscriptionCallback() — بعد از تأیید پرداخت:
$subscriptionService->createFromPayment($payment, $periodUuid, $entityType, $entityId);
// این متد:
// 1. Period را پیدا میکند
// 2. expires_at را محاسبه میکند (در صورت تمدید از expires_at قبلی)
// 3. ClinicSubscription جدید میسازد
// 4. persist/flush