feat(projects): Plan 5 Task 3 — store + StoreProjectRequest + ProjectService::create

- StoreProjectRequest: 3-way conditional validation (site domain regex, call 7\d{10}, sms senders required)
- ProjectService::create(): max_projects limit check via Tenant.limits JSONB + dispatch SyncSupplierProjectJob
- ProjectController: constructor DI + store() method returning 201
- SyncSupplierProjectJob: stub (Task 4 полная реализация)
- POST /api/projects route inside auth:sanctum+tenant group (name projects.store)
- Migration add_limits_to_tenants: JSONB DEFAULT '{}' per-tenant limits column
- Tenant model: limits added to fillable + casts as array
- schema.sql/CHANGELOG: tenants.limits documented in v8.20
- phpstan-baseline: +8 actingAs entries for new test file
- Quirk: region_mode in request uses 'include'/'exclude' (schema CHECK) not 'all'/'whitelist' (plan spec typo)
- Quirk: Project::first() → Project::where('signal_identifier','x.ru')->latest()->first() (no RefreshDatabase, persistent test DB)
- 8/8 ProjectsStoreTest passed; 699/706 total (4 pre-existing failures unchanged)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-11 18:29:54 +03:00
parent e242e7d7fc
commit 9d2e7270de
11 changed files with 290 additions and 5 deletions
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreProjectRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user() !== null;
}
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'],
'region_mask' => ['required', 'integer', 'min:0'],
'region_mode' => ['required', Rule::in(['include', 'exclude'])],
'delivery_days_mask' => ['required', 'integer', 'min:1', 'max:127'],
];
if ($signalType === 'site') {
$base['signal_identifier'] = ['required', 'string', 'regex:/^[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', 'max:50'];
}
return $base;
}
}