// ADR 0009: carrito real conectado a Medusa (Store API), en vez del // estado 100% local que existia antes. Archivo separado de medusa.ts // a proposito -- ese es de solo lectura (catalogo, cacheado con // revalidate/tags), este es de escritura (carrito, siempre no-store, // nunca debe cachearse). // // Formas de respuesta verificadas en vivo contra la API real // (2026-08-29), no asumidas contra documentacion: // - crear/agregar/actualizar linea: { cart: {...} } // - eliminar linea: { id, object, deleted, parent: {...} } -- el // carrito actualizado viene en "parent", no en "cart". // - agregar el mismo variant_id dos veces hace merge de cantidad en la // misma linea (confirmado con un carrito de prueba real), no crea // una linea duplicada -- por eso addLineItem siempre manda // quantity=1 sin revisar antes si ya existe. export interface CartLineProduct { id: string; name: string; thumbnail: string; price: number | null; currency: string; } export interface CartLine { lineItemId: string; product: CartLineProduct; quantity: number; variantId: string; } export interface CartSummary { cartId: string; currency: string; subtotal: number; items: CartLine[]; } function backendUrl(): string { const value = process.env.MEDUSA_BACKEND_URL; if (!value) { throw new Error("MEDUSA_BACKEND_URL no está configurado."); } return value.replace(/\/$/, ""); } function regionId(): string { const value = process.env.MEDUSA_REGION_ID; if (!value) { throw new Error("MEDUSA_REGION_ID no está configurado."); } return value; } type MedusaLineItem = { id: string; product_id: string; variant_id: string; title?: string | null; product_title?: string | null; thumbnail?: string | null; quantity: number; unit_price?: number | null; product?: { images?: Array<{ url?: string | null }> | null } | null; }; type MedusaCart = { id: string; currency_code: string; subtotal?: number | null; items?: MedusaLineItem[] | null; }; function mapCart(cart: MedusaCart): CartSummary { return { cartId: cart.id, currency: cart.currency_code, subtotal: cart.subtotal ?? 0, items: (cart.items ?? []).map((item) => ({ lineItemId: item.id, variantId: item.variant_id, quantity: item.quantity, product: { id: item.product_id, name: item.product_title || item.title || "Producto", // item.thumbnail viene null en la practica para todo el catalogo // real (confirmado contra /store/products: product.thumbnail // tambien es null ahi, incluso con fotos reales cargadas) -- el // catalogo ya resuelve esto cayendo a images[0] (mapProduct() en // medusa.ts), asi que la linea del carrito pide esa misma relacion // explicita (no viene por defecto) y aplica el mismo fallback. thumbnail: item.thumbnail || item.product?.images?.[0]?.url || "/images/brand/logo-ari.png", price: typeof item.unit_price === "number" ? item.unit_price : null, currency: cart.currency_code, }, })), }; } async function medusaCartFetch( path: string, init?: { method?: string; body?: unknown }, ): Promise> { const publishableKey = process.env.MEDUSA_PUBLISHABLE_KEY; const response = await fetch(`${backendUrl()}${path}`, { method: init?.method ?? "GET", headers: { "Content-Type": "application/json", ...(publishableKey ? { "x-publishable-api-key": publishableKey } : {}), }, body: init?.body ? JSON.stringify(init.body) : undefined, cache: "no-store", }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`Medusa cart API ${response.status}: ${text.slice(0, 300)}`); } return response.json(); } // *items.product.images no viene por defecto (verificado en vivo) y es // lo unico que necesitamos ademas del set por defecto -- *items ya trae // unit_price/product_title/variant_id/quantity sin pedirlos aparte. const ITEMS_FIELDS = "fields=*items,*items.product.images"; export async function createCart(): Promise { const data = await medusaCartFetch("/store/carts", { method: "POST", body: { region_id: regionId() }, }); return mapCart(data.cart as MedusaCart); } export async function getCart(cartId: string): Promise { const data = await medusaCartFetch(`/store/carts/${cartId}?${ITEMS_FIELDS}`); return mapCart(data.cart as MedusaCart); } export async function addLineItem( cartId: string, variantId: string, quantity: number, ): Promise { const data = await medusaCartFetch(`/store/carts/${cartId}/line-items?${ITEMS_FIELDS}`, { method: "POST", body: { variant_id: variantId, quantity }, }); return mapCart(data.cart as MedusaCart); } export async function updateLineItemQuantity( cartId: string, lineItemId: string, quantity: number, ): Promise { const data = await medusaCartFetch(`/store/carts/${cartId}/line-items/${lineItemId}?${ITEMS_FIELDS}`, { method: "POST", body: { quantity }, }); return mapCart(data.cart as MedusaCart); } export async function removeLineItem(cartId: string, lineItemId: string): Promise { const data = await medusaCartFetch(`/store/carts/${cartId}/line-items/${lineItemId}?${ITEMS_FIELDS}`, { method: "DELETE", }); // Forma distinta a las demas: el carrito actualizado viene en // "parent", no en "cart" -- verificado en vivo, no documentado. return mapCart(data.parent as MedusaCart); }