fb55bfdd1f
Plan 2/5 Task 1 — слой данных для supplier-webhook flow. - supplier_leads (SaaS-level, без RLS) — raw payload incoming webhook'ов - projects.delivered_today — дневной счётчик для проверки daily quota - system_settings: supplier_webhook_secret + supplier_ip_allowlist Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §5-§6 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
3.3 KiB
PHP
95 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
test('supplier_leads table exists with required columns', function () {
|
|
expect(Schema::hasTable('supplier_leads'))->toBeTrue();
|
|
|
|
expect(Schema::hasColumns('supplier_leads', [
|
|
'id',
|
|
'supplier_project_id',
|
|
'platform',
|
|
'raw_payload',
|
|
'vid',
|
|
'phone',
|
|
'received_at',
|
|
'source',
|
|
'processed_at',
|
|
'deals_created_count',
|
|
]))->toBeTrue();
|
|
});
|
|
|
|
test('supplier_leads has FK to supplier_projects with ON DELETE SET NULL', function () {
|
|
$row = DB::selectOne(
|
|
"SELECT c.confdeltype
|
|
FROM pg_constraint c
|
|
JOIN pg_class t ON c.conrelid = t.oid
|
|
JOIN pg_class r ON c.confrelid = r.oid
|
|
WHERE t.relname = 'supplier_leads'
|
|
AND r.relname = 'supplier_projects'
|
|
AND c.contype = 'f'"
|
|
);
|
|
|
|
expect($row)->not->toBeNull();
|
|
// confdeltype: 'a'=NO ACTION, 'r'=RESTRICT, 'c'=CASCADE, 'n'=SET NULL, 'd'=SET DEFAULT
|
|
expect($row->confdeltype)->toBe('n');
|
|
});
|
|
|
|
test('supplier_leads.source has CHECK enum', function () {
|
|
// Valid source: должно пройти.
|
|
$insertedId = DB::table('supplier_leads')->insertGetId([
|
|
'platform' => 'B1',
|
|
'raw_payload' => json_encode(['test' => 'valid_source']),
|
|
'vid' => 1000001,
|
|
'phone' => '+79991234567',
|
|
'source' => 'webhook',
|
|
]);
|
|
expect($insertedId)->toBeGreaterThan(0);
|
|
DB::table('supplier_leads')->where('id', $insertedId)->delete();
|
|
|
|
// Invalid source: должно отклониться CHECK constraint'ом.
|
|
expect(fn () => DB::table('supplier_leads')->insert([
|
|
'platform' => 'B1',
|
|
'raw_payload' => json_encode(['test' => 'invalid']),
|
|
'vid' => 1000002,
|
|
'phone' => '+79991234568',
|
|
'source' => 'invalid_source',
|
|
]))->toThrow(QueryException::class);
|
|
});
|
|
|
|
test('projects.delivered_today exists with default 0', function () {
|
|
expect(Schema::hasColumn('projects', 'delivered_today'))->toBeTrue();
|
|
|
|
$col = DB::selectOne(
|
|
"SELECT column_default
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'projects' AND column_name = 'delivered_today'"
|
|
);
|
|
|
|
expect($col)->not->toBeNull();
|
|
expect($col->column_default)->toContain('0');
|
|
});
|
|
|
|
test('system_settings seed rows exist for supplier_webhook_secret + supplier_ip_allowlist', function () {
|
|
$secret = DB::table('system_settings')
|
|
->where('key', 'supplier_webhook_secret')
|
|
->first();
|
|
expect($secret)->not->toBeNull();
|
|
expect($secret->type)->toBe('string');
|
|
// Placeholder __SET_ON_DEPLOY__ (17 chars) валиден; реальный secret (≥32 chars) будет SET ON DEPLOY.
|
|
// Лимит 16 ловит пустую строку и слишком короткие случайные значения, но пропускает наш placeholder.
|
|
expect(strlen($secret->value))->toBeGreaterThanOrEqual(16);
|
|
|
|
$allowlist = DB::table('system_settings')
|
|
->where('key', 'supplier_ip_allowlist')
|
|
->first();
|
|
expect($allowlist)->not->toBeNull();
|
|
expect($allowlist->type)->toBe('json');
|
|
$decoded = json_decode($allowlist->value, true);
|
|
expect($decoded)->toBeArray();
|
|
});
|