From c9a7c7e942f6ddd1c1f53719959b89cf83787314 Mon Sep 17 00:00:00 2001 From: Lucas Tettamanti Date: Mon, 18 May 2026 00:54:44 -0300 Subject: [PATCH] Woo: construir base de API REST de forma idempotente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El operador puede cargar el base_url con o sin /wp-json/wc/v3. El código anterior siempre concatenaba el sufijo, lo que producía URLs duplicadas tipo https://store/wp-json/wc/v3/wp-json/wc/v3/products → 404 (rest_no_route). Nuevo helper buildWooApiBase detecta si la URL ya incluye /wp-json/ y respeta lo que vino. Centraliza la lógica en un solo lugar consumido por los tres clientes Woo (wooSnapshot, wooOrders, woo.js). Co-Authored-By: Claude Sonnet 4.6 --- src/modules/2-identity/services/woo.js | 3 ++- src/modules/4-woo-orders/wooOrders.js | 4 ++-- src/modules/shared/wooApiBase.js | 13 +++++++++++++ src/modules/shared/wooApiBase.test.js | 25 +++++++++++++++++++++++++ src/modules/shared/wooSnapshot.js | 4 ++-- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/modules/shared/wooApiBase.js create mode 100644 src/modules/shared/wooApiBase.test.js diff --git a/src/modules/2-identity/services/woo.js b/src/modules/2-identity/services/woo.js index a7b9832..43a142a 100644 --- a/src/modules/2-identity/services/woo.js +++ b/src/modules/2-identity/services/woo.js @@ -1,6 +1,7 @@ import crypto from "crypto"; import { getDecryptedTenantEcommerceConfig } from "../db/repo.js"; import { debug } from "../../shared/debug.js"; +import { buildWooApiBase } from "../../shared/wooApiBase.js"; // --- Simple in-memory lock to serialize work per key (e.g. wa_chat_id) --- const locks = new Map(); @@ -172,7 +173,7 @@ export async function createWooCustomer({ tenantId, wa_chat_id, phone, name }) { throw new Error("consumer_secret not set"); })(); - const base = `${cfg.base_url.replace(/\/+$/, "")}/wp-json/${cfg.api_version || "wc/v3"}`; + const base = buildWooApiBase(cfg.base_url, cfg.api_version); const email = `${phone || wa_chat_id}@no-email.local`; const username = wa_chat_id; const timeout = Math.max(cfg.timeout_ms ?? 20000, 20000); diff --git a/src/modules/4-woo-orders/wooOrders.js b/src/modules/4-woo-orders/wooOrders.js index ea1b787..97a4498 100644 --- a/src/modules/4-woo-orders/wooOrders.js +++ b/src/modules/4-woo-orders/wooOrders.js @@ -2,6 +2,7 @@ import { getDecryptedTenantEcommerceConfig } from "../2-identity/db/repo.js"; import { debug as dbg } from "../shared/debug.js"; import { getSnapshotPriceByWooId, getSnapshotItemsByIds } from "../shared/wooSnapshot.js"; import { normalizeUnitToken } from "../shared/sellUnitPolicy.js"; +import { buildWooApiBase } from "../shared/wooApiBase.js"; import * as ordersRepo from "./ordersRepo.js"; // --- Simple in-memory lock to serialize work per key --- @@ -90,8 +91,7 @@ async function getWooClient({ tenantId }) { (() => { throw new Error("consumer_secret not set"); })(); - const apiVersion = cfg.api_version || "wc/v3"; - const base = `${cfg.base_url.replace(/\/+$/, "")}/wp-json/${apiVersion}`; + const base = buildWooApiBase(cfg.base_url, cfg.api_version); const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64"); return { base, diff --git a/src/modules/shared/wooApiBase.js b/src/modules/shared/wooApiBase.js new file mode 100644 index 0000000..a4dac42 --- /dev/null +++ b/src/modules/shared/wooApiBase.js @@ -0,0 +1,13 @@ +/** + * Construye la URL base de la API REST de WooCommerce de forma idempotente. + * + * Acepta tanto el formato "limpio" (https://store.com) como el formato + * completo (https://store.com/wp-json/wc/v3) que muchas docs/operadores + * usan al copiar la URL. Si ya viene con /wp-json/, se respeta tal cual. + */ +export function buildWooApiBase(baseUrl, apiVersion = "wc/v3") { + const cleanBase = String(baseUrl || "").replace(/\/+$/, ""); + const cleanVersion = String(apiVersion || "wc/v3").replace(/^\/+|\/+$/g, ""); + if (cleanBase.includes("/wp-json/")) return cleanBase; + return `${cleanBase}/wp-json/${cleanVersion}`; +} diff --git a/src/modules/shared/wooApiBase.test.js b/src/modules/shared/wooApiBase.test.js new file mode 100644 index 0000000..69ccc82 --- /dev/null +++ b/src/modules/shared/wooApiBase.test.js @@ -0,0 +1,25 @@ +import { buildWooApiBase } from "./wooApiBase.js"; + +describe("buildWooApiBase", () => { + it("agrega /wp-json/wc/v3 al base limpio", () => { + expect(buildWooApiBase("https://x.com", "wc/v3")).toBe("https://x.com/wp-json/wc/v3"); + }); + it("normaliza trailing slash del base", () => { + expect(buildWooApiBase("https://x.com/", "wc/v3")).toBe("https://x.com/wp-json/wc/v3"); + expect(buildWooApiBase("https://x.com///", "wc/v3")).toBe("https://x.com/wp-json/wc/v3"); + }); + it("idempotente: respeta base que ya incluye /wp-json/", () => { + expect(buildWooApiBase("https://x.com/wp-json/wc/v3", "wc/v3")).toBe("https://x.com/wp-json/wc/v3"); + expect(buildWooApiBase("https://x.com/wp-json/wc/v3/", "wc/v3")).toBe("https://x.com/wp-json/wc/v3"); + }); + it("respeta versión distinta si ya viene en la URL", () => { + expect(buildWooApiBase("https://x.com/wp-json/wc/v2", "wc/v3")).toBe("https://x.com/wp-json/wc/v2"); + }); + it("usa wc/v3 por default si no se pasa versión", () => { + expect(buildWooApiBase("https://x.com")).toBe("https://x.com/wp-json/wc/v3"); + }); + it("no rompe con base null/empty", () => { + expect(buildWooApiBase(null)).toBe("/wp-json/wc/v3"); + expect(buildWooApiBase("")).toBe("/wp-json/wc/v3"); + }); +}); diff --git a/src/modules/shared/wooSnapshot.js b/src/modules/shared/wooSnapshot.js index 7e4903c..25bd30c 100644 --- a/src/modules/shared/wooSnapshot.js +++ b/src/modules/shared/wooSnapshot.js @@ -2,6 +2,7 @@ import { pool } from "./db/pool.js"; import { getDecryptedTenantEcommerceConfig } from "../2-identity/db/repo.js"; import { debug as dbg } from "./debug.js"; import { computeAllowedOrderUnits } from "./sellUnitPolicy.js"; +import { buildWooApiBase } from "./wooApiBase.js"; async function fetchWoo({ url, method = "GET", body = null, timeout = 20000, headers = {} }) { const controller = new AbortController(); @@ -58,8 +59,7 @@ async function getWooClient({ tenantId }) { (() => { throw new Error("consumer_secret not set"); })(); - const apiVersion = cfg.api_version || "wc/v3"; - const base = `${cfg.base_url.replace(/\/+$/, "")}/wp-json/${apiVersion}`; + const base = buildWooApiBase(cfg.base_url, cfg.api_version); const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64"); return { base,