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.
231 lines
9.7 KiB
PHP
231 lines
9.7 KiB
PHP
<?php
|
||
|
||
use Illuminate\Support\Str;
|
||
use Pdo\Mysql;
|
||
|
||
return [
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| Default Database Connection Name
|
||
|--------------------------------------------------------------------------
|
||
|
|
||
| Here you may specify which of the database connections below you wish
|
||
| to use as your default connection for database operations. This is
|
||
| the connection which will be utilized unless another connection
|
||
| is explicitly specified when you execute a query / statement.
|
||
|
|
||
*/
|
||
|
||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| Database Connections
|
||
|--------------------------------------------------------------------------
|
||
|
|
||
| Below are all of the database connections defined for your application.
|
||
| An example configuration is provided for each database system which
|
||
| is supported by Laravel. You're free to add / remove connections.
|
||
|
|
||
*/
|
||
|
||
'connections' => [
|
||
|
||
'sqlite' => [
|
||
'driver' => 'sqlite',
|
||
'url' => env('DB_URL'),
|
||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||
'prefix' => '',
|
||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||
'busy_timeout' => null,
|
||
'journal_mode' => null,
|
||
'synchronous' => null,
|
||
'transaction_mode' => 'DEFERRED',
|
||
],
|
||
|
||
'mysql' => [
|
||
'driver' => 'mysql',
|
||
'url' => env('DB_URL'),
|
||
'host' => env('DB_HOST', '127.0.0.1'),
|
||
'port' => env('DB_PORT', '3306'),
|
||
'database' => env('DB_DATABASE', 'laravel'),
|
||
'username' => env('DB_USERNAME', 'root'),
|
||
'password' => env('DB_PASSWORD', ''),
|
||
'unix_socket' => env('DB_SOCKET', ''),
|
||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||
'prefix' => '',
|
||
'prefix_indexes' => true,
|
||
'strict' => true,
|
||
'engine' => null,
|
||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||
]) : [],
|
||
],
|
||
|
||
'mariadb' => [
|
||
'driver' => 'mariadb',
|
||
'url' => env('DB_URL'),
|
||
'host' => env('DB_HOST', '127.0.0.1'),
|
||
'port' => env('DB_PORT', '3306'),
|
||
'database' => env('DB_DATABASE', 'laravel'),
|
||
'username' => env('DB_USERNAME', 'root'),
|
||
'password' => env('DB_PASSWORD', ''),
|
||
'unix_socket' => env('DB_SOCKET', ''),
|
||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||
'prefix' => '',
|
||
'prefix_indexes' => true,
|
||
'strict' => true,
|
||
'engine' => null,
|
||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||
]) : [],
|
||
],
|
||
|
||
'pgsql' => [
|
||
'driver' => 'pgsql',
|
||
'url' => env('DB_URL'),
|
||
'host' => env('DB_HOST', '127.0.0.1'),
|
||
'port' => env('DB_PORT', '5432'),
|
||
'database' => env('DB_DATABASE', 'laravel'),
|
||
'username' => env('DB_USERNAME', 'root'),
|
||
'password' => env('DB_PASSWORD', ''),
|
||
'charset' => env('DB_CHARSET', 'utf8'),
|
||
'prefix' => '',
|
||
'prefix_indexes' => true,
|
||
'search_path' => 'public',
|
||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||
// PG session timezone = UTC. Без этого TIMESTAMPTZ возвращается с локальным offset
|
||
// (+03), а Carbon::parse теряет offset → password reset token expiry-check
|
||
// и аналогичные TZ-чувствительные сравнения ломаются.
|
||
'timezone' => env('DB_TIMEZONE', 'UTC'),
|
||
],
|
||
|
||
// Plan 3 Task 3: dedicated PG connection для supplier-flow под BYPASSRLS-ролью
|
||
// crm_supplier_worker (создана Plan 2.6 #iv 7899071 в db/00_create_roles.sql v1.1).
|
||
// Закрывает 3 backlog-айтема одной правкой:
|
||
// - BLOCKER #6: INSERT в failed_webhook_jobs с tenant_id=NULL под BYPASSRLS
|
||
// проходит (политика tenant_isolation отвергает NULL под обычной ролью).
|
||
// - WARN #2: LeadRouter::matchEligibleProjects видит проекты ВСЕХ tenant'ов
|
||
// без SET LOCAL app.current_tenant_id (sharing-model §6 spec'а).
|
||
// - WARN #3: ResetDeliveredTodayCommand сбрасывает delivered_today по всем
|
||
// tenant'ам в одном UPDATE.
|
||
//
|
||
// На production env-keys DB_SUPPLIER_USERNAME=crm_supplier_worker + DB_SUPPLIER_PASSWORD
|
||
// указывают на BYPASSRLS-роль. На dev fallback на DB_USERNAME/DB_PASSWORD (postgres
|
||
// superuser — BYPASSRLS implicit), тесты работают без отдельной роли.
|
||
//
|
||
// WHERE(tenant_id=) фильтры в коде сохраняются как defense-in-depth — если в будущем
|
||
// роль сменится на не-BYPASSRLS, бизнес-логика останется корректной.
|
||
//
|
||
// Brainstorm decision: вариант C (BYPASSRLS-role) из 3 опций. См.
|
||
// docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §1.
|
||
'pgsql_supplier' => array_merge(
|
||
[
|
||
'driver' => 'pgsql',
|
||
'url' => env('DB_URL'),
|
||
'host' => env('DB_HOST', '127.0.0.1'),
|
||
'port' => env('DB_PORT', '5432'),
|
||
'database' => env('DB_DATABASE', 'laravel'),
|
||
'charset' => env('DB_CHARSET', 'utf8'),
|
||
'prefix' => '',
|
||
'prefix_indexes' => true,
|
||
'search_path' => 'public',
|
||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||
'timezone' => env('DB_TIMEZONE', 'UTC'),
|
||
],
|
||
[
|
||
'username' => env('DB_SUPPLIER_USERNAME', env('DB_USERNAME', 'root')),
|
||
'password' => env('DB_SUPPLIER_PASSWORD', env('DB_PASSWORD', '')),
|
||
'options' => [
|
||
'_role_purpose' => 'crm_supplier_worker (BYPASSRLS) for supplier-flow jobs',
|
||
],
|
||
]
|
||
),
|
||
|
||
'sqlsrv' => [
|
||
'driver' => 'sqlsrv',
|
||
'url' => env('DB_URL'),
|
||
'host' => env('DB_HOST', 'localhost'),
|
||
'port' => env('DB_PORT', '1433'),
|
||
'database' => env('DB_DATABASE', 'laravel'),
|
||
'username' => env('DB_USERNAME', 'root'),
|
||
'password' => env('DB_PASSWORD', ''),
|
||
'charset' => env('DB_CHARSET', 'utf8'),
|
||
'prefix' => '',
|
||
'prefix_indexes' => true,
|
||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||
],
|
||
|
||
],
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| Migration Repository Table
|
||
|--------------------------------------------------------------------------
|
||
|
|
||
| This table keeps track of all the migrations that have already run for
|
||
| your application. Using this information, we can determine which of
|
||
| the migrations on disk haven't actually been run on the database.
|
||
|
|
||
*/
|
||
|
||
'migrations' => [
|
||
'table' => 'migrations',
|
||
'update_date_on_publish' => true,
|
||
],
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| Redis Databases
|
||
|--------------------------------------------------------------------------
|
||
|
|
||
| Redis is an open source, fast, and advanced key-value store that also
|
||
| provides a richer body of commands than a typical key-value system
|
||
| such as Memcached. You may define your connection settings here.
|
||
|
|
||
*/
|
||
|
||
'redis' => [
|
||
|
||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||
|
||
'options' => [
|
||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||
'persistent' => env('REDIS_PERSISTENT', false),
|
||
],
|
||
|
||
'default' => [
|
||
'url' => env('REDIS_URL'),
|
||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||
'username' => env('REDIS_USERNAME'),
|
||
'password' => env('REDIS_PASSWORD'),
|
||
'port' => env('REDIS_PORT', '6379'),
|
||
'database' => env('REDIS_DB', '0'),
|
||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||
],
|
||
|
||
'cache' => [
|
||
'url' => env('REDIS_URL'),
|
||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||
'username' => env('REDIS_USERNAME'),
|
||
'password' => env('REDIS_PASSWORD'),
|
||
'port' => env('REDIS_PORT', '6379'),
|
||
'database' => env('REDIS_CACHE_DB', '1'),
|
||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||
],
|
||
|
||
],
|
||
|
||
];
|