|
|
|
@@ -7,8 +7,11 @@ import type {
|
|
|
|
|
IStockLocationService,
|
|
|
|
|
} from "@medusajs/framework/types";
|
|
|
|
|
import {
|
|
|
|
|
createCollectionsWorkflow,
|
|
|
|
|
createInventoryItemsWorkflow,
|
|
|
|
|
createProductCategoriesWorkflow,
|
|
|
|
|
createProductsWorkflow,
|
|
|
|
|
updateProductsWorkflow,
|
|
|
|
|
uploadFilesWorkflow,
|
|
|
|
|
} from "@medusajs/medusa/core-flows";
|
|
|
|
|
import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
|
|
|
|
@@ -20,6 +23,27 @@ import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
|
|
|
|
|
// fila se procesa independiente (una foto rota o categoria mal escrita
|
|
|
|
|
// no tumba el resto del archivo).
|
|
|
|
|
//
|
|
|
|
|
// categoria/coleccion: si el nombre no matchea una categoria/coleccion
|
|
|
|
|
// ya existente (case-insensitive), se crea nueva -- antes se descartaba
|
|
|
|
|
// en silencio y el producto quedaba sin categoria/coleccion sin avisar
|
|
|
|
|
// (confirmado en vivo: "Hello Kitty & Friends" nunca se creo como
|
|
|
|
|
// coleccion). El handle de la nueva categoria/coleccion se genera con
|
|
|
|
|
// el mismo slugify() ASCII-safe de mas abajo, por la misma razon que el
|
|
|
|
|
// 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).
|
|
|
|
@@ -50,6 +74,7 @@ type RowResult = {
|
|
|
|
|
row: number;
|
|
|
|
|
title: string;
|
|
|
|
|
ok: boolean;
|
|
|
|
|
action?: "created" | "updated";
|
|
|
|
|
error?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -139,7 +164,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|
|
|
|
const salesChannelService = req.scope.resolve<ISalesChannelModuleService>(Modules.SALES_CHANNEL);
|
|
|
|
|
const productService = req.scope.resolve<IProductModuleService>(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({}),
|
|
|
|
@@ -150,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;
|
|
|
|
@@ -169,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[] = [];
|
|
|
|
|
|
|
|
|
@@ -199,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: {
|
|
|
|
@@ -215,8 +261,33 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|
|
|
|
const inventoryItemId = inventoryItems[0]?.id;
|
|
|
|
|
if (!inventoryItemId) throw new Error("No se pudo crear el registro de inventario");
|
|
|
|
|
|
|
|
|
|
const categoryId = row.categoria ? categoryByName.get(row.categoria.toLowerCase().trim()) : undefined;
|
|
|
|
|
const collectionId = row.coleccion ? collectionByName.get(row.coleccion.toLowerCase().trim()) : undefined;
|
|
|
|
|
const categoryName = row.categoria?.trim();
|
|
|
|
|
let categoryId = categoryName ? categoryByName.get(categoryName.toLowerCase()) : undefined;
|
|
|
|
|
if (categoryName && !categoryId) {
|
|
|
|
|
const { result: createdCategories } = await createProductCategoriesWorkflow(req.scope).run({
|
|
|
|
|
input: {
|
|
|
|
|
product_categories: [
|
|
|
|
|
{ name: categoryName, handle: slugify(categoryName, `${Date.now().toString(36)}${index}`) },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
categoryId = createdCategories[0].id;
|
|
|
|
|
categoryByName.set(categoryName.toLowerCase(), categoryId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const collectionName = row.coleccion?.trim();
|
|
|
|
|
let collectionId = collectionName ? collectionByName.get(collectionName.toLowerCase()) : undefined;
|
|
|
|
|
if (collectionName && !collectionId) {
|
|
|
|
|
const { result: createdCollections } = await createCollectionsWorkflow(req.scope).run({
|
|
|
|
|
input: {
|
|
|
|
|
collections: [
|
|
|
|
|
{ title: collectionName, handle: slugify(collectionName, `${Date.now().toString(36)}${index}`) },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
collectionId = createdCollections[0].id;
|
|
|
|
|
collectionByName.set(collectionName.toLowerCase(), collectionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await createProductsWorkflow(req.scope).run({
|
|
|
|
|
input: {
|
|
|
|
@@ -247,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 });
|
|
|
|
|
}
|
|
|
|
|