fix(смс): Теле2 не принимал имя отправителя в чужом написании
Первая клиентская рассылка на Теле2 упала: invalid_source_address. Причина не опечатка, а устройство: имена регистрируются У КАЖДОГО ОПЕРАТОРА ОТДЕЛЬНО, и написание разное — у МТС согласовано `liderra.ru`, у t2 `Liderra.ru`. Клиентская рассылка несёт ОДНО имя на всех получателей (campaign.sender_name): имя выбирается на кампанию, а не на канал. Для МТС оно верное, для t2 — нет. Проверено опытом на живом t2, а не рассуждением: имя [liderra.ru] → ОТКАЗ: invalid_source_address имя [Liderra.ru] → ПРИНЯТО, message-id-kDQU1vVzEMbT Лечение: канал приводит написание к согласованному с t2 — но ТОЛЬКО когда это то же самое имя, отличающееся регистром. Чужое имя не трогаем: подменить его значило бы соврать про отправителя, а честный отказ t2 лучше тихой подмены. Пустое имя, как и раньше, заменяется именем канала. 🪤 Лечение половинчатое и так и подписано в коде: правильное — реестр имён по каналам и в клиентском модуле (в модуле отдела продаж он уже есть, SalesSmsSender::activeByProviderKey). Тогда эта склейка станет не нужна. Четыре теста на разбор имени: тот же в другом регистре, точное совпадение, чужое имя, пустое. Всего 82 теста зелёные, Pint чист, статанализ по всему проекту 0 ошибок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,8 +54,7 @@ final class T2SmsProvider implements SmsDeliveryPayloadParser, SmsDeliveryReport
|
||||
|
||||
public function send(SmsOutgoing $message): SmsSendResult
|
||||
{
|
||||
// Имя отправителя: из сообщения (per-channel sender из БД), иначе дефолт канала.
|
||||
$shortcode = $message->senderName !== '' ? $message->senderName : $this->naming;
|
||||
$shortcode = $this->shortcodeFor($message->senderName);
|
||||
|
||||
try {
|
||||
$response = Http::withBasicAuth($this->login, $this->password)
|
||||
@@ -102,6 +101,38 @@ final class T2SmsProvider implements SmsDeliveryPayloadParser, SmsDeliveryReport
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Имя отправителя для t2.
|
||||
*
|
||||
* 🔴 Имена регистрируются У КАЖДОГО ОПЕРАТОРА ОТДЕЛЬНО, и написание бывает
|
||||
* разным: у МТС согласовано `liderra.ru`, у t2 — `Liderra.ru`. А клиентская
|
||||
* рассылка несёт ОДНО имя на всех (`campaign.sender_name`), потому что имя
|
||||
* там выбирается на кампанию, а не на канал (в модуле отдела продаж это уже
|
||||
* сделано по каналу — `SalesSmsSender::activeByProviderKey`).
|
||||
*
|
||||
* Проверено живьём на бою 04.08.2026: `liderra.ru` → отказ
|
||||
* `invalid_source_address`, `Liderra.ru` → принято.
|
||||
*
|
||||
* Поэтому приводим написание к согласованному с t2 — но ТОЛЬКО когда это то
|
||||
* же самое имя, отличающееся регистром. Чужое имя не трогаем: подменить его
|
||||
* значило бы соврать про отправителя, а честный отказ t2 лучше тихой подмены.
|
||||
*
|
||||
* 🪤 Это лечение половинчатое. Правильное — реестр имён по каналам и в
|
||||
* клиентском модуле; тогда эта склейка станет не нужна.
|
||||
*/
|
||||
private function shortcodeFor(string $requested): string
|
||||
{
|
||||
if ($requested === '') {
|
||||
return $this->naming;
|
||||
}
|
||||
|
||||
if ($this->naming !== '' && mb_strtolower($requested) === mb_strtolower($this->naming)) {
|
||||
return $this->naming;
|
||||
}
|
||||
|
||||
return $requested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Пакетного запроса у t2 НЕТ — спрашивать можно только по одному сообщению.
|
||||
* Единица здесь не осторожность, а форма их API: адрес опроса несёт номер
|
||||
|
||||
@@ -297,3 +297,49 @@ it('непонятную бумагу не разбирает — ноль за
|
||||
// Есть слово, но нет номера сообщения — правит нечего.
|
||||
->and(t2_provider()->parseDeliveryPayload(['status' => 'delivered']))->toBe([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* 🔴 Имена отправителей регистрируются У КАЖДОГО ОПЕРАТОРА ОТДЕЛЬНО, и написание
|
||||
* бывает разным: у МТС согласовано `liderra.ru`, у t2 — `Liderra.ru`.
|
||||
*
|
||||
* Клиентская рассылка несёт ОДНО имя на всех получателей (`campaign.sender_name`),
|
||||
* и для t2 оно приезжает в чужом написании. Живой опыт на бою 04.08.2026:
|
||||
* `liderra.ru` → отказ `invalid_source_address`, `Liderra.ru` → принято.
|
||||
*
|
||||
* Поэтому канал приводит написание к своему — но ТОЛЬКО когда это то же самое имя,
|
||||
* отличающееся регистром. Чужое имя не трогаем: подменить его значило бы соврать
|
||||
* про отправителя.
|
||||
*/
|
||||
it('имя отправителя приводит к написанию, согласованному с t2', function () {
|
||||
Http::fake(['target.t2.ru/*' => Http::response(['result' => ['uid' => 'u'], 'status' => 'ok'], 200)]);
|
||||
|
||||
t2_provider()->send(t2_message(sender: 'liderra'));
|
||||
|
||||
Http::assertSent(fn ($request) => $request['shortcode'] === 'Liderra');
|
||||
});
|
||||
|
||||
it('точное совпадение имени не трогает', function () {
|
||||
Http::fake(['target.t2.ru/*' => Http::response(['result' => ['uid' => 'u'], 'status' => 'ok'], 200)]);
|
||||
|
||||
t2_provider()->send(t2_message(sender: 'Liderra'));
|
||||
|
||||
Http::assertSent(fn ($request) => $request['shortcode'] === 'Liderra');
|
||||
});
|
||||
|
||||
it('чужое имя отправителя НЕ подменяет', function () {
|
||||
// Когда у клиента появится своё согласованное имя, канал обязан слать его,
|
||||
// а не наше. Отказ t2 в этом случае — честный ответ, а не повод врать.
|
||||
Http::fake(['target.t2.ru/*' => Http::response(['result' => ['uid' => 'u'], 'status' => 'ok'], 200)]);
|
||||
|
||||
t2_provider()->send(t2_message(sender: 'ChuzhoeImya'));
|
||||
|
||||
Http::assertSent(fn ($request) => $request['shortcode'] === 'ChuzhoeImya');
|
||||
});
|
||||
|
||||
it('пустое имя заменяет своим', function () {
|
||||
Http::fake(['target.t2.ru/*' => Http::response(['result' => ['uid' => 'u'], 'status' => 'ok'], 200)]);
|
||||
|
||||
t2_provider()->send(t2_message(sender: ''));
|
||||
|
||||
Http::assertSent(fn ($request) => $request['shortcode'] === 'Liderra');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user