$currencyOverride, 'fallback' => false] : StoreLocales::resolve($language); $woo = new WooCommerceClient(); if ($orderNumber !== null && $orderNumber !== '') { $order = $woo->getOrder(orderNumber: $orderNumber, currency: $locale['currency']); // Security: verify that the caller's email matches the billing email on the order. $billingEmail = strtolower(trim($order['billing']['email'] ?? '')); if ($billingEmail === '' || $billingEmail !== strtolower(trim($email))) { throw new HttpException(403, 'UNAUTHORIZED', 'Provided email does not match the order.'); } } else { $order = $woo->getOrder(email: $email, currency: $locale['currency']); } $response = [ 'ok' => true, 'data' => self::formatOrder($order), 'currency' => $locale['currency'], ]; if ($locale['fallback']) { $response['currency_fallback'] = true; $response['currency_note'] = self::buildCurrencyNote($locale, $language); } // Return a curated subset useful for the LLM agent Router::sendJson(200, $response); } // ================================================================= // POST /tools/get_product_data // ================================================================= public static function getProductData(): void { Auth::requireBearer(); $body = Router::jsonBody(); $productId = isset($body['productId']) ? (int)$body['productId'] : (isset($body['product_id']) ? (int)$body['product_id'] : null); $sku = $body['sku'] ?? null; $search = $body['search'] ?? null; $language = $body['language'] ?? null; $currencyOverride = isset($body['currency']) ? strtoupper(trim((string)$body['currency'])) : null; $locale = ($currencyOverride !== null && $currencyOverride !== '' && StoreLocales::isSupported($currencyOverride)) ? ['currency' => $currencyOverride, 'fallback' => false] : StoreLocales::resolve($language); $woo = new WooCommerceClient(); $currencyMeta = ['currency' => $locale['currency']]; if ($locale['fallback']) { $currencyMeta['currency_fallback'] = true; $currencyMeta['currency_note'] = self::buildCurrencyNote($locale, $language); } $lang = $language ?? ''; if ($productId !== null && $productId > 0) { $product = $woo->getProduct(productId: $productId, currency: $locale['currency'], lang: $lang); Router::sendJson(200, array_merge(['ok' => true, 'data' => self::formatProduct($product)], $currencyMeta)); return; } if ($sku !== null && $sku !== '') { $product = $woo->getProduct(sku: $sku, currency: $locale['currency'], lang: $lang); Router::sendJson(200, array_merge(['ok' => true, 'data' => self::formatProduct($product)], $currencyMeta)); return; } if ($search !== null && $search !== '') { $products = $woo->searchProducts($search, currency: $locale['currency'], lang: $lang); $formatted = array_map(fn($p) => self::formatProduct($p), $products); Router::sendJson(200, array_merge(['ok' => true, 'data' => $formatted], $currencyMeta)); return; } throw new HttpException(400, 'MISSING_PARAMS', 'Provide productId, sku, or search.'); } // ================================================================= // POST /tools/get_shipping_data // ================================================================= public static function getShippingData(): void { Auth::requireBearer(); $body = Router::jsonBody(); $language = $body['language'] ?? null; $currencyOverride = isset($body['currency']) ? strtoupper(trim((string)$body['currency'])) : null; $locale = ($currencyOverride !== null && $currencyOverride !== '' && StoreLocales::isSupported($currencyOverride)) ? ['currency' => $currencyOverride, 'fallback' => false] : StoreLocales::resolve($language); $woo = new WooCommerceClient(); // If zoneId provided — return methods for that specific zone $zoneId = $body['zoneId'] ?? ($body['zone_id'] ?? null); try { if ($zoneId !== null) { // Use custom WP endpoint to get per-currency shipping costs // (WC REST API only returns PLN costs; WCML costs are in wp_options) $wpBase = rtrim(Env::get('WOOCOMMERCE_BASE_URL'), '/') . '/wp-json/eksrelay/v1'; $url = $wpBase . '/shipping-costs?' . http_build_query([ 'zone_id' => (int)$zoneId, 'currency' => $locale['currency'], ]); $res = HttpClient::request('GET', $url); $response = ['ok' => true, 'data' => $res['json'] ?? [], 'currency' => $locale['currency']]; if ($locale['fallback']) { $response['currency_fallback'] = true; $response['currency_note'] = self::buildCurrencyNote($locale, $language); } Router::sendJson(200, $response); return; } // Default: return list of zones with ISO country codes // Zone 0 (Rest of the World) has no locations — it's the fallback zone. $zones = $woo->getShippingZones(); $formatted = []; foreach ($zones as $z) { $zid = (int)($z['id'] ?? 0); $locations = $woo->getShippingZoneLocations($zid); // Extract country-level ISO codes only (skip state/continent entries) $countries = array_values(array_filter(array_map( fn($l) => ($l['type'] ?? '') === 'country' ? strtoupper((string)($l['code'] ?? '')) : null, $locations ))); $formatted[] = [ 'id' => $zid, 'name' => $z['name'] ?? '', 'countries' => $countries, // ISO 3166-1 alpha-2 codes, e.g. ["PL"] or ["GB","IE"] ]; } Router::sendJson(200, [ 'ok' => true, 'data' => $formatted, 'hint' => 'Pass {"zoneId": } to get shipping methods for a specific zone.', ]); } catch (HttpException $e) { if ($e->httpCode === 502) { Router::sendJson(200, [ 'ok' => false, 'code' => 'NOT_IMPLEMENTED', 'message' => 'Shipping data is not available via WooCommerce REST API. ' . 'Ensure shipping zones are configured and the consumer key has read access to shipping endpoints.', ]); return; } throw $e; } } // ================================================================= // POST /tools/get_payment_methods // ================================================================= public static function getPaymentMethods(): void { Auth::requireBearer(); $woo = new WooCommerceClient(); try { $gateways = $woo->getPaymentGateways(); // Filter to enabled gateways only $enabled = array_values(array_filter($gateways, fn($g) => ($g['enabled'] ?? false) === true)); $formatted = array_map(fn($g) => [ 'id' => $g['id'] ?? '', 'title' => $g['title'] ?? '', 'description' => $g['description'] ?? '', 'enabled' => $g['enabled'] ?? false, ], $enabled); Router::sendJson(200, ['ok' => true, 'data' => $formatted]); } catch (HttpException $e) { if ($e->httpCode === 502) { Router::sendJson(200, [ 'ok' => false, 'code' => 'NOT_IMPLEMENTED', 'message' => 'Payment gateways endpoint not accessible. ' . 'Ensure the WooCommerce consumer key has admin-level (read/write) permissions ' . 'to access GET /payment_gateways.', ]); return; } throw $e; } } // ================================================================= // POST /tools/get_product_compatibility // ================================================================= public static function getProductCompatibility(): void { Auth::requireBearer(); $body = Router::jsonBody(); $restBase = rtrim(Env::get('WOOCOMMERCE_BASE_URL'), '/') . '/wp-json/eksrelay/v1'; $productName = $body['product_name'] ?? ($body['productName'] ?? null); $productId = isset($body['productId']) ? (int)$body['productId'] : (isset($body['product_id']) ? (int)$body['product_id'] : null); $carBrand = $body['car_brand'] ?? ($body['carBrand'] ?? null); $carModel = $body['car_model'] ?? ($body['carModel'] ?? null); $carYear = $body['car_year'] ?? ($body['carYear'] ?? null); $carEngine = $body['car_engine'] ?? ($body['carEngine'] ?? null); $carEngineIndex = isset($body['car_engine_index']) ? (int)$body['car_engine_index'] : -1; $language = $body['language'] ?? null; $currencyOverride = isset($body['currency']) ? strtoupper(trim((string)$body['currency'])) : null; $locale = ($currencyOverride !== null && $currencyOverride !== '' && StoreLocales::isSupported($currencyOverride)) ? ['currency' => $currencyOverride, 'fallback' => false] : StoreLocales::resolve($language); // When a non-Polish product name is given, try to pre-resolve it to a product ID // via the WooCommerce REST API (which has a PL language fallback). This prevents // the WP endpoint from failing to find products with non-Polish search terms. if ($productName !== null && $productName !== '' && ($productId === null || $productId === 0)) { $woo = new WooCommerceClient(); $found = $woo->searchProducts($productName, 5, '', $language ?? ''); if (empty($found) && $language && $language !== 'pl') { // Fallback: search in PL (default) database without language filter $found = $woo->searchProducts($productName, 5, ''); } if (count($found) === 1) { // Unambiguous match — use product ID so WP endpoint skips name-based search $productId = (int)$found[0]['id']; $productName = null; } // Multiple matches or no match: pass product_name to WP endpoint as usual } $payload = []; if ($productId !== null && $productId > 0) $payload['product_id'] = $productId; if ($productName !== null && $productName !== '') $payload['product_name'] = $productName; if ($carBrand !== null && $carBrand !== '') $payload['car_brand'] = $carBrand; if ($carModel !== null && $carModel !== '') $payload['car_model'] = $carModel; if ($carYear !== null && $carYear !== '') $payload['car_year'] = $carYear; if ($carEngine !== null && $carEngine !== '') $payload['car_engine'] = $carEngine; if ($carEngineIndex >= 0) $payload['car_engine_index'] = $carEngineIndex; if ($language !== null && $language !== '') $payload['lang'] = $language; $payload['currency'] = $locale['currency']; $res = HttpClient::request('POST', $restBase . '/product-compatibility', [], $payload); $data = $res['json'] ?? $res['body']; // Post-process product titles: the WP endpoint does not switch WPML for the // selected_product branch, so titles come back in Polish. Re-fetch from WC REST // API with the correct language to get the translated title. if (is_array($data) && $language !== null && $language !== '' && $language !== 'pl') { $woo = new WooCommerceClient(); // Case A: specific product compatibility check — replace title in selected_product if (isset($data['selected_product']['id'])) { try { $translated = $woo->getProduct( productId: (int)$data['selected_product']['id'], lang: $language, currency: $locale['currency'] ); $data['selected_product']['title'] = $translated['name'] ?? $data['selected_product']['title']; } catch (\Throwable) { /* keep original title on failure */ } } // Case B: disambiguation options list for product_name — batch-translate titles if ( ($data['error'] ?? '') === 'options' && ($data['field'] ?? '') === 'product_name' && !empty($data['options']) && is_array($data['options']) ) { $ids = array_values(array_filter(array_column($data['options'], 'id'))); if (!empty($ids)) { try { $translated = $woo->getProductsByIds($ids, $language, $locale['currency']); $byId = []; foreach ($translated as $tp) { $byId[(int)($tp['id'] ?? 0)] = $tp['name'] ?? null; } foreach ($data['options'] as &$opt) { $pid = (int)($opt['id'] ?? 0); if ($pid && isset($byId[$pid])) { $opt['title'] = $byId[$pid]; } } unset($opt); } catch (\Throwable) { /* keep original titles on failure */ } } } } $response = [ 'ok' => true, 'data' => $data, 'currency' => $locale['currency'], ]; if ($locale['fallback']) { $response['currency_fallback'] = true; $response['currency_note'] = self::buildCurrencyNote($locale, $language); } Router::sendJson(200, $response); } // ================================================================= // POST /tools/get_car_data // ================================================================= public static function getCarData(): void { Auth::requireBearer(); $body = Router::jsonBody(); $restBase = rtrim(Env::get('WOOCOMMERCE_BASE_URL'), '/') . '/wp-json/eksrelay/v1'; $carBrand = $body['car_brand'] ?? ($body['make'] ?? null); $carModel = $body['car_model'] ?? ($body['model'] ?? null); $carYear = $body['car_year'] ?? ($body['year'] ?? null); $carEngine = $body['car_engine'] ?? ($body['engine'] ?? null); $payload = []; if ($carBrand !== null && $carBrand !== '') $payload['car_brand'] = $carBrand; if ($carModel !== null && $carModel !== '') $payload['car_model'] = $carModel; if ($carYear !== null && $carYear !== '') $payload['car_year'] = $carYear; if ($carEngine !== null && $carEngine !== '') $payload['car_engine'] = $carEngine; $res = HttpClient::request( 'POST', $restBase . '/car-data', [], $payload ); Router::sendJson(200, ['ok' => true, 'data' => $res['json'] ?? $res['body']]); } // ================================================================= // Formatters (curate data for LLM consumption) // ================================================================= private static function formatOrder(array $order): array { return [ 'id' => $order['id'] ?? null, 'number' => $order['number'] ?? null, 'status' => $order['status'] ?? null, 'date_created' => $order['date_created'] ?? null, 'total' => $order['total'] ?? null, 'currency' => $order['currency'] ?? null, 'billing' => [ 'first_name' => $order['billing']['first_name'] ?? '', 'last_name' => $order['billing']['last_name'] ?? '', 'email' => $order['billing']['email'] ?? '', 'phone' => $order['billing']['phone'] ?? '', 'city' => $order['billing']['city'] ?? '', 'country' => $order['billing']['country'] ?? '', ], 'shipping' => [ 'first_name' => $order['shipping']['first_name'] ?? '', 'last_name' => $order['shipping']['last_name'] ?? '', 'city' => $order['shipping']['city'] ?? '', 'country' => $order['shipping']['country'] ?? '', 'address_1' => $order['shipping']['address_1'] ?? '', ], 'payment_method' => $order['payment_method_title'] ?? null, 'shipping_total' => $order['shipping_total'] ?? null, 'line_items' => array_map(fn($item) => [ 'name' => $item['name'] ?? '', 'sku' => $item['sku'] ?? '', 'quantity' => $item['quantity'] ?? 0, 'total' => $item['total'] ?? '0', ], $order['line_items'] ?? []), 'shipping_lines' => array_map(fn($sl) => [ 'method_title' => $sl['method_title'] ?? '', 'total' => $sl['total'] ?? '0', ], $order['shipping_lines'] ?? []), 'customer_note' => $order['customer_note'] ?? '', ]; } private static function buildCurrencyNote(array $locale, ?string $language): string { if (($locale['reason'] ?? '') === 'unknown_language') { return 'Unknown language code ' . ($language ?? '?') . '. Prices shown in ' . StoreLocales::FALLBACK_CURRENCY . '.'; } $natural = $locale['natural_currency'] ?? ($language ?? '?'); return 'Currency for ' . $natural . ' is not supported in the store. Prices shown in ' . StoreLocales::FALLBACK_CURRENCY . '.'; } private static function formatProduct(array $product): array { return [ 'id' => $product['id'] ?? null, 'name' => $product['name'] ?? '', 'sku' => $product['sku'] ?? '', 'slug' => $product['slug'] ?? '', 'status' => $product['status'] ?? '', 'price' => $product['price'] ?? '', 'regular_price' => $product['regular_price'] ?? '', 'sale_price' => $product['sale_price'] ?? '', 'stock_status' => $product['stock_status'] ?? '', 'stock_quantity' => $product['stock_quantity'] ?? null, 'short_description' => strip_tags((string)($product['short_description'] ?? '')), 'categories' => array_map(fn($c) => $c['name'] ?? '', $product['categories'] ?? []), 'attributes' => array_map(fn($a) => [ 'name' => $a['name'] ?? '', 'options' => $a['options'] ?? [], ], $product['attributes'] ?? []), 'permalink' => $product['permalink'] ?? '', ]; } }