feat(projects): source+name dedup with human messages on create

This commit is contained in:
Дмитрий
2026-05-21 07:01:46 +03:00
parent f3250ce178
commit 5427cdc740
3 changed files with 118 additions and 0 deletions
@@ -213,6 +213,56 @@ class ProjectService
return ['updated' => $updated, 'skipped' => $skipped, 'warnings' => []];
}
private function assertNameUnique(int $tenantId, string $name, ?int $exceptId = null): void
{
$q = Project::where('tenant_id', $tenantId)->where('name', $name);
if ($exceptId !== null) {
$q->where('id', '!=', $exceptId);
}
if ($q->exists()) {
throw new HttpResponseException(response()->json([
'errors' => ['name' => ['Проект с таким названием у вас уже есть. Выберите другое название.']],
], 422));
}
}
/** @param array<string,mixed> $data */
private function assertSourceUnique(int $tenantId, array $data, ?int $exceptId = null): void
{
$signalType = $data['signal_type'] ?? null;
$q = Project::where('tenant_id', $tenantId)->where('signal_type', $signalType);
if ($exceptId !== null) {
$q->where('id', '!=', $exceptId);
}
if (in_array($signalType, ['call', 'site'], true)) {
$identifier = (string) ($data['signal_identifier'] ?? '');
if ($identifier === '') {
return;
}
$q->where('signal_identifier', $identifier);
} elseif ($signalType === 'sms') {
$senders = (array) ($data['sms_senders'] ?? []);
$norm = collect($senders)->map(fn ($s) => mb_strtolower(trim((string) $s)))->sort()->values()->all();
if ($norm === []) {
return;
}
$keyword = $data['sms_keyword'] ?? null;
$q->where('sms_keyword', $keyword)
->whereJsonContains('sms_senders', $norm)
->whereRaw('jsonb_array_length(sms_senders::jsonb) = ?', [count($norm)]);
} else {
return;
}
$existing = $q->first();
if ($existing !== null) {
throw new HttpResponseException(response()->json([
'errors' => ['signal_identifier' => ["У вас уже есть проект с этим источником: «{$existing->name}»."]],
], 422));
}
}
public function create(Tenant $tenant, array $data): Project
{
$limit = (int) ($tenant->limits['max_projects'] ?? 10);
@@ -230,6 +280,10 @@ class ProjectService
// PhonePrefixService / LeadRouter, удаляются в Plan 6.5 после переключения читателей.
$data['region_mask'] = 255;
$data['region_mode'] = 'include';
$this->assertNameUnique($tenant->id, (string) $data['name']);
$this->assertSourceUnique($tenant->id, $data);
$project = Project::create($data);
SyncSupplierProjectJob::dispatch($project->id);