6d6181b8cc
Закрывает 3 backlog-айтема Plan 2.6 одной правкой:
- BLOCKER #6: failed_webhook_jobs INSERT с tenant_id=NULL теперь проходит
(BYPASSRLS обходит RLS-политику отвергавшую NULL под обычной ролью)
- WARN #2: LeadRouter::matchEligibleProjects видит projects всех tenant'ов
через Project::on('pgsql_supplier') без SET LOCAL app.current_tenant_id
- WARN #3: ResetDeliveredTodayCommand обновляет projects всех tenant'ов
через DB::connection('pgsql_supplier')
Архитектура: crm_supplier_worker BYPASSRLS-роль (создана Plan 2.6 #iv 7899071)
+ новый pgsql_supplier connection в config/database.php. WHERE(tenant_id=)
фильтры сохраняются как defense-in-depth.
Уточнение по Job's $connection: оригинальный план предполагал public $connection
= 'pgsql_supplier' на RouteSupplierLeadJob, но в Laravel Job's $connection
управляет очередью (sync/database/redis), не БД. Заменено на константу
RouteSupplierLeadJob::DB_CONNECTION + явный DB::connection(self::DB_CONNECTION)
в failed() callback'е. Это:
1) не ломает queue resolution (без этой правки тесты падают
'pgsql_supplier queue connection has not been configured')
2) явно документирует intent — failed_webhook_jobs INSERT идёт через BYPASSRLS
3) handle()'s tenant-scoped транзакции остаются на default pgsql + SET LOCAL,
где RLS нужна для defense-in-depth.
Также добавлено в tests/TestCase.php разделение PDO между pgsql и
pgsql_supplier connection'ами через setPdo/setReadPdo — иначе DatabaseTransactions
не откатывал бы supplier-side данные (две PDO-сессии = две независимые транзакции,
supplier не видит uncommitted INSERTs default-side).
Brainstorm decision: вариант C из 3 опций (A=schema bump, B=отдельная таблица,
C=BYPASSRLS-role). См. docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §1.
+4 теста в Feature/Supplier/SupplierConnectionTest.php (DB_CONNECTION constant +
BLOCKER#6 + WARN#2 + WARN#3). 0 schema changes.
Pest: 562/560 + 2 skipped (baseline 558/556 + 4 new = 562/560, ok). PHPStan: 0 errors
(добавлен 1 baseline entry для известного Pest+PHPStan limitation на artisan()).
Pint: clean.
35 lines
1.7 KiB
PHP
35 lines
1.7 KiB
PHP
<?php
|
||
|
||
namespace Tests;
|
||
|
||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
abstract class TestCase extends BaseTestCase
|
||
{
|
||
/**
|
||
* Plan 3 Task 3: share PDO between `pgsql` and `pgsql_supplier` connections в тестах,
|
||
* чтобы DatabaseTransactions corretly rollback'ил данные, созданные через дефолтный
|
||
* connection, но запрошенные через supplier. Без этого Project::on('pgsql_supplier')
|
||
* не видит свежесозданные через Project::factory() записи — две PDO-сессии = две
|
||
* разные транзакции, supplier-side не видит uncommitted INSERTs default-side.
|
||
*
|
||
* На production обе роли (crm_app_user + crm_supplier_worker) — две настоящие
|
||
* сессии, общая видимость через commit. В тестах достаточно одной разделяемой PDO.
|
||
*/
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
// После того как Laravel инициализировал dafault connection (pgsql),
|
||
// ре-используем его PDO для pgsql_supplier. Делаем только если оба
|
||
// connection'а есть в конфиге (продакшен может не иметь supplier).
|
||
if (config()->has('database.connections.pgsql_supplier')) {
|
||
$defaultPdo = DB::connection('pgsql')->getPdo();
|
||
$defaultReadPdo = DB::connection('pgsql')->getReadPdo();
|
||
DB::connection('pgsql_supplier')->setPdo($defaultPdo);
|
||
DB::connection('pgsql_supplier')->setReadPdo($defaultReadPdo);
|
||
}
|
||
}
|
||
}
|