*/ private array $providers; public function __construct( private SmsWalletService $walletService, private SmsSegmentCalculator $segmentCalculator, private SmsAlertService $alertService, private SuperAdminSmsClient $superAdminSms, MeliPayamakProvider $meliPayamakProvider, CentralSmsApiProvider $centralSmsApiProvider, ) { $this->providers = [ $meliPayamakProvider, $centralSmsApiProvider, ]; } public function send( string $to, string $message, ?string $source = null, bool $isAlert = false ): SmsSendResult { $config = $this->buildProviderConfig(); $useSuperAdmin = $this->superAdminSms->enabled(); $billingOperational = SmsWalletService::isBillingOperational() || $useSuperAdmin; $text = trim($message); if ($text === '') { return SmsSendResult::failed('متن پیامک خالی است.'); } $partsEstimate = max(1, $this->segmentCalculator->countParts($text)); if ($billingOperational && ! $this->maySend($partsEstimate, $isAlert)) { return SmsSendResult::blocked(); } $provider = $this->resolveProvider($config); if (! $provider) { Log::warning('SmsGateway: no SMS provider configured'); return SmsSendResult::failed('سرویس پیامک پیکربندی نشده است.'); } try { $result = $provider->send($to, $text, $config); if (! ($result['success'] ?? false)) { Log::warning('SmsGateway: provider send failed', [ 'provider' => $this->providerLabel($provider), 'to' => $to, 'raw' => $result['raw'] ?? null, ]); return SmsSendResult::failed('ارسال پیامک ناموفق بود.'); } $parts = $this->segmentCalculator->countParts($text); if ($parts <= 0) { return SmsSendResult::failed('محاسبه پارت پیامک نامعتبر است.'); } $providerName = $this->providerLabel($provider); if ($billingOperational) { $this->billAfterSuccess($to, $text, $parts, $providerName, $source, $isAlert); } return SmsSendResult::success($providerName); } catch (\Throwable $e) { Log::error('SmsGateway: exception during send', [ 'to' => $to, 'message' => $e->getMessage(), ]); return SmsSendResult::failed($e->getMessage()); } } public function sendOrThrow( string $to, string $message, ?string $source = null, bool $isAlert = false ): SmsSendResult { $result = $this->send($to, $message, $source, $isAlert); if ($result->blocked) { throw new SmsBlockedException(); } return $result; } private function maySend(int $parts, bool $isAlert): bool { if ($this->superAdminSms->enabled()) { $status = $this->superAdminSms->status($parts, $isAlert); if ($status === null) { return ! $this->walletService->isBlocked(); } return (bool) $status['allowed']; } return ! $this->walletService->isBlocked(); } private function billAfterSuccess( string $to, string $text, int $parts, string $providerName, ?string $source, bool $isAlert ): void { if ($this->superAdminSms->enabled()) { $consumed = $this->superAdminSms->consume( $to, $text, $parts, $providerName, $source, $isAlert ); if ($consumed === null) { Log::error('SmsGateway: SuperAdmin consume failed after successful send', [ 'to' => $to, 'parts' => $parts, 'source' => $source, ]); return; } // Parallel period: mirror balance + local send log/ledger from SuperAdmin. try { $this->walletService->mirrorConsumeFromSuperAdmin( $to, $text, $parts, $providerName, $source, $isAlert, (int) ($consumed['balanceRials'] ?? 0), (int) ($consumed['costRials'] ?? 0), (int) ($consumed['pricePerPartRials'] ?? 0) ); } catch (\Throwable $e) { Log::warning('SmsGateway: local mirror after SuperAdmin consume failed', [ 'error' => $e->getMessage(), ]); } return; } $debit = $this->walletService->debitSuccessfulSms( $to, $text, $parts, $providerName, $source, $isAlert ); if (! $isAlert) { $this->alertService->maybeSendLowBalanceAlerts($debit['wallet']); } } private function buildProviderConfig(): array { $config = app(ConfigService::class); return [ 'melipayamak_user' => $config->getString('melipayamak_user'), 'melipayamak_password' => $config->getString('melipayamak_password'), 'melipayamak_from' => $config->getString('melipayamak_from'), 'name_fa' => $config->getString('name_fa'), 'name_en' => $config->getString('name_en'), ]; } private function resolveProvider(array $config): ?SmsProviderInterface { $meliUser = trim((string) ($config['melipayamak_user'] ?? '')); if (strlen($meliUser) > 1) { return $this->providerByClass(MeliPayamakProvider::class); } return $this->providerByClass(CentralSmsApiProvider::class); } private function providerByClass(string $class): ?SmsProviderInterface { foreach ($this->providers as $provider) { if ($provider instanceof $class) { return $provider; } } return null; } private function providerLabel(SmsProviderInterface $provider): string { return $provider->name(); } }