'sms_charge_card_number', 'sheba' => 'sms_charge_sheba', 'account_holder' => 'sms_charge_account_holder', 'note' => 'sms_charge_note', ]; /** مقادیر پیش‌فرض اطلاعات واریز. */ public const DEPOSIT_DEFAULTS = [ 'card_number' => '6219861961033817', 'sheba' => 'IR080560611828005603604201', 'account_holder' => 'حسین منتظر', 'note' => 'لطفا مبلغ مورد نظر خود جهت شارژ سامانه پیامکی را به شماره کارت یا حساب زیر واریز نمایید و تصویر فیش پرداختی را برای پشتیبانی بفرستید.', ]; /** Billing/panel فعال است مگر sms_panel صریحاً false باشد. */ public static function isBillingEnabled(): bool { $value = app(ConfigService::class)->getString('sms_panel'); if ($value === null || trim((string) $value) === '') { return true; } $normalized = strtolower(trim((string) $value)); return ! in_array($normalized, ['false', '0', 'no', 'off'], true); } /** billing فعال است و جداول migration هم وجود دارند. */ public static function isBillingOperational(): bool { if (! self::isBillingEnabled()) { return false; } try { return Schema::hasTable('sms_wallets') && Schema::hasTable('sms_send_logs') && Schema::hasTable('sms_ledger'); } catch (\Throwable) { return false; } } public function getWallet(): SmsWallet { return SmsWallet::query()->firstOrCreate([], [ 'balance' => 0, 'price_per_part' => 0, 'is_blocked' => false, 'alert_days_sent' => 0, ]); } public function isBlocked(): bool { if (! self::isBillingOperational()) { return false; } return (bool) $this->getWallet()->is_blocked; } public function setBlocked(bool $blocked): SmsWallet { $wallet = $this->getWallet(); $wallet->is_blocked = $blocked; $wallet->save(); return $wallet->fresh(); } /** * Keep local MySQL wallet mirrored with SuperAdmin during the parallel period. */ public function mirrorFromSuperAdmin(?int $balance = null, ?int $pricePerPart = null, ?bool $isBlocked = null): SmsWallet { return DB::transaction(function () use ($balance, $pricePerPart, $isBlocked) { $wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [ 'balance' => 0, 'price_per_part' => 0, 'is_blocked' => false, 'alert_days_sent' => 0, ]); if ($balance !== null) { $wallet->balance = $balance; } if ($pricePerPart !== null) { $wallet->price_per_part = max(0, $pricePerPart); } if ($isBlocked !== null) { $wallet->is_blocked = $isBlocked; } if ($wallet->balance > 0) { $wallet->alert_days_sent = 0; $wallet->last_alert_date = null; } $wallet->save(); return $wallet->fresh(); }); } public function updatePricePerPart(int $pricePerPart): SmsWallet { $wallet = $this->getWallet(); $wallet->price_per_part = max(0, $pricePerPart); $wallet->save(); return $wallet; } public function charge(int $amount, ?int $adminUserId = null, ?string $note = null): SmsWallet { if ($amount <= 0) { throw new \InvalidArgumentException('مبلغ شارژ باید بزرگتر از صفر باشد.'); } return DB::transaction(function () use ($amount, $adminUserId, $note) { $wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [ 'balance' => 0, 'price_per_part' => 0, 'is_blocked' => false, 'alert_days_sent' => 0, ]); $wallet->balance += $amount; if ($wallet->balance > 0) { $wallet->alert_days_sent = 0; $wallet->last_alert_date = null; } $wallet->save(); SmsLedger::create([ 'type' => self::TYPE_CHARGE, 'amount' => $amount, 'balance_after' => $wallet->balance, 'note' => $note, 'created_by' => $adminUserId, 'created_at' => now(), ]); return $wallet; }); } /** * After SuperAdmin consume: set local balance to SA value and write local send log/ledger * so the product SMS panel stays in sync during the parallel period. */ public function mirrorConsumeFromSuperAdmin( string $receiver, string $body, int $parts, string $provider, ?string $source, bool $isAlert, int $balanceAfter, int $costRials, int $pricePerPart ): void { DB::transaction(function () use ( $receiver, $body, $parts, $provider, $source, $isAlert, $balanceAfter, $costRials, $pricePerPart ) { $wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [ 'balance' => 0, 'price_per_part' => 0, 'is_blocked' => false, 'alert_days_sent' => 0, ]); $wallet->balance = $balanceAfter; if ($pricePerPart > 0) { $wallet->price_per_part = $pricePerPart; } if ($wallet->balance > 0) { $wallet->alert_days_sent = 0; $wallet->last_alert_date = null; } $wallet->save(); $log = SmsSendLog::create([ 'receiver' => $receiver, 'body' => $body, 'parts' => $parts, 'price_per_part' => $pricePerPart, 'total_amount' => $costRials, 'provider' => $provider, 'source' => $source, 'is_alert' => $isAlert, 'created_at' => now(), ]); SmsLedger::create([ 'type' => $isAlert ? self::TYPE_ALERT_DEBIT : self::TYPE_DEBIT, 'amount' => -$costRials, 'balance_after' => $balanceAfter, 'price_per_part' => $pricePerPart, 'parts' => $parts, 'sms_send_log_id' => $log->id, 'note' => $isAlert ? 'هشدار موجودی پیامک' : 'مصرف ارسال (superadmin)', 'created_at' => now(), ]); }); } /** * @return array{log: SmsSendLog, wallet: SmsWallet} */ public function debitSuccessfulSms( string $receiver, string $body, int $parts, string $provider, ?string $source = null, bool $isAlert = false ): array { return DB::transaction(function () use ($receiver, $body, $parts, $provider, $source, $isAlert) { $wallet = SmsWallet::query()->lockForUpdate()->firstOrCreate([], [ 'balance' => 0, 'price_per_part' => 0, 'is_blocked' => false, 'alert_days_sent' => 0, ]); $pricePerPart = (int) $wallet->price_per_part; $totalAmount = $parts * $pricePerPart; $wallet->balance -= $totalAmount; $wallet->save(); $log = SmsSendLog::create([ 'receiver' => $receiver, 'body' => $body, 'parts' => $parts, 'price_per_part' => $pricePerPart, 'total_amount' => $totalAmount, 'provider' => $provider, 'source' => $source, 'is_alert' => $isAlert, 'created_at' => now(), ]); SmsLedger::create([ 'type' => $isAlert ? self::TYPE_ALERT_DEBIT : self::TYPE_DEBIT, 'amount' => -$totalAmount, 'balance_after' => $wallet->balance, 'price_per_part' => $pricePerPart, 'parts' => $parts, 'sms_send_log_id' => $log->id, 'note' => $isAlert ? 'هشدار موجودی پیامک' : null, 'created_at' => now(), ]); return ['log' => $log, 'wallet' => $wallet->fresh()]; }); } /** * اطلاعات واریز جهت شارژ سامانه پیامکی؛ اگر تنظیمی ذخیره نشده باشد، مقدار پیش‌فرض برمی‌گردد. * * @return array{card_number: string, sheba: string, account_holder: string, note: string} */ public static function getDepositInfo(): array { $values = []; try { $values = Setting::whereIn('key', array_values(self::DEPOSIT_SETTING_KEYS)) ->pluck('value', 'key') ->toArray(); } catch (\Throwable) { $values = []; } $info = []; foreach (self::DEPOSIT_SETTING_KEYS as $field => $key) { $stored = $values[$key] ?? null; $info[$field] = ($stored === null || trim((string) $stored) === '') ? self::DEPOSIT_DEFAULTS[$field] : (string) $stored; } return $info; } /** * ذخیره اطلاعات واریز در تنظیمات. فقط فیلدهای ارسال‌شده به‌روزرسانی می‌شوند. * * @param array $data * @return array{card_number: string, sheba: string, account_holder: string, note: string} */ public static function setDepositInfo(array $data): array { foreach (self::DEPOSIT_SETTING_KEYS as $field => $key) { if (! array_key_exists($field, $data)) { continue; } $setting = Setting::firstOrNew(['key' => $key]); $incoming = trim((string) ($data[$field] ?? '')); if ($incoming === '') { if ($setting->exists && trim((string) $setting->value) !== '') { continue; } $incoming = self::DEPOSIT_DEFAULTS[$field]; } $setting->value = $incoming; if ($setting->title_fa === null || $setting->title_fa === '') { $setting->title_fa = self::depositFieldTitle($field); } if (! $setting->exists) { $setting->is_active = 1; $setting->can_edit = 0; } $setting->save(); } return self::getDepositInfo(); } private static function depositFieldTitle(string $field): string { return match ($field) { 'card_number' => 'شماره کارت شارژ پیامک', 'sheba' => 'شماره شبا شارژ پیامک', 'account_holder' => 'نام صاحب حساب شارژ پیامک', 'note' => 'متن راهنمای شارژ پیامک', default => $field, }; } public static function isSmsPanelSuperAdmin(?object $user): bool { if (! $user) { return false; } return self::normalizeMobile((string) ($user->mobile ?? '')) === self::SUPER_ADMIN_MOBILE; } public static function normalizeMobile(string $mobile): string { $digits = preg_replace('/\D/', '', $mobile) ?? ''; if (str_starts_with($digits, '98') && strlen($digits) === 12) { $digits = '0'.substr($digits, 2); } elseif (strlen($digits) === 10 && str_starts_with($digits, '9')) { $digits = '0'.$digits; } return $digits; } }