|
|
@@ -37,24 +37,37 @@ final class WooToolsHandler
|
|
|
throw new HttpException(400, 'MISSING_PARAMS', 'email is required to verify order ownership.');
|
|
|
}
|
|
|
|
|
|
+ $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 ($orderNumber !== null && $orderNumber !== '') {
|
|
|
- $order = $woo->getOrder(orderNumber: $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);
|
|
|
+ $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, [
|
|
|
- 'ok' => true,
|
|
|
- 'data' => self::formatOrder($order),
|
|
|
- ]);
|
|
|
+ Router::sendJson(200, $response);
|
|
|
}
|
|
|
|
|
|
// =================================================================
|
|
|
@@ -68,25 +81,38 @@ final class WooToolsHandler
|
|
|
$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);
|
|
|
- Router::sendJson(200, ['ok' => true, 'data' => self::formatProduct($product)]);
|
|
|
+ $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);
|
|
|
- Router::sendJson(200, ['ok' => true, 'data' => self::formatProduct($product)]);
|
|
|
+ $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);
|
|
|
+ $products = $woo->searchProducts($search, currency: $locale['currency'], lang: $lang);
|
|
|
$formatted = array_map(fn($p) => self::formatProduct($p), $products);
|
|
|
- Router::sendJson(200, ['ok' => true, 'data' => $formatted]);
|
|
|
+ Router::sendJson(200, array_merge(['ok' => true, 'data' => $formatted], $currencyMeta));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -101,6 +127,12 @@ final class WooToolsHandler
|
|
|
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
|
|
|
@@ -108,17 +140,42 @@ final class WooToolsHandler
|
|
|
|
|
|
try {
|
|
|
if ($zoneId !== null) {
|
|
|
- $methods = $woo->getShippingMethods((int)$zoneId);
|
|
|
- Router::sendJson(200, ['ok' => true, 'data' => ['zone_id' => (int)$zoneId, 'methods' => $methods]]);
|
|
|
+ // 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 (without methods to avoid 29+ API calls)
|
|
|
- $zones = $woo->getShippingZones();
|
|
|
- $formatted = array_map(fn($z) => [
|
|
|
- 'id' => $z['id'] ?? 0,
|
|
|
- 'name' => $z['name'] ?? '',
|
|
|
- ], $zones);
|
|
|
+ // 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,
|
|
|
@@ -186,27 +243,107 @@ final class WooToolsHandler
|
|
|
|
|
|
$restBase = rtrim(Env::get('WOOCOMMERCE_BASE_URL'), '/') . '/wp-json/eksrelay/v1';
|
|
|
|
|
|
- $productName = $body['product_name'] ?? ($body['productName'] ?? 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);
|
|
|
+ $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 ($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 */ }
|
|
|
+ }
|
|
|
|
|
|
- $res = HttpClient::request(
|
|
|
- 'POST',
|
|
|
- $restBase . '/product-compatibility',
|
|
|
- [],
|
|
|
- $payload
|
|
|
- );
|
|
|
+ // 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 */ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- Router::sendJson(200, ['ok' => true, 'data' => $res['json'] ?? $res['body']]);
|
|
|
+ $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);
|
|
|
}
|
|
|
|
|
|
// =================================================================
|
|
|
@@ -284,6 +421,15 @@ final class WooToolsHandler
|
|
|
];
|
|
|
}
|
|
|
|
|
|
+ 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 [
|