create quote * GET /quotes/{token} -> get quote by token * PUT /quotes/{token} -> update quote * POST /quotes/{token}/items -> add item * DELETE /quotes/{token}/items/{id} -> remove item * POST /quotes/{token}/submit -> submit quote * GET /admin/quotes -> list quotes (auth) * GET /admin/quotes/stats -> get stats (auth) * GET /admin/quotes/{id} -> get quote by ID (auth) * PUT /admin/quotes/{id} -> update quote status (auth) * POST /admin/quotes/{id}/sync-syncro -> sync to Syncro (auth) */ // Error reporting: log only, never display to client ini_set('display_errors', '0'); error_reporting(E_ALL); require_once __DIR__ . '/helpers.php'; // Emit CORS headers on every request (handles OPTIONS preflight too) cors_headers(); // Parse request $method = $_SERVER['REQUEST_METHOD']; // Get the path relative to the API directory // Strip the script directory from REQUEST_URI to get the route path $request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // Determine the base path (the directory where index.php lives) $script_dir = dirname($_SERVER['SCRIPT_NAME']); if ($script_dir !== '/' && $script_dir !== '\\') { $path = substr($request_uri, strlen($script_dir)); } else { $path = $request_uri; } // Normalize: ensure leading slash, remove trailing slash (except root) $path = '/' . ltrim($path, '/'); if ($path !== '/' && substr($path, -1) === '/') { $path = rtrim($path, '/'); } // Split path into segments for matching $segments = array_values(array_filter(explode('/', $path), function ($s) { return $s !== ''; })); $seg_count = count($segments); // -------------------------------------------------------------------------- // Route dispatch // -------------------------------------------------------------------------- // -- Public quote routes: /quotes/... -- if ($seg_count >= 1 && $segments[0] === 'quotes') { require_once __DIR__ . '/routes/quotes.php'; // POST /quotes -> create if ($seg_count === 1 && $method === 'POST') { handle_create_quote(); } // GET /quotes/{token} -> get if ($seg_count === 2 && $method === 'GET') { handle_get_quote($segments[1]); } // PUT /quotes/{token} -> update if ($seg_count === 2 && $method === 'PUT') { handle_update_quote($segments[1]); } // POST /quotes/{token}/items -> add item if ($seg_count === 3 && $segments[2] === 'items' && $method === 'POST') { handle_add_item($segments[1]); } // DELETE /quotes/{token}/items/{id} -> remove item if ($seg_count === 4 && $segments[2] === 'items' && $method === 'DELETE') { handle_remove_item($segments[1], $segments[3]); } // POST /quotes/{token}/submit -> submit if ($seg_count === 3 && $segments[2] === 'submit' && $method === 'POST') { handle_submit_quote($segments[1]); } // If we got here with a quotes path but no match, 404 error_response('Not found', 404); } // -- Admin routes: /admin/quotes/... -- if ($seg_count >= 2 && $segments[0] === 'admin' && $segments[1] === 'quotes') { require_once __DIR__ . '/routes/admin.php'; // GET /admin/quotes -> list if ($seg_count === 2 && $method === 'GET') { handle_list_quotes(); } // GET /admin/quotes/stats -> stats if ($seg_count === 3 && $segments[2] === 'stats' && $method === 'GET') { handle_get_stats(); } // GET /admin/quotes/{id} -> get by ID if ($seg_count === 3 && $segments[2] !== 'stats' && $method === 'GET') { handle_admin_get_quote($segments[2]); } // PUT /admin/quotes/{id} -> admin update if ($seg_count === 3 && $method === 'PUT') { handle_admin_update_quote($segments[2]); } // POST /admin/quotes/{id}/sync-syncro -> syncro sync if ($seg_count === 4 && $segments[3] === 'sync-syncro' && $method === 'POST') { handle_sync_syncro($segments[2]); } // If we got here with an admin path but no match, 404 error_response('Not found', 404); } // -------------------------------------------------------------------------- // Health check: GET /health // -------------------------------------------------------------------------- if ($seg_count === 1 && $segments[0] === 'health' && $method === 'GET') { // Quick DB connectivity check try { require_once __DIR__ . '/db.php'; $db = get_db(); $db->query('SELECT 1'); json_response(['status' => 'ok', 'database' => 'connected']); } catch (\Throwable $e) { json_response(['status' => 'error', 'database' => 'disconnected'], 503); } } // -------------------------------------------------------------------------- // Root: GET / // -------------------------------------------------------------------------- if ($seg_count === 0 && $method === 'GET') { json_response([ 'service' => 'MSP Quote Wizard API', 'version' => '1.0.0', 'status' => 'running', ]); } // -------------------------------------------------------------------------- // 404 fallback // -------------------------------------------------------------------------- error_response('Not found', 404);