# معماری — تسک ۱۴: پنل پیامکی ## ساختار فایل‌ها ``` 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 ```php #[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 ```php #[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 ```php #[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 ```php 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() ```php 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; } ```