From bbea378bf149ef81ddb81bbbf6ae2a53e0a94d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 23 Jul 2026 11:59:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(=D1=81=D0=BC=D1=81):=20=D0=BA=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=A1=D0=9C=D0=A1-=D1=86=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=80=20(smsc.ru)=20=E2=80=94=20=D1=83=D0=BD=D0=B8=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sms/Providers/SmscSmsProvider.php | 84 +++++++++++++++++++ app/tests/Unit/Sms/SmscSmsProviderTest.php | 56 +++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 app/app/Services/Sms/Providers/SmscSmsProvider.php create mode 100644 app/tests/Unit/Sms/SmscSmsProviderTest.php diff --git a/app/app/Services/Sms/Providers/SmscSmsProvider.php b/app/app/Services/Sms/Providers/SmscSmsProvider.php new file mode 100644 index 00000000..b55904c5 --- /dev/null +++ b/app/app/Services/Sms/Providers/SmscSmsProvider.php @@ -0,0 +1,84 @@ + $serves + * @param array $priceKopecks + */ +final class SmscSmsProvider implements SmsProvider +{ + private const SEND_URL = 'https://smsc.ru/sys/send.php'; + + public function __construct( + private readonly string $login, + private readonly string $password, + private readonly array $serves, + private readonly array $priceKopecks, + ) {} + + public function key(): string + { + return 'smsc'; + } + + public function servesOperators(): array + { + return $this->serves; + } + + public function priceKopecks(string $operator): int + { + return $this->priceKopecks[$operator] ?? $this->priceKopecks['*'] ?? 0; + } + + public function send(SmsOutgoing $message): SmsSendResult + { + try { + $response = Http::get(self::SEND_URL, [ + 'login' => $this->login, + 'psw' => $this->password, + 'phones' => $message->phone, + 'mes' => $message->body, + 'sender' => $message->senderName, + 'charset' => 'utf-8', + 'fmt' => 3, + ]); + } catch (Throwable $e) { + throw new SmsSendException('smsc.ru недоступен: '.$e->getMessage(), terminal: false); + } + + if ($response->serverError()) { + throw new SmsSendException('smsc.ru вернул '.$response->status(), terminal: false); + } + + $body = (array) $response->json(); + + if (isset($body['error'])) { + throw new SmsSendException( + 'smsc.ru отказал: '.(string) $body['error'], + terminal: true, + ); + } + + return new SmsSendResult( + providerMessageId: (string) ($body['id'] ?? ''), + segments: $message->segments, + costKopecks: $this->priceKopecks($message->operator) * $message->segments, + acceptedAt: CarbonImmutable::now(), + ); + } +} diff --git a/app/tests/Unit/Sms/SmscSmsProviderTest.php b/app/tests/Unit/Sms/SmscSmsProviderTest.php new file mode 100644 index 00000000..90b3360f --- /dev/null +++ b/app/tests/Unit/Sms/SmscSmsProviderTest.php @@ -0,0 +1,56 @@ + 300]); +} + +function smsc_message(): SmsOutgoing +{ + return new SmsOutgoing( + phone: '79990000001', + body: 'Здравствуйте!', + senderName: 'liderra.ru', + operator: 'МегаФон', + segments: 1, + ); +} + +it('обслуживает всех как универсал', function () { + expect(smsc_provider()->servesOperators())->toBe(['*']) + ->and(smsc_provider()->key())->toBe('smsc'); +}); + +it('успешная отправка возвращает id сообщения и цену по сегментам', function () { + Http::fake(['smsc.ru/*' => Http::response(['id' => 555, 'cnt' => 1], 200)]); + + $result = smsc_provider()->send(smsc_message()); + + expect($result->providerMessageId)->toBe('555') + ->and($result->costKopecks)->toBe(300); +}); + +it('отказ smsc.ru бросает terminal-исключение', function () { + Http::fake(['smsc.ru/*' => Http::response(['error' => 'invalid number', 'error_code' => 7], 200)]); + + expect(fn () => smsc_provider()->send(smsc_message())) + ->toThrow(SmsSendException::class); +}); + +it('сетевой сбой — не terminal (можно повторить)', function () { + Http::fake(['smsc.ru/*' => Http::response('', 500)]); + + try { + smsc_provider()->send(smsc_message()); + $this->fail('ожидалось SmsSendException'); + } catch (SmsSendException $e) { + expect($e->terminal)->toBeFalse(); + } +});