99afcbc25c
- app/Models/Project.php — добавлены fillable+casts для supplier integration:
signal_type, signal_identifier, sms_senders (jsonb array), sms_keyword,
delivered_in_month, supplier_b{1,2,3}_project_id.
+ supplierB1/B2/B3() BelongsTo relations на SupplierProject (sharing-model).
+ scopeActiveOnDay($iso) — bitmask проверка по delivery_days_mask
(bit 0 = Mon, bit 6 = Sun; ISO=1 → 1<<0 = 1; ISO=7 → 1<<6 = 64).
+ scopeForSignal($type, $identifier) — фильтр по сигналу (для роутинга в Plan 2).
- database/factories/ProjectFactory.php — defaults null/0 для новых полей
(CHECK constraints не нарушаются: signal_type IS NULL → остальные опциональны).
+ state-методы asSiteSignal($domain), asCallSignal($phone), asSmsSignal($senders, $keyword).
- tests/Feature/Models/ProjectExtensionsTest.php — 6 тестов: signal_type fillable,
sms_senders array cast + sms_keyword, SMS без keyword, supplierB1/B2/B3 relations,
scopeActiveOnDay (bitmask Mon/Sat), scopeForSignal (3 сигнала + edge-case).
Pest: 469 / 467 passed / 2 skipped (461 + 6 новых = 467, с retry на transient
PG connection issues — на параллельных тестах с testing_rls_user GRANT тяжёл).
Larastan: 0 errors. Pint passed.
Spec: §2.1
Plan: Task 10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.3 KiB
PHP
82 lines
2.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Database\Factories;
|
|
|
|
use App\Models\Project;
|
|
use App\Models\Tenant;
|
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
|
|
/**
|
|
* @extends Factory<Project>
|
|
*/
|
|
class ProjectFactory extends Factory
|
|
{
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function definition(): array
|
|
{
|
|
return [
|
|
'tenant_id' => Tenant::factory(),
|
|
'name' => fake()->words(3, true),
|
|
'type' => 'webhook',
|
|
'is_active' => true,
|
|
'daily_limit_target' => 10,
|
|
'region_mask' => 255,
|
|
'region_mode' => 'include',
|
|
'delivery_days_mask' => 127,
|
|
'assignment_strategy' => 'manual',
|
|
'ttfr_target_minutes' => 15,
|
|
// Supplier integration (Plan 1/5 Task 1+10) — по умолчанию пусто,
|
|
// CHECK constraint'ы projects не нарушаются (signal_type IS NULL → все остальные опциональны).
|
|
'signal_type' => null,
|
|
'signal_identifier' => null,
|
|
'sms_senders' => null,
|
|
'sms_keyword' => null,
|
|
'delivered_in_month' => 0,
|
|
'supplier_b1_project_id' => null,
|
|
'supplier_b2_project_id' => null,
|
|
'supplier_b3_project_id' => null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Сайт-сигнал с указанным доменом.
|
|
*/
|
|
public function asSiteSignal(string $domain): self
|
|
{
|
|
return $this->state([
|
|
'signal_type' => 'site',
|
|
'signal_identifier' => $domain,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Звонок-сигнал с указанным телефоном.
|
|
*/
|
|
public function asCallSignal(string $phone): self
|
|
{
|
|
return $this->state([
|
|
'signal_type' => 'call',
|
|
'signal_identifier' => $phone,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* SMS-сигнал. Sender-имена в массиве (1+); keyword optional (пусто = только B3).
|
|
*
|
|
* @param array<int, string> $senders
|
|
*/
|
|
public function asSmsSignal(array $senders, ?string $keyword = null): self
|
|
{
|
|
return $this->state([
|
|
'signal_type' => 'sms',
|
|
'signal_identifier' => null,
|
|
'sms_senders' => $senders,
|
|
'sms_keyword' => $keyword,
|
|
]);
|
|
}
|
|
}
|