Reportado en produccion: el carrito mostraba el logo generico de ARI en vez de la foto del producto. Verificado en vivo: item.thumbnail viene null en la respuesta de Medusa para todo el catalogo real (confirmado tambien contra /store/products directamente -- el campo thumbnail del producto es null en la fuente, no solo omitido en la respuesta del carrito). El catalogo ya resuelve esto cayendo a images[0] (mapProduct() en medusa.ts); la linea del carrito necesita pedir esa relacion explicita via fields=*items.product.images (no viene por defecto) y aplicar el mismo fallback.
174 lines
5.4 KiB
TypeScript
174 lines
5.4 KiB
TypeScript
// 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<Record<string, unknown>> {
|
|
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<CartSummary> {
|
|
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<CartSummary> {
|
|
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<CartSummary> {
|
|
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<CartSummary> {
|
|
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<CartSummary> {
|
|
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);
|
|
}
|