fix/projects: нормализация телефона-источника + понятная ошибка и подсказка
Косяк 02: поле телефона проекта типа call отвергало +7.., 8.., пробелы и скобки. prepareForValidation в StoreProjectRequest и UpdateProjectRequest приводит номер через PhoneNormalizer к канону 7XXXXXXXXXX без ведущего плюса, чтобы раздача LeadRouter матчила без плюса. Финальная regex оставлена страховкой. Кастомные messages по signal_type: ошибка с примером формата, без имени Источник. Фронт: постоянная подсказка под полем в NewProjectDialog и ProjectDetailsDrawer. TDD: ProjectPhoneNormalizationTest 8 кейсов, GREEN. Проверено глазами на 8000. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\PricingTier;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
||||
PricingTier::query()->create([
|
||||
'tier_no' => 1,
|
||||
'leads_in_tier' => null,
|
||||
'price_per_lead_kopecks' => 5000, // 50₽/лид
|
||||
'is_active' => true,
|
||||
'effective_from' => now(),
|
||||
]);
|
||||
// Баланс заведомо большой — преflight всегда проходит, тест про нормализацию.
|
||||
$this->tenant = Tenant::factory()->withRequisites()->create(['balance_rub' => '100000.00']);
|
||||
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
||||
});
|
||||
|
||||
function createCallProject(string $rawPhone, string $name = 'Звонок'): TestResponse
|
||||
{
|
||||
return test()->actingAs(test()->user)->postJson('/api/projects', [
|
||||
'name' => $name,
|
||||
'signal_type' => 'call',
|
||||
'signal_identifier' => $rawPhone,
|
||||
'daily_limit_target' => 5,
|
||||
'regions' => [],
|
||||
'delivery_days_mask' => 127,
|
||||
]);
|
||||
}
|
||||
|
||||
it('normalizes +7 (916) 123-45-67 to 79161234567 on create', function () {
|
||||
createCallProject('+7 (916) 123-45-67')->assertCreated();
|
||||
expect(Project::where('tenant_id', $this->tenant->id)->value('signal_identifier'))
|
||||
->toBe('79161234567');
|
||||
});
|
||||
|
||||
it('normalizes leading 8 to 7 on create', function () {
|
||||
createCallProject('8 916 123 45 67')->assertCreated();
|
||||
expect(Project::where('tenant_id', $this->tenant->id)->value('signal_identifier'))
|
||||
->toBe('79161234567');
|
||||
});
|
||||
|
||||
it('normalizes hyphenated 7-916-123-45-67 on create', function () {
|
||||
createCallProject('7-916-123-45-67')->assertCreated();
|
||||
expect(Project::where('tenant_id', $this->tenant->id)->value('signal_identifier'))
|
||||
->toBe('79161234567');
|
||||
});
|
||||
|
||||
it('keeps already canonical 79161234567 unchanged (idempotent)', function () {
|
||||
createCallProject('79161234567')->assertCreated();
|
||||
expect(Project::where('tenant_id', $this->tenant->id)->value('signal_identifier'))
|
||||
->toBe('79161234567');
|
||||
});
|
||||
|
||||
it('never stores a leading plus (routing matches without +)', function () {
|
||||
createCallProject('+79161234567')->assertCreated();
|
||||
$stored = Project::where('tenant_id', $this->tenant->id)->value('signal_identifier');
|
||||
expect($stored)->toBe('79161234567')
|
||||
->and($stored)->not->toContain('+');
|
||||
});
|
||||
|
||||
it('rejects real garbage with a helpful message naming the on-screen field', function () {
|
||||
$response = createCallProject('12345');
|
||||
$response->assertStatus(422);
|
||||
$msg = $response->json('errors.signal_identifier.0');
|
||||
expect($msg)->toContain('79161234567') // показывает пример формата
|
||||
->and($msg)->not->toContain('Источник'); // не всплывает внутреннее имя поля
|
||||
});
|
||||
|
||||
it('does NOT normalize a site domain (only call is a phone)', function () {
|
||||
test()->actingAs($this->user)->postJson('/api/projects', [
|
||||
'name' => 'Сайт',
|
||||
'signal_type' => 'site',
|
||||
'signal_identifier' => 'my-site.ru',
|
||||
'daily_limit_target' => 5,
|
||||
'regions' => [],
|
||||
'delivery_days_mask' => 127,
|
||||
])->assertCreated();
|
||||
expect(Project::where('tenant_id', $this->tenant->id)->value('signal_identifier'))
|
||||
->toBe('my-site.ru');
|
||||
});
|
||||
|
||||
it('normalizes phone on update of a call project', function () {
|
||||
$project = Project::factory()->for($this->tenant)->create([
|
||||
'signal_type' => 'call',
|
||||
'signal_identifier' => '79990001122',
|
||||
'is_active' => true,
|
||||
'daily_limit_target' => 5,
|
||||
]);
|
||||
|
||||
test()->actingAs($this->user)->patchJson("/api/projects/{$project->id}", [
|
||||
'signal_identifier' => '+7 (916) 123-45-67',
|
||||
])->assertOk();
|
||||
|
||||
expect($project->fresh()->signal_identifier)->toBe('79161234567');
|
||||
});
|
||||
Reference in New Issue
Block a user