get('/admin/schema-check', function () { ScopeMiddleware::requireScope(Scope::SUPERUSER_READ, '/admin/schema-check'); global /** @var response $response */ $response; // Self-heal: run all schema bootstraps first if (class_exists(customer_invoice_email_schema_bootstrap::class)) { try { customer_invoice_email_schema_bootstrap::ensureSchema(); } catch (\Throwable $e) { // Bootstrap may fail in environments where $db is // not yet wired up; report and continue with check } } $report = $this->runSchemaCheck(); $response->setStatus($report['ok'] ? 200 : 503); $response->setBody(json_encode($report, JSON_PRETTY_PRINT)); }); } /** * Returns ['ok' => bool, 'missing' => array, ...]. * If ok=false, the deploy should be blocked. */ private function runSchemaCheck(): array { global $db; $report = [ 'ok' => true, 'missing' => [], 'tables_checked' => 0, 'columns_checked' => 0, 'timestamp' => date('c'), 'note' => 'If "missing" is non-empty, the migration that adds these columns was not run on the database.', ]; if (!isset($db) || !is_object($db) || !method_exists($db, 'query')) { $report['ok'] = false; $report['error'] = 'no_db_connection'; return $report; } $requirements = [ 'users' => [ 'invoice_email', // TRU-77 (added 2026-08-16, was missing on production) 'wash_certificate_email', 'email', 'customer_number', ], 'invoices' => [ 'po_number', 'closed_at', 'customer_number', ], 'bookings' => [ 'id', 'customer_number', 'department', ], ]; foreach ($requirements as $table => $columns) { $report['tables_checked']++; $tableSafe = str_replace('`', '', $table); $result = $db->query("SHOW TABLES LIKE '{$tableSafe}'"); if (!$result || (int)$result->num_rows === 0) { $report['ok'] = false; $report['missing'][] = "table `{$table}` does not exist"; continue; } foreach ($columns as $column) { $report['columns_checked']++; $colSafe = str_replace("'", '', $column); $r = $db->query("SHOW COLUMNS FROM `{$tableSafe}` LIKE '{$colSafe}'"); if (!$r || (int)$r->num_rows === 0) { $report['ok'] = false; $report['missing'][] = "{$table}.{$column}"; } } } return $report; } }