diff --git a/app/app/Services/Sms/Providers/BeelineSmsProvider.php b/app/app/Services/Sms/Providers/BeelineSmsProvider.php index f6923e28..f40ad6c1 100644 --- a/app/app/Services/Sms/Providers/BeelineSmsProvider.php +++ b/app/app/Services/Sms/Providers/BeelineSmsProvider.php @@ -263,6 +263,19 @@ final class BeelineSmsProvider implements SmsDeliveryPayloadParser, SmsDeliveryR throw new SmsSendException($e->getMessage(), terminal: false); } + // 🪤 Та же ловушка, что при отправке: платформа отвечает кодом 200 и на + // отказ, а слова кладёт в тело. Промолчать здесь опаснее, чем при отправке: + // сломается ключ — судьба сообщений просто перестанет обновляться, и + // пустой ответ будет неотличим от честного «такого сообщения нет». + // А за недоставленное клиенту возвращаются рубли (В-198), то есть + // молчание = невозвращённые деньги. + if ($this->kodOshibki($otvet) !== '') { + throw new SmsSendException( + 'Билайн отказал в опросе судьбы: '.$this->tekstOshibki($otvet), + terminal: false, + ); + } + return $this->parseDeliveryPayload($otvet); } diff --git a/app/tests/Unit/Sms/BeelineSmsProviderTest.php b/app/tests/Unit/Sms/BeelineSmsProviderTest.php index d2c422ce..ab155c6d 100644 --- a/app/tests/Unit/Sms/BeelineSmsProviderTest.php +++ b/app/tests/Unit/Sms/BeelineSmsProviderTest.php @@ -218,6 +218,36 @@ it('незнакомое слово о судьбе не угадывает', fu ->and($reports[0]->rawState)->toBe('nechto-novoe'); }); +it('отказ платформы при опросе судьбы не проглатывает молча', function () { + // 🪤 Билайн отдаёт отказ кодом ответа 200, а слова кладёт в тело. Раньше + // разбор поля `error` не смотрел и возвращал пустоту, неотличимую от честного + // «такого сообщения нет»: сломался бы ключ — судьба перестала бы обновляться + // МОЛЧА, а за недоставленное клиенту возвращаются рубли (В-198). + Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response([ + 'error' => ['code' => '-20107', 'message' => 'Ошибка авторизации'], + ], 200)]); + + try { + bee_provider()->fetchDelivery(['508909732338141600334']); + throw new RuntimeException('ожидалось SmsSendException, но опрос прошёл молча'); + } catch (SmsSendException $e) { + // Судьбу всегда можно переспросить следующим заходом — отказ здесь + // НЕ окончательный, в отличие от отправки. + expect($e->terminal)->toBeFalse() + ->and($e->getMessage())->toContain('20107') + ->and($e->getMessage())->toContain('Ошибка авторизации'); + } +}); + +it('честное «такого сообщения нет» отказом не считает', function () { + // Обратная сторона: ответ без отказа и без действий — это НЕ поломка, а + // «платформа про такое сообщение ничего не знает». Ругаться здесь значило бы + // сорвать весь заход опроса из-за одного забытого номера. + Http::fake(['a2p-sms-https.beeline.ru/*' => Http::response(['actions' => []], 200)]); + + expect(bee_provider()->fetchDelivery(['1']))->toBe([]); +}); + it('принятое оператором и отклонённое им разводит по разным судьбам', function () { $slova = ['accepted', 'rejected', 'undeliverable', 'aborted'];