f7963bcfb3
Косяк 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>
77 lines
3.6 KiB
PHP
77 lines
3.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Http\Requests;
|
||
|
||
use App\Support\PhoneNormalizer;
|
||
use Illuminate\Foundation\Http\FormRequest;
|
||
use Illuminate\Validation\Rule;
|
||
|
||
class StoreProjectRequest extends FormRequest
|
||
{
|
||
public function authorize(): bool
|
||
{
|
||
return $this->user() !== null;
|
||
}
|
||
|
||
/**
|
||
* Косяк 02: для типа «call» приводим введённый номер к каноничному виду
|
||
* 7XXXXXXXXXX тем же нормализатором, что и реквизиты (PhoneNormalizer).
|
||
* Источник проекта хранится без ведущего «+» — раздача лидов матчит
|
||
* signal_identifier как есть (LeadRouter), поэтому «+» срезаем.
|
||
* Невалидный мусор оставляем как ввели — финальная regex даст ошибку.
|
||
*/
|
||
protected function prepareForValidation(): void
|
||
{
|
||
if ($this->input('signal_type') === 'call' && $this->filled('signal_identifier')) {
|
||
$normalized = PhoneNormalizer::normalize((string) $this->input('signal_identifier'));
|
||
if ($normalized !== null) {
|
||
$this->merge(['signal_identifier' => ltrim($normalized, '+')]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** @return array<string, string> */
|
||
public function messages(): array
|
||
{
|
||
return match ($this->input('signal_type')) {
|
||
'call' => ['signal_identifier.regex' => 'Введите номер в формате 79161234567 — цифра 7 и 10 цифр после неё. Можно вводить с +7, 8, скобками и пробелами — приведём сами.'],
|
||
'site' => ['signal_identifier.regex' => 'Введите домен в формате example.ru — без http://, без www и без пути.'],
|
||
default => [],
|
||
};
|
||
}
|
||
|
||
public function rules(): array
|
||
{
|
||
$signalType = $this->input('signal_type');
|
||
|
||
$base = [
|
||
'name' => ['required', 'string', 'max:255'],
|
||
'signal_type' => ['required', Rule::in(['site', 'call', 'sms'])],
|
||
'daily_limit_target' => ['required', 'integer', 'min:1', 'max:10000'],
|
||
// Plan 6: subject-level regions[] заменил region_mask/region_mode на API-уровне.
|
||
// Empty array = "вся РФ" (паритет с legacy region_mask=255 + region_mode='include').
|
||
// present = поле должно быть в payload (даже если []), enforces explicit choice.
|
||
'regions' => ['present', 'array'],
|
||
'regions.*' => ['integer', 'between:1,89'],
|
||
'delivery_days_mask' => ['required', 'integer', 'min:1', 'max:127'],
|
||
// Spec C §3.4: при перегрузке преfflight UI шлёт force_save_blocked=true →
|
||
// проект создаётся с preflight_blocked_at=now() вместо ответа 409.
|
||
'force_save_blocked' => ['sometimes', 'boolean'],
|
||
];
|
||
|
||
if ($signalType === 'site') {
|
||
$base['signal_identifier'] = ['required', 'string', 'regex:/^[a-z0-9][a-z0-9\-]*(\.[a-z0-9][a-z0-9\-]*)*\.[a-z]{2,}$/i'];
|
||
} elseif ($signalType === 'call') {
|
||
$base['signal_identifier'] = ['required', 'string', 'regex:/^7\d{10}$/'];
|
||
} elseif ($signalType === 'sms') {
|
||
$base['sms_senders'] = ['required', 'array', 'min:1'];
|
||
$base['sms_senders.*'] = ['string', 'max:11'];
|
||
$base['sms_keyword'] = ['nullable', 'string', 'min:1', 'max:50'];
|
||
}
|
||
|
||
return $base;
|
||
}
|
||
}
|