51b8f501ae
Жизненный цикл имени: клиент requestSender (freeze первого месяца) / disableSender; админ approve (charge+release, active, paid_until+1мес) / reject / disable. Помесячная оплата ChargeSmsNameFeeJob (проверка средств ДО списания, долг>29 дней→suspended, идемпотентно по external_key), расписание в console.php. Авто-рассылка: DealSmsObserver (защитный, freshness-guard, НИКОГДА не роняет приём лида) → SendAutoSmsForDealJob (best-effort, идемпотентно по deal_id, песочница/деньги/маршрут). Действующее имя кампании = active-имя тенанта иначе liderra.ru. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
196 lines
7.9 KiB
PHP
196 lines
7.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\AdWallet;
|
||
use App\Models\ClientSmsCampaign;
|
||
use App\Models\ClientSmsContact;
|
||
use App\Models\ClientSmsSender;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use App\Services\Advertising\AdWalletService;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Illuminate\Support\Facades\Queue;
|
||
|
||
/**
|
||
* Task 13a: клиентский «свой отправитель» (client_sms_senders) — заявка на имя,
|
||
* бронь платы в рекламном кошельке (AdWalletService), отключение имени и
|
||
* прокидывание активного имени в снимок sender_name кампании.
|
||
*
|
||
* Auth/tenant setup зеркалит EndpointTest: Tenant::factory + User::factory(tenant_id)
|
||
* + actingAs; middleware ['auth:sanctum','tenant']. Настройки (fee 2500.00)
|
||
* сидированы миграцией client_sms_settings. Синтетические номера 7999… — реальные НИКОГДА.
|
||
*/
|
||
uses(RefreshDatabase::class);
|
||
|
||
beforeEach(function () {
|
||
$this->tenant = Tenant::factory()->create();
|
||
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
||
$this->actingAs($this->user);
|
||
});
|
||
|
||
it('GET /api/sms/sender без имени → sender:null, effective_name = liderra.ru', function () {
|
||
config(['services.sms.mts.naming' => '']);
|
||
|
||
$r = $this->getJson('/api/sms/sender');
|
||
|
||
$r->assertOk()
|
||
->assertJsonPath('sender', null)
|
||
->assertJsonPath('effective_name', 'liderra.ru')
|
||
->assertJsonPath('name_fee_rub_per_operator', '2500.00');
|
||
});
|
||
|
||
it('POST /api/sms/sender (sandbox) → заявка pending, плата 2500.00, кошелёк не тронут', function () {
|
||
config(['services.sms.sandbox' => true]);
|
||
|
||
$r = $this->postJson('/api/sms/sender', [
|
||
'name' => 'mysite.ru',
|
||
'name_type' => 'website',
|
||
'consent' => true,
|
||
]);
|
||
|
||
$r->assertSuccessful()
|
||
->assertJsonPath('status', ClientSmsSender::STATUS_PENDING);
|
||
|
||
$sender = ClientSmsSender::where('tenant_id', $this->tenant->id)->first();
|
||
expect($sender)->not->toBeNull();
|
||
expect($sender->status)->toBe(ClientSmsSender::STATUS_PENDING);
|
||
expect($sender->name)->toBe('mysite.ru');
|
||
expect($sender->name_type)->toBe(ClientSmsSender::TYPE_WEBSITE);
|
||
expect($sender->operators)->toBe(['mts']);
|
||
expect($sender->monthly_fee_rub)->toBe('2500.00');
|
||
expect($sender->consent_at)->not->toBeNull();
|
||
|
||
// Sandbox: заморозки нет — кошелёк не создан.
|
||
expect(AdWallet::where('tenant_id', $this->tenant->id)->exists())->toBeFalse();
|
||
});
|
||
|
||
it('POST /api/sms/sender (реальный режим, денег хватает) → pending + бронь 2500.00 в кошельке', function () {
|
||
config(['services.sms.sandbox' => false]);
|
||
|
||
app(AdWalletService::class)->topup($this->tenant->id, '5000.00', null, 'test');
|
||
|
||
$r = $this->postJson('/api/sms/sender', [
|
||
'name' => 'mysite.ru',
|
||
'name_type' => 'website',
|
||
'consent' => true,
|
||
]);
|
||
|
||
$r->assertSuccessful()
|
||
->assertJsonPath('status', ClientSmsSender::STATUS_PENDING);
|
||
|
||
$wallet = AdWallet::where('tenant_id', $this->tenant->id)->first();
|
||
expect($wallet->frozen_rub)->toBe('2500.00');
|
||
});
|
||
|
||
it('POST /api/sms/sender (реальный режим, денег НЕ хватает) → 409 и заявка не сохранена', function () {
|
||
config(['services.sms.sandbox' => false]);
|
||
|
||
app(AdWalletService::class)->topup($this->tenant->id, '100.00', null, 'test');
|
||
|
||
$r = $this->postJson('/api/sms/sender', [
|
||
'name' => 'mysite.ru',
|
||
'name_type' => 'website',
|
||
'consent' => true,
|
||
]);
|
||
|
||
$r->assertStatus(409);
|
||
|
||
// Заявка откачена (nested tx) — сироты pending нет.
|
||
expect(ClientSmsSender::where('tenant_id', $this->tenant->id)->count())->toBe(0);
|
||
|
||
// Бронь не появилась.
|
||
$wallet = AdWallet::where('tenant_id', $this->tenant->id)->first();
|
||
expect($wallet->frozen_rub)->toBe('0.00');
|
||
});
|
||
|
||
it('POST /api/sms/sender при ACTIVE имени → 422', function () {
|
||
config(['services.sms.sandbox' => true]);
|
||
|
||
ClientSmsSender::create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'name' => 'Активное', 'name_type' => ClientSmsSender::TYPE_COMPANY,
|
||
'status' => ClientSmsSender::STATUS_ACTIVE, 'operators' => ['mts'],
|
||
'monthly_fee_rub' => '2500.00',
|
||
]);
|
||
|
||
$this->postJson('/api/sms/sender', [
|
||
'name' => 'другое.ru', 'name_type' => 'website', 'consent' => true,
|
||
])->assertStatus(422)
|
||
->assertJsonPath('message', 'У вас уже есть активное имя отправителя');
|
||
});
|
||
|
||
it('POST /api/sms/sender при PENDING заявке → 422', function () {
|
||
config(['services.sms.sandbox' => true]);
|
||
|
||
ClientSmsSender::create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'name' => 'На рассмотрении', 'name_type' => ClientSmsSender::TYPE_WEBSITE,
|
||
'status' => ClientSmsSender::STATUS_PENDING, 'operators' => ['mts'],
|
||
'monthly_fee_rub' => '2500.00',
|
||
]);
|
||
|
||
$this->postJson('/api/sms/sender', [
|
||
'name' => 'другое.ru', 'name_type' => 'website', 'consent' => true,
|
||
])->assertStatus(422)
|
||
->assertJsonPath('message', 'Заявка на имя уже на рассмотрении');
|
||
});
|
||
|
||
it('store: кампания снимает АКТИВНОЕ имя отправителя, иначе liderra.ru', function () {
|
||
config(['services.sms.sandbox' => true]);
|
||
config(['services.sms.mts.naming' => '']);
|
||
Queue::fake();
|
||
|
||
ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001', 'name' => 'А', 'operator' => 'mts']);
|
||
|
||
// Без активного имени → liderra.ru.
|
||
$r1 = $this->postJson('/api/sms/campaigns', [
|
||
'title' => 'Без имени', 'body' => 'Привет', 'source' => 'base',
|
||
]);
|
||
$r1->assertCreated();
|
||
expect(ClientSmsCampaign::find($r1->json('id'))->sender_name)->toBe('liderra.ru');
|
||
|
||
// Появилось активное имя → снимок MyBrand.
|
||
ClientSmsSender::create([
|
||
'tenant_id' => $this->tenant->id,
|
||
'name' => 'MyBrand', 'name_type' => ClientSmsSender::TYPE_COMPANY,
|
||
'status' => ClientSmsSender::STATUS_ACTIVE, 'operators' => ['mts'],
|
||
'monthly_fee_rub' => '2500.00',
|
||
]);
|
||
|
||
$r2 = $this->postJson('/api/sms/campaigns', [
|
||
'title' => 'С именем', 'body' => 'Привет', 'source' => 'base',
|
||
]);
|
||
$r2->assertCreated();
|
||
expect(ClientSmsCampaign::find($r2->json('id'))->sender_name)->toBe('MyBrand');
|
||
|
||
// index тоже отдаёт активное имя.
|
||
$this->getJson('/api/sms/campaigns')->assertOk()->assertJsonPath('sender_name', 'MyBrand');
|
||
});
|
||
|
||
it('POST /api/sms/sender/disable по PENDING → cancelled и бронь снята (frozen_rub → 0)', function () {
|
||
config(['services.sms.sandbox' => false]);
|
||
|
||
app(AdWalletService::class)->topup($this->tenant->id, '5000.00', null, 'test');
|
||
|
||
// Заводим заявку с бронью через реальный флоу.
|
||
$this->postJson('/api/sms/sender', [
|
||
'name' => 'mysite.ru', 'name_type' => 'website', 'consent' => true,
|
||
])->assertSuccessful();
|
||
|
||
expect(AdWallet::where('tenant_id', $this->tenant->id)->first()->frozen_rub)->toBe('2500.00');
|
||
|
||
$r = $this->postJson('/api/sms/sender/disable');
|
||
$r->assertOk()->assertJsonPath('disabled', true);
|
||
|
||
$sender = ClientSmsSender::where('tenant_id', $this->tenant->id)->first();
|
||
expect($sender->status)->toBe(ClientSmsSender::STATUS_CANCELLED);
|
||
|
||
// Бронь освобождена.
|
||
expect(AdWallet::where('tenant_id', $this->tenant->id)->first()->frozen_rub)->toBe('0.00');
|
||
});
|
||
|
||
it('POST /api/sms/sender/disable без имени → 404', function () {
|
||
$this->postJson('/api/sms/sender/disable')->assertStatus(404);
|
||
});
|