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 });
}
|