fix(supplier): миграция supplier_order_checks — pgsql_supplier + инлайн-GRANT crm_supplier_worker

По rls-ревью: соседние SaaS-supplier-таблицы (supplier_sync_runs/deferred_sync)
создаются через pgsql_supplier + GRANT SELECT,INSERT + USAGE на sequence для
crm_supplier_worker в DO-блоке с IF EXISTS. Дефолтный Schema::create оставлял
таблицу во владении crm_migrator без грантов → VerifySupplierOrderJob упал бы
permission denied на боевом Managed PG. Приведено к паттерну + SaaS-коммент + CHECK status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-10 05:16:08 +03:00
parent 1db9cca989
commit 8ccf5d955d
2 changed files with 49 additions and 20 deletions
@@ -3,32 +3,61 @@
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
/**
* supplier_order_checks история итоговой проверки заказа у поставщика.
*
* VerifySupplierOrderJob после ночного прогона робота перечитывает живой кабинет
* поставщика и сверяет каждую нашу строку с формулой; результат
* (ok / mismatch / unable_to_verify + список расхождений) пишется сюда одной строкой.
*
* Системная таблица (как supplier_sync_runs / supplier_deferred_sync) без RLS/
* tenant_id, пишет supplier-flow джоб под crm_supplier_worker (BYPASSRLS), читает SaaS-admin.
* Создаётся через pgsql_supplier + инлайн-GRANT (guard IF EXISTS) на проде миграции
* катит crm_migrator, поэтому дефолтные привилегии crm_supplier_worker не наследуются.
*
* Spec: docs/superpowers/specs/2026-07-09-supplier-order-verification-and-deadline-watch-design.md §6.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('supplier_order_checks', function (Blueprint $table): void {
$table->bigIncrements('id');
$table->timestampTz('checked_at');
$table->unsignedBigInteger('sync_run_id')->nullable();
$table->integer('intended_rows')->default(0);
$table->integer('live_rows')->default(0);
$table->integer('mismatch_count')->default(0);
// ok | mismatch | unable_to_verify
$table->string('status', 32);
$table->jsonb('details')->nullable();
$table->timestampTz('created_at')->useCurrent();
$supplier = DB::connection('pgsql_supplier');
$table->index('checked_at');
$table->index('status');
});
$supplier->statement(<<<'SQL'
CREATE TABLE IF NOT EXISTS supplier_order_checks (
id BIGSERIAL PRIMARY KEY,
checked_at TIMESTAMPTZ NOT NULL,
sync_run_id BIGINT,
intended_rows INTEGER NOT NULL DEFAULT 0,
live_rows INTEGER NOT NULL DEFAULT 0,
mismatch_count INTEGER NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL
CHECK (status IN ('ok','mismatch','unable_to_verify')),
details JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
SQL);
$supplier->statement('CREATE INDEX IF NOT EXISTS idx_supplier_order_checks_checked ON supplier_order_checks (checked_at DESC)');
$supplier->statement('CREATE INDEX IF NOT EXISTS idx_supplier_order_checks_status ON supplier_order_checks (status)');
foreach (['crm_supplier_worker'] as $role) {
$supplier->statement(<<<SQL
DO \$\$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{$role}') THEN
GRANT SELECT, INSERT ON supplier_order_checks TO {$role};
GRANT USAGE, SELECT ON SEQUENCE supplier_order_checks_id_seq TO {$role};
END IF;
END
\$\$
SQL);
}
}
public function down(): void
{
Schema::dropIfExists('supplier_order_checks');
DB::connection('pgsql_supplier')->statement('DROP TABLE IF EXISTS supplier_order_checks CASCADE');
}
};
+3 -3
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-07-10T01:43:47.562Z
Last updated: 2026-07-10T01:58:51.729Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -112,8 +112,8 @@ Episodes since last run: 542 / threshold: 10
| PID | Имя | CPU-время | Возраст |
|---|---|---|---|
| 11216 | MsMpEng | 2.69ч | NaNч |
| 10400 | Code | 1.98ч | NaNч |
| 11216 | MsMpEng | 2.76ч | 0.0ч |
| 10400 | Code | 2.00ч | 0.0ч |
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.