diff --git a/workloads/commerce-backend/src/admin/routes/importacion/page.tsx b/workloads/commerce-backend/src/admin/routes/importacion/page.tsx index 4da4f8d..1a78b40 100644 --- a/workloads/commerce-backend/src/admin/routes/importacion/page.tsx +++ b/workloads/commerce-backend/src/admin/routes/importacion/page.tsx @@ -14,6 +14,7 @@ type RowResult = { row: number; title: string; ok: boolean; + action?: "created" | "updated"; error?: string; }; @@ -91,9 +92,11 @@ const ProductImportPanel = () => {
Columnas: titulo, descripcion, precio, categoria, coleccion, cantidad, fotos. Varias fotos se - separan con ; dentro de la misma celda. categoria/coleccion deben - escribirse igual que en la tienda o se dejan vacías. Cada foto se descarga y se guarda en el almacenamiento - propio de la tienda (no queda enlazada al sitio de origen). + separan con ; dentro de la misma celda. Si categoria/coleccion no + coinciden con una ya existente, se crean nuevas. Si titulo coincide con un producto ya + importado, no se duplica: se le agregan las fotos nuevas y se actualiza la descripción si la fila trae una. + Cada foto se descarga y se guarda en el almacenamiento propio de la tienda (no queda enlazada al sitio de + origen).
@@ -122,7 +125,7 @@ const ProductImportPanel = () => { {result.title} {result.ok ? ( - Importado + {result.action === "updated" ? "Actualizado (ya existía)" : "Importado"} ) : ( {result.error} )} diff --git a/workloads/commerce-backend/src/api/admin/product-import/route.ts b/workloads/commerce-backend/src/api/admin/product-import/route.ts index e1a46a4..b5b6031 100644 --- a/workloads/commerce-backend/src/api/admin/product-import/route.ts +++ b/workloads/commerce-backend/src/api/admin/product-import/route.ts @@ -11,6 +11,7 @@ import { createInventoryItemsWorkflow, createProductCategoriesWorkflow, createProductsWorkflow, + updateProductsWorkflow, uploadFilesWorkflow, } from "@medusajs/medusa/core-flows"; import { csvRowsToObjects, parseCsv } from "../../../lib/csv"; @@ -31,6 +32,18 @@ import { csvRowsToObjects, parseCsv } from "../../../lib/csv"; // handle del producto: el default de Medusa no quita tildes/enies y // rompe la ruta publica. // +// Re-importar el mismo titulo (ej. para agregar mas fotos o actualizar +// la descripcion de un producto ya cargado) actualiza el producto +// existente en vez de crear uno nuevo -- antes cada re-importacion +// generaba un producto duplicado con handle distinto (confirmado en +// vivo con "Hello Kitty & Friends": misma fila re-subida para sumar +// fotos, dos productos con el mismo titulo en la tienda). El match es +// por titulo exacto (case-insensitive/trim), igual que categoria y +// coleccion; en un match, las fotos nuevas se agregan a las que ya +// tenia (no se reemplazan) y la descripcion se actualiza si la fila +// trae una. No toca precio/cantidad/categoria/coleccion del producto +// existente -- eso se edita a mano en el admin si hace falta. +// // Las fotos se descargan del origen y se re-alojan en el mismo // MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow -> // mismo File Module Provider de medusa-config.ts, ya corre en el NAS). @@ -61,6 +74,7 @@ type RowResult = { row: number; title: string; ok: boolean; + action?: "created" | "updated"; error?: string; }; @@ -150,7 +164,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo const salesChannelService = req.scope.resolve(Modules.SALES_CHANNEL); const productService = req.scope.resolve(Modules.PRODUCT); - const [locations, shippingProfiles, salesChannels, categories, collections] = await Promise.all([ + const [locations, shippingProfiles, salesChannels, categories, collections, existingProducts] = await Promise.all([ stockLocationService.listStockLocations({}), fulfillmentService.listShippingProfiles({}), salesChannelService.listSalesChannels({}), @@ -161,6 +175,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo // los trae completos). productService.listProductCategories({}, { select: ["id", "name"] }), productService.listProductCollections({}, { select: ["id", "title"] }), + productService.listProducts({}, { select: ["id", "title"], relations: ["images"] }), ]); const locationId = locations[0]?.id; @@ -180,6 +195,9 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo const collectionByName = new Map( collections.filter((c) => c.title).map((c) => [c.title.toLowerCase().trim(), c.id]), ); + const productByTitle = new Map( + existingProducts.filter((p) => p.title).map((p) => [p.title.toLowerCase().trim(), p]), + ); const results: RowResult[] = []; @@ -210,6 +228,23 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo }, }); + const existingProduct = productByTitle.get(title.toLowerCase()); + if (existingProduct) { + await updateProductsWorkflow(req.scope).run({ + input: { + products: [ + { + id: existingProduct.id, + description: row.descripcion?.trim() || undefined, + images: [...(existingProduct.images ?? []).map((image) => ({ url: image.url })), ...uploadedFiles.map((f) => ({ url: f.url }))], + }, + ], + }, + }); + results.push({ row: rowNumber, title, ok: true, action: "updated" }); + continue; + } + const quantity = Number(row.cantidad); const { result: inventoryItems } = await createInventoryItemsWorkflow(req.scope).run({ input: { @@ -283,7 +318,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo }, }); - results.push({ row: rowNumber, title, ok: true }); + results.push({ row: rowNumber, title, ok: true, action: "created" }); } catch (error) { results.push({ row: rowNumber, title: title || `Fila ${rowNumber}`, ok: false, error: (error as Error).message }); }