8177cac39b
Этап 5, Task 1. Кода, который что-то меняет в базе или в деньгах, здесь нет — только новая способность канала и её читатель для МТС. Что появилось: - SmsDeliveryState — словарь судеб ПОРТАЛА (везу / доставлено / не доставлено / оператор не отправил). Один дом на все каналы: у МТС свои слова, у следующего оператора будут другие, а экран и деньги обязаны говорить одним языком. - SmsDeliveryReport — что канал рассказал про одно сообщение. - SmsDeliveryReporter — НЕОБЯЗАТЕЛЬНАЯ способность канала. Отдельно от SmsProvider намеренно: канал, умеющий только отправлять, остаётся годным каналом. Когда у Т2, Мегафона и Билайна появятся кабинеты, их файл реализует этот разъём — и журнал, экран, деньги, итоги не тронутся ни строчкой (решение владельца 31.07). - MtsSmsProvider::fetchDelivery — читатель, до 1000 номеров сообщений за запрос. Форма ответа взята из ЖИВОЙ пробы 30.07 (журнал В-211), а НЕ из документации МТС: там описан вход по логину-паролю (events_info), а мы ходим по токену и получаем data[].statuses[]. Тест на выдуманной форме охранял бы выдумку (В-72). Незнакомое слово оператора НЕ угадывается: судьба остаётся пустой, слово уходит в журнал. Угадать значило бы соврать про деньги — за «не доставлено» клиенту возвращаются рубли. Цена МТС (cost) сохраняется как есть, в рубли не переводится: единицы поля неизвестны, выдумывать ответ внешней стороны запрещено. Проверено: 7 тестов зелёные, оба выреза покраснили ровно те тесты, что должны (подмена NotSent на «не доставлено» и угадывание незнакомого слова). Соседи — 36/36. phpstan — ровно 2 чужие давние, pint чисто.
128 lines
5.3 KiB
PHP
128 lines
5.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Services\Sms\Providers\MtsSmsProvider;
|
||
use App\Services\Sms\SmsDeliveryState;
|
||
use App\Services\Sms\SmsSendException;
|
||
use Illuminate\Support\Facades\Http;
|
||
|
||
/**
|
||
* Строка листа 5.1: по каждому номеру видна настоящая судьба.
|
||
*
|
||
* 🔴 Форма ответа взята из ЖИВОЙ пробы 30.07 (журнал В-211), а НЕ из документации
|
||
* МТС: там описан вход по логину-паролю (`events_info` с `message_id`/`event_at`),
|
||
* а мы ходим по токену и получаем `data[].statuses[]`. Тест на выдуманной форме
|
||
* охранял бы выдумку (урок В-72: записанный чужой вывод — гипотеза, не факт).
|
||
*
|
||
* Помощники с префиксом mtsDelivery* — имена функций в Pest ГЛОБАЛЬНЫЕ.
|
||
* Телефоны только синтетические 7999… — реальные НИКОГДА.
|
||
*/
|
||
function mtsDeliveryProvider(): MtsSmsProvider
|
||
{
|
||
return new MtsSmsProvider('токен', ['mts'], ['*' => 300], 'https://api.mts.test/v1');
|
||
}
|
||
|
||
/**
|
||
* Ответ МТС на вопрос о судьбе — ровно той формы, что пришла живьём.
|
||
*
|
||
* @param array<int, array<string, mixed>> $statuses
|
||
* @return array<string, mixed>
|
||
*/
|
||
function mtsDeliveryBody(string $messageId, array $statuses): array
|
||
{
|
||
return ['code' => 0, 'data' => [['messageID' => $messageId, 'statuses' => $statuses]]];
|
||
}
|
||
|
||
it('читает доставленное сообщение по живой форме ответа', function () {
|
||
Http::fake(['*' => Http::response(mtsDeliveryBody('44747238', [[
|
||
'msid' => '79990000001',
|
||
'status' => 'Delivered',
|
||
'date' => '2026-07-30T16:31:02',
|
||
'userDeliveryDate' => '2026-07-30T16:31:40',
|
||
'partCount' => 2,
|
||
'isViber' => false,
|
||
'trafficPatternType' => 'service',
|
||
'cost' => 3.4,
|
||
]]))]);
|
||
|
||
$reports = mtsDeliveryProvider()->fetchDelivery(['44747238']);
|
||
|
||
expect($reports)->toHaveCount(1);
|
||
expect($reports[0]->providerMessageId)->toBe('44747238');
|
||
expect($reports[0]->phone)->toBe('79990000001');
|
||
expect($reports[0]->state)->toBe(SmsDeliveryState::DELIVERED);
|
||
expect($reports[0]->deliveredAt?->toDateTimeString())->toBe('2026-07-30 16:31:40');
|
||
expect($reports[0]->partCount)->toBe(2);
|
||
expect($reports[0]->providerCost)->toBe('3.4');
|
||
});
|
||
|
||
it('читает «принял, но не отправил» — тот самый случай живой пробы 30.07', function () {
|
||
Http::fake(['*' => Http::response(mtsDeliveryBody('44747239', [[
|
||
'msid' => '79990000002',
|
||
'status' => 'NotSent',
|
||
'date' => '2026-07-30T16:31:02',
|
||
'userDeliveryDate' => null,
|
||
'partCount' => 1,
|
||
'cost' => 0,
|
||
]]))]);
|
||
|
||
$reports = mtsDeliveryProvider()->fetchDelivery(['44747239']);
|
||
|
||
expect($reports[0]->state)->toBe(SmsDeliveryState::NOT_SENT);
|
||
expect($reports[0]->deliveredAt)->toBeNull();
|
||
expect($reports[0]->providerCost)->toBe('0');
|
||
});
|
||
|
||
it('читает «везу» и «не довёз» как разные судьбы', function () {
|
||
Http::fake(['*' => Http::response(['code' => 0, 'data' => [
|
||
['messageID' => '1', 'statuses' => [['msid' => '79990000003', 'status' => 'Sending']]],
|
||
['messageID' => '2', 'statuses' => [['msid' => '79990000004', 'status' => 'NotDelivered']]],
|
||
]])]);
|
||
|
||
$reports = mtsDeliveryProvider()->fetchDelivery(['1', '2']);
|
||
|
||
expect($reports[0]->state)->toBe(SmsDeliveryState::SENDING);
|
||
expect($reports[1]->state)->toBe(SmsDeliveryState::NOT_DELIVERED);
|
||
});
|
||
|
||
it('незнакомое слово оператора не угадывает, а отдаёт пустую судьбу и само слово', function () {
|
||
Http::fake(['*' => Http::response(mtsDeliveryBody('1', [[
|
||
'msid' => '79990000005',
|
||
'status' => 'СовершенноНовоеСлово',
|
||
'partCount' => 1,
|
||
]]))]);
|
||
|
||
$reports = mtsDeliveryProvider()->fetchDelivery(['1']);
|
||
|
||
expect($reports[0]->state)->toBeNull();
|
||
expect($reports[0]->rawState)->toBe('СовершенноНовоеСлово');
|
||
});
|
||
|
||
it('спрашивает номера сообщений одной строкой через запятую', function () {
|
||
Http::fake(['*' => Http::response(['code' => 0, 'data' => []])]);
|
||
|
||
mtsDeliveryProvider()->fetchDelivery(['1', '2', '3']);
|
||
|
||
Http::assertSent(fn ($request) => str_contains(urldecode($request->url()), 'messageIDs=1,2,3'));
|
||
});
|
||
|
||
it('пустой список никого не спрашивает вовсе', function () {
|
||
Http::fake(['*' => Http::response(['code' => 0, 'data' => []])]);
|
||
|
||
expect(mtsDeliveryProvider()->fetchDelivery([]))->toBe([]);
|
||
|
||
Http::assertNothingSent();
|
||
});
|
||
|
||
it('отказ МТС — не окончательный: судьбу всегда можно переспросить позже', function () {
|
||
Http::fake(['*' => Http::response('', 502)]);
|
||
|
||
try {
|
||
mtsDeliveryProvider()->fetchDelivery(['1']);
|
||
$this->fail('ожидали отказ канала');
|
||
} catch (SmsSendException $e) {
|
||
expect($e->terminal)->toBeFalse();
|
||
}
|
||
});
|