Files
clinicpro/docs/phase2_taskes/task-14-sms-panel/architecture.md
T

5.0 KiB

معماری — تسک ۱۴: پنل پیامکی

ساختار فایل‌ها

src/Sms/
├── Controller/
│   ├── SmsController.php          ← موجود (تغییر نمی‌کند)
│   └── SmsWalletController.php    ← جدید
├── Entity/
│   ├── SmsLog.php                 ← موجود
│   ├── SmsTemplate.php            ← موجود
│   ├── SmsWallet.php              ← جدید
│   └── SmsWalletTransaction.php   ← جدید
│   └── SmsSettings.php            ← جدید
├── Repository/
│   └── SmsWalletRepository.php   ← جدید
└── Service/
    ├── SmsService.php             ← موجود — باید کسر wallet اضافه شود
    └── SmsWalletService.php       ← جدید

فایل‌هایی که تغییر می‌کنند:

  • src/Payment/Entity/Payment.php — اضافه کردن const TYPE_SMS_WALLET = 'sms_wallet'
  • src/Payment/Controller/PaymentController.php — callback برای sms_wallet type، شارژ wallet
  • src/Sms/Service/SmsService.php — قبل از ارسال، balance بررسی و کسر شود

Entity: SmsWallet

#[ORM\Entity]
#[ORM\Table(name: 'sms_wallets')]
#[ORM\UniqueConstraint(name: 'uniq_sms_wallet_entity', columns: ['entity_type', 'entity_id'])]
class SmsWallet
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 10)]
    private string $entityType;

    #[ORM\Column(type: 'integer')]
    private int $entityId;

    #[ORM\Column(type: 'integer')]
    private int $balanceRials = 0;

    #[ORM\Column(type: 'integer')]
    private int $createdAt;

    #[ORM\Column(type: 'integer')]
    private int $updatedAt;
}

Entity: SmsWalletTransaction

#[ORM\Entity]
#[ORM\Table(name: 'sms_wallet_transactions')]
#[ORM\Index(columns: ['sms_wallet_id', 'created_at'], name: 'idx_sms_wallet_tx')]
class SmsWalletTransaction
{
    public const TYPE_CREDIT = 'credit';
    public const TYPE_DEBIT  = 'debit';

    #[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: SmsWallet::class)]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private SmsWallet $wallet;

    #[ORM\Column(type: 'string', length: 10)]
    private string $type; // 'credit' | 'debit'

    #[ORM\Column(type: 'integer')]
    private int $amountRials;

    #[ORM\Column(type: 'string', length: 255, nullable: true)]
    private ?string $description = null;

    #[ORM\ManyToOne(targetEntity: \App\Payment\Entity\Payment::class)]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    private ?\App\Payment\Entity\Payment $payment = null;

    #[ORM\Column(type: 'integer')]
    private int $createdAt;
}

Entity: SmsSettings

#[ORM\Entity]
#[ORM\Table(name: 'sms_settings')]
#[ORM\UniqueConstraint(name: 'uniq_sms_settings_entity', columns: ['entity_type', 'entity_id'])]
class SmsSettings
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 10)]
    private string $entityType;

    #[ORM\Column(type: 'integer')]
    private int $entityId;

    #[ORM\Column(type: 'boolean')]
    private bool $reminderEnabled = false;

    #[ORM\Column(type: 'smallint')]
    private int $reminderHoursBefore = 2;

    #[ORM\Column(type: 'boolean')]
    private bool $postVisitEnabled = false;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $postVisitText = null;

    #[ORM\Column(type: 'integer')]
    private int $updatedAt;
}

SmsWalletService

class SmsWalletService
{
    public function getOrCreate(string $entityType, int $entityId): SmsWallet
    {
        // findOneBy([entityType, entityId]) یا ایجاد جدید با balance=0
    }

    public function charge(SmsWallet $wallet, int $amountRials, Payment $payment): void
    {
        $wallet->setBalanceRials($wallet->getBalanceRials() + $amountRials);
        // ثبت SmsWalletTransaction با type=credit
    }

    public function deduct(SmsWallet $wallet, int $amountRials, string $description): bool
    {
        if ($wallet->getBalanceRials() < $amountRials) return false;
        $wallet->setBalanceRials($wallet->getBalanceRials() - $amountRials);
        // ثبت SmsWalletTransaction با type=debit
        return true;
    }
}

تغییر SmsService::send()

public function send(string $phone, string $message, string $entityType, int $entityId): bool
{
    $priceRials = (int) $this->siteConfigRepo->getValue('sms_price_rials', '0');
    $wallet     = $this->walletService->getOrCreate($entityType, $entityId);

    if (!$this->walletService->deduct($wallet, $priceRials, 'ارسال پیامک')) {
        // لاگ: ارسال نشد — موجودی ناکافی
        return false;
    }

    // ارسال از طریق Provider موجود ...
    return true;
}