table('supplier_csv_reconcile_log')->insert([ 'started_at' => now()->subMinutes(10), 'finished_at' => now()->subMinutes(9), 'window_start' => now()->subDay(), 'window_end' => now(), 'total_csv_rows' => 100, 'matched_count' => 98, 'recovered_count' => 2, 'drift_ratio' => 0.02, 'status' => 'ok', 'created_at' => now()->subMinutes(10), ]); $response = $this->getJson('/api/admin/supplier-integration'); $response->assertOk(); $response->assertJsonStructure([ 'health' => ['last_run_at', 'last_status', 'drift_ratio', 'webhook_state'], 'history' => [['started_at', 'status', 'total_csv_rows', 'matched_count', 'recovered_count', 'drift_ratio']], ]); expect($response->json('health.last_status'))->toBe('ok'); expect($response->json('health.webhook_state'))->toBe('live'); }); it('webhook_state is "down" when last run had drift_alert', function (): void { DB::connection('pgsql_supplier')->table('supplier_csv_reconcile_log')->insert([ 'started_at' => now()->subMinutes(5), 'finished_at' => now()->subMinutes(4), 'window_start' => now()->subDay(), 'window_end' => now(), 'total_csv_rows' => 100, 'matched_count' => 80, 'recovered_count' => 20, 'drift_ratio' => 0.20, 'status' => 'drift_alert', 'created_at' => now()->subMinutes(5), ]); $response = $this->getJson('/api/admin/supplier-integration'); expect($response->json('health.webhook_state'))->toBe('down'); }); it('POST /api/admin/supplier-integration/reconcile dispatches CsvReconcileJob', function (): void { Bus::fake([CsvReconcileJob::class]); $response = $this->postJson('/api/admin/supplier-integration/reconcile'); $response->assertOk(); $response->assertJson(['dispatched' => true]); Bus::assertDispatched(CsvReconcileJob::class, 1); }); it('returns nulls in health when reconcile log is empty (no run yet)', function (): void { // Пустой supplier_csv_reconcile_log — до первой сверки. Контроллер не должен // падать на $last === null (property access на null). DB::connection('pgsql_supplier')->table('supplier_csv_reconcile_log')->truncate(); $response = $this->getJson('/api/admin/supplier-integration'); $response->assertOk(); expect($response->json('health.last_run_at'))->toBeNull(); expect($response->json('health.last_status'))->toBeNull(); expect($response->json('health.drift_ratio'))->toBeNull(); expect($response->json('health.webhook_state'))->toBe('live'); expect($response->json('history'))->toBe([]); });