8fce10f5a0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
78 lines
3.7 KiB
PHP
78 lines
3.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Project;
|
||
use App\Models\SupplierProject;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* Подбор eligible Лидерра-проектов для входящего лида (sharing-model §6).
|
||
*
|
||
* Eligibility — структурно через pivot project_supplier_links: проект eligible,
|
||
* если связан с пришедшим supplier_project (= источник × субъект) + активен +
|
||
* сегодня рабочий день + есть остаток лимита + у тенанта есть баланс.
|
||
*
|
||
* Регион сопоставляется самим supplier_project (тег = субъект) — phone-prefix
|
||
* фильтр убран (эпик миграции проектов, Q5): для мобильных он no-op, а регион
|
||
* гарантирован тем, через какой supplier_project пришёл лид.
|
||
*
|
||
* Запрос через connection pgsql_supplier (BYPASSRLS crm_supplier_worker) — в
|
||
* sharing-flow tenant ещё не определён, SELECT видит проекты всех tenant'ов.
|
||
*
|
||
* Spec: docs/superpowers/specs/2026-05-20-project-migration-redesign-design.md §4.5.
|
||
*/
|
||
class LeadRouter
|
||
{
|
||
/**
|
||
* Возвращает ONE project per tenant_id — тот, у которого наибольший остаток
|
||
* дневного лимита (DISTINCT ON (tenant_id) с ORDER BY remaining DESC, created_at, id).
|
||
*
|
||
* Семантика (Spec B Task 3): один лид продаётся не более чем 3 РАЗЛИЧНЫМ тенантам
|
||
* (клиентам), каждый тенант получает ровно ОДИН проект — с наибольшим остатком.
|
||
* LeadDistributor::selectRecipients (CAP=3) теперь ограничивает число тенантов,
|
||
* а не число проектов, потому что входные данные уже one-per-tenant.
|
||
*
|
||
* Запрос через pgsql_supplier (BYPASSRLS crm_supplier_worker) — tenant ещё не
|
||
* определён, SELECT видит проекты всех tenant'ов.
|
||
*
|
||
* @return Collection<int, Project>
|
||
*/
|
||
public function matchEligibleProjects(SupplierProject $supplierProject): Collection
|
||
{
|
||
// МСК-aligned ISO day-of-week (reset-cron тоже 00:00 МСК).
|
||
$todayBit = 1 << (Carbon::now('Europe/Moscow')->isoWeekday() - 1);
|
||
|
||
$sql = <<<'SQL'
|
||
SELECT DISTINCT ON (projects.tenant_id) projects.*
|
||
FROM projects
|
||
WHERE EXISTS (
|
||
SELECT 1 FROM project_supplier_links psl
|
||
WHERE psl.project_id = projects.id
|
||
AND psl.supplier_project_id = ?
|
||
)
|
||
AND projects.is_active = true
|
||
AND (projects.delivery_days_mask & ?) <> 0
|
||
AND projects.delivered_today < COALESCE(projects.effective_daily_limit_today, projects.daily_limit_target)
|
||
AND EXISTS (
|
||
SELECT 1 FROM tenants
|
||
WHERE tenants.id = projects.tenant_id
|
||
AND (tenants.balance_leads > 0 OR tenants.balance_rub > 0)
|
||
)
|
||
ORDER BY
|
||
projects.tenant_id,
|
||
(COALESCE(projects.effective_daily_limit_today, projects.daily_limit_target) - projects.delivered_today) DESC,
|
||
projects.created_at,
|
||
projects.id
|
||
SQL;
|
||
|
||
$rows = DB::connection('pgsql_supplier')->select($sql, [$supplierProject->id, $todayBit]);
|
||
|
||
return Project::hydrate($rows)->values();
|
||
}
|
||
}
|