8220a85a5d
Refactor ProjectService::bulkAction to accept full payload array and
return structured {updated, skipped, warnings}. Add bulkUpdateRegions
using PG raw bitmask expr (region_mask | add) & ~remove & 255.
Add stubs for bulkUpdateDays/bulkUpdateLimit (Tasks 3-4). Update
controller to pass merged payload and return service result directly.
Un-todo Task-1 region validation test; add regions bitmask test (18/20).
Update phpstan-baseline: actingAs count 5->6, restore match.unhandled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
4.4 KiB
PHP
131 lines
4.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Services\Project;
|
||
|
||
use App\Jobs\SyncSupplierProjectJob;
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||
|
||
class ProjectService
|
||
{
|
||
public function update(Project $project, array $data): Project
|
||
{
|
||
// Immutable fields — silently drop (don't 422)
|
||
unset(
|
||
$data['tenant_id'], $data['signal_type'], $data['signal_identifier'],
|
||
$data['delivered_today'], $data['delivered_in_month'],
|
||
$data['supplier_b1_project_id'], $data['supplier_b2_project_id'], $data['supplier_b3_project_id'],
|
||
$data['archived_at'],
|
||
);
|
||
|
||
if (isset($data['daily_limit_target']) && $data['daily_limit_target'] < $project->delivered_today) {
|
||
throw new HttpResponseException(response()->json([
|
||
'errors' => [
|
||
'daily_limit_target' => [
|
||
"Лимит не может быть меньше уже доставленных лидов сегодня ({$project->delivered_today}).",
|
||
],
|
||
],
|
||
], 422));
|
||
}
|
||
|
||
$needsResync = array_key_exists('sms_senders', $data) || array_key_exists('sms_keyword', $data);
|
||
|
||
$project->update($data);
|
||
|
||
if ($needsResync) {
|
||
SyncSupplierProjectJob::dispatch($project->id);
|
||
}
|
||
|
||
return $project->fresh();
|
||
}
|
||
|
||
public function archive(Project $project): void
|
||
{
|
||
if ($project->archived_at !== null) {
|
||
throw new HttpResponseException(response()->json([
|
||
'message' => 'Project уже архивирован.',
|
||
], 409));
|
||
}
|
||
$project->update([
|
||
'is_active' => false,
|
||
'archived_at' => now(),
|
||
]);
|
||
}
|
||
|
||
public function triggerSync(Project $project): void
|
||
{
|
||
SyncSupplierProjectJob::dispatch($project->id);
|
||
}
|
||
|
||
public function bulkAction(int $tenantId, string $action, array $payload): array
|
||
{
|
||
$ids = $payload['ids'] ?? [];
|
||
if (empty($ids)) {
|
||
return ['updated' => 0, 'skipped' => [], 'warnings' => []];
|
||
}
|
||
|
||
$query = Project::where('tenant_id', $tenantId)->whereIn('id', $ids);
|
||
|
||
return match ($action) {
|
||
'pause' => $this->bulkSimpleUpdate($query, ['is_active' => false]),
|
||
'resume' => $this->bulkSimpleUpdate($query, ['is_active' => true]),
|
||
'archive' => $this->bulkSimpleUpdate($query, ['is_active' => false, 'archived_at' => now()]),
|
||
'update_regions' => $this->bulkUpdateRegions($query, $payload),
|
||
'update_days' => $this->bulkUpdateDays($query, $payload),
|
||
'update_limit' => $this->bulkUpdateLimit($query, $payload),
|
||
};
|
||
}
|
||
|
||
private function bulkSimpleUpdate($query, array $update): array
|
||
{
|
||
$updated = $query->update($update);
|
||
|
||
return ['updated' => $updated, 'skipped' => [], 'warnings' => []];
|
||
}
|
||
|
||
private function bulkUpdateRegions($query, array $payload): array
|
||
{
|
||
$add = (int) ($payload['add'] ?? 0);
|
||
$remove = (int) ($payload['remove'] ?? 0);
|
||
|
||
// region_mask = (region_mask | add) & ~remove, clamped to 8 bits (0–255)
|
||
$updated = $query->update([
|
||
'region_mask' => \DB::raw("(region_mask | {$add}) & ~{$remove} & 255"),
|
||
]);
|
||
|
||
return ['updated' => $updated, 'skipped' => [], 'warnings' => []];
|
||
}
|
||
|
||
private function bulkUpdateDays($query, array $payload): array
|
||
{
|
||
return ['updated' => 0, 'skipped' => [], 'warnings' => []];
|
||
}
|
||
|
||
private function bulkUpdateLimit($query, array $payload): array
|
||
{
|
||
return ['updated' => 0, 'skipped' => [], 'warnings' => []];
|
||
}
|
||
|
||
public function create(Tenant $tenant, array $data): Project
|
||
{
|
||
$limit = (int) ($tenant->limits['max_projects'] ?? 10);
|
||
$current = Project::where('tenant_id', $tenant->id)->active()->count();
|
||
if ($current >= $limit) {
|
||
throw new HttpResponseException(response()->json([
|
||
'message' => "Достигнут лимит проектов ({$limit}). Смените тариф.",
|
||
], 403));
|
||
}
|
||
|
||
$data['tenant_id'] = $tenant->id;
|
||
$data['is_active'] = true;
|
||
$project = Project::create($data);
|
||
|
||
SyncSupplierProjectJob::dispatch($project->id);
|
||
|
||
return $project->fresh();
|
||
}
|
||
}
|