122 lines
3.9 KiB
PHP
122 lines
3.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Models;
|
||
|
||
use Database\Factories\DealFactory;
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||
|
||
/**
|
||
* Сделка (лид) — основная транзакционная сущность.
|
||
*
|
||
* Tenant-aware модель с RLS: SELECT/INSERT/UPDATE/DELETE фильтруются
|
||
* политикой `tenant_isolation` по `current_setting('app.current_tenant_id')`.
|
||
*
|
||
* Композитный PRIMARY KEY: (id, received_at). Таблица партиционирована
|
||
* по received_at. Все save/update/delete операции должны включать
|
||
* received_at в WHERE — иначе planner не сможет применить partition
|
||
* pruning. Eloquent не поддерживает composite PK нативно, поэтому
|
||
* setKeysForSaveQuery() переопределён.
|
||
*
|
||
* Идемпотентность webhook'ов реализована через отдельную таблицу
|
||
* webhook_dedup_keys (v8.6, CTO-17): UNIQUE на партиционированной deals
|
||
* без partition key невозможен. См. §5.5 narrative ТЗ.
|
||
*
|
||
* Источник: db/schema.sql v8.6 §5, table `deals`.
|
||
*
|
||
* @mixin IdeHelperDeal
|
||
*/
|
||
class Deal extends Model
|
||
{
|
||
/** @use HasFactory<DealFactory> */
|
||
use HasFactory, SoftDeletes;
|
||
|
||
protected $fillable = [
|
||
'id',
|
||
'tenant_id',
|
||
'source_crm_id',
|
||
'project_id',
|
||
'phone',
|
||
'phones',
|
||
'status',
|
||
'contact_name',
|
||
'comment',
|
||
'manager_id',
|
||
'assigned_at',
|
||
'escalated_count',
|
||
'duplicate_of_id',
|
||
'utm_source',
|
||
'utm_medium',
|
||
'utm_campaign',
|
||
'utm_content',
|
||
'region_code',
|
||
'subject_code',
|
||
'city',
|
||
'time_in_form_seconds',
|
||
'lead_score',
|
||
'is_test',
|
||
'received_at',
|
||
'deleted_at',
|
||
// Lead region resolution (Session 1, 31.05.2026).
|
||
'phone_operator',
|
||
'region_substituted',
|
||
];
|
||
|
||
protected function casts(): array
|
||
{
|
||
return [
|
||
'tenant_id' => 'integer',
|
||
'source_crm_id' => 'integer',
|
||
'project_id' => 'integer',
|
||
'manager_id' => 'integer',
|
||
'duplicate_of_id' => 'integer',
|
||
'escalated_count' => 'integer',
|
||
'time_in_form_seconds' => 'integer',
|
||
'subject_code' => 'integer',
|
||
'lead_score' => 'decimal:2',
|
||
'phones' => 'array',
|
||
'is_test' => 'boolean',
|
||
'region_substituted' => 'boolean',
|
||
'assigned_at' => 'datetime',
|
||
'received_at' => 'datetime',
|
||
'created_at' => 'datetime',
|
||
'updated_at' => 'datetime',
|
||
'deleted_at' => 'datetime',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Composite PK: WHERE id = ? AND received_at = ? для save/update/delete.
|
||
* Без переопределения Eloquent сгенерил бы только WHERE id = ?, что
|
||
* заставило бы planner сканировать все партиции (partition pruning не работает).
|
||
*/
|
||
protected function setKeysForSaveQuery($query)
|
||
{
|
||
return $query
|
||
->where('id', $this->getAttribute('id'))
|
||
->where('received_at', $this->getAttribute('received_at'));
|
||
}
|
||
|
||
/** @return BelongsTo<Tenant, $this> */
|
||
public function tenant(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Tenant::class);
|
||
}
|
||
|
||
/** @return BelongsTo<Project, $this> */
|
||
public function project(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Project::class);
|
||
}
|
||
|
||
/** @return BelongsTo<User, $this> */
|
||
public function manager(): BelongsTo
|
||
{
|
||
return $this->belongsTo(User::class, 'manager_id');
|
||
}
|
||
}
|