// 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; shippingTotal: number; total: number; items: CartLine[]; } export interface ShippingAddressInput { firstName: string; lastName: string; address1: string; city: string; province?: string; phone: string; email: string; } export interface ShippingOption { id: string; name: string; amount: number; } 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; shipping_total?: number | null; total?: number | null; items?: MedusaLineItem[] | null; }; function mapCart(cart: MedusaCart): CartSummary { return { cartId: cart.id, currency: cart.currency_code, subtotal: cart.subtotal ?? 0, shippingTotal: cart.shipping_total ?? 0, total: cart.total ?? 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); } export async function setShippingAddress( cartId: string, address: ShippingAddressInput, ): Promise { const data = await medusaCartFetch(`/store/carts/${cartId}?${ITEMS_FIELDS}`, { method: "POST", body: { email: address.email, shipping_address: { first_name: address.firstName, last_name: address.lastName, address_1: address.address1, city: address.city, province: address.province || undefined, country_code: "co", phone: address.phone, }, }, }); return mapCart(data.cart as MedusaCart); } type MedusaShippingOption = { id: string; name: string; amount?: number | null; calculated_price?: { calculated_amount?: number | null } | null; }; export async function getShippingOptions(cartId: string): Promise { const data = await medusaCartFetch(`/store/shipping-options?cart_id=${cartId}`); const options = (data.shipping_options as MedusaShippingOption[] | undefined) ?? []; return options.map((option) => ({ id: option.id, name: option.name, amount: option.calculated_price?.calculated_amount ?? option.amount ?? 0, })); } export async function addShippingMethod(cartId: string, optionId: string): Promise { const data = await medusaCartFetch(`/store/carts/${cartId}/shipping-methods?${ITEMS_FIELDS}`, { method: "POST", body: { option_id: optionId }, }); return mapCart(data.cart as MedusaCart); }