- Configured BOMs for all 585 master variants across 5 categories (Oil Ozone, Aroma Oil, Car Diffuser, Room Spray, Reed Diffuser) - Mapped 2,111 marketplace products (Shopee and Lazada) to master catalog variants - Added backend profitability routes (/api/profitability/matrix, /omnichannel, /mrp) - Added 3 admin pages under Product menu: Cost & Profitability Matrix, Omnichannel Margin, and MRP & Material Demand - Added comprehensive ledgers and production sync scripts
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import prisma from "../apps/backend/src/lib/prisma";
|
|
|
|
const PROD_API = "https://api.homaroma.shop/api";
|
|
|
|
async function main() {
|
|
console.log("== Syncing Production Raw Materials and BOMs to Local DB ==");
|
|
|
|
// 1. Fetch Production Raw Materials
|
|
console.log("1. Fetching Production Raw Materials...");
|
|
const prodRmsRes = await fetch(`${PROD_API}/raw-materials`);
|
|
const prodRms = (await prodRmsRes.json()) as any[];
|
|
console.log(`Found ${prodRms.length} raw materials on production.`);
|
|
|
|
// Upsert into local DB by name
|
|
const nameToLocalId = new Map<string, number>();
|
|
for (const rm of prodRms) {
|
|
const upserted = await prisma.rawMaterial.upsert({
|
|
where: { name: rm.name },
|
|
create: {
|
|
name: rm.name,
|
|
unit: rm.unit || "piece",
|
|
pricePerUnit: rm.pricePerUnit || 0,
|
|
},
|
|
update: {
|
|
unit: rm.unit || "piece",
|
|
pricePerUnit: rm.pricePerUnit || 0,
|
|
},
|
|
});
|
|
nameToLocalId.set(rm.name, upserted.id);
|
|
}
|
|
console.log("✓ Raw materials synced to local DB.");
|
|
|
|
// 2. Fetch Production BOM Status
|
|
console.log("2. Fetching Production BOM status...");
|
|
const prodStatusRes = await fetch(`${PROD_API}/products/bom-status?limit=300`);
|
|
const prodStatus = (await prodStatusRes.json()) as any;
|
|
const prodProducts = prodStatus.products || [];
|
|
console.log(`Found ${prodProducts.length} products on production.`);
|
|
|
|
// Map to local products and variants by SKU / Product Name
|
|
let variantBomCount = 0;
|
|
|
|
for (const prod of prodProducts) {
|
|
// Fetch full BOM for this product from production
|
|
try {
|
|
const detailRes = await fetch(`${PROD_API}/products/${prod.id}/bom`);
|
|
if (!detailRes.ok) continue;
|
|
const detail = (await detailRes.json()) as any;
|
|
const prodVariants = detail.variants || [];
|
|
|
|
for (const pv of prodVariants) {
|
|
if (!pv.bomItems || pv.bomItems.length === 0) continue;
|
|
|
|
// Find local variant by sku
|
|
const localVariant = await prisma.productVariant.findFirst({
|
|
where: { sku: pv.sku },
|
|
});
|
|
|
|
if (!localVariant) continue;
|
|
|
|
// Clear existing local BOM items for this variant
|
|
await prisma.bomItem.deleteMany({
|
|
where: { productVariantId: localVariant.id },
|
|
});
|
|
|
|
// Create new BOM items
|
|
for (const item of pv.bomItems) {
|
|
const rmName = item.name || item.rawMaterial?.name;
|
|
const localRmId = rmName ? nameToLocalId.get(rmName) : null;
|
|
if (!localRmId) continue;
|
|
|
|
await prisma.bomItem.create({
|
|
data: {
|
|
productVariantId: localVariant.id,
|
|
rawMaterialId: localRmId,
|
|
quantity: item.quantity,
|
|
},
|
|
});
|
|
}
|
|
variantBomCount++;
|
|
}
|
|
} catch (err: any) {
|
|
console.error(`Error syncing product #${prod.id}:`, err.message);
|
|
}
|
|
}
|
|
|
|
console.log(`✓ Successfully synced ${variantBomCount} variant BOMs to local DB!`);
|
|
|
|
// 3. Sync Marketplace Product Mappings from Production
|
|
console.log("3. Syncing Marketplace Product Mappings...");
|
|
try {
|
|
const mpRes = await fetch(`${PROD_API}/tools/marketplace-products?limit=3000`);
|
|
if (mpRes.ok) {
|
|
const mpData = (await mpRes.json()) as any;
|
|
const mpItems = mpData.items || [];
|
|
console.log(`Found ${mpItems.length} marketplace products on production.`);
|
|
|
|
let mappedCount = 0;
|
|
for (const item of mpItems) {
|
|
if (!item.masterVariant?.sku) continue;
|
|
|
|
const localVariant = await prisma.productVariant.findFirst({
|
|
where: { sku: item.masterVariant.sku },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!localVariant) continue;
|
|
|
|
// Upsert or update local MarketplaceProduct
|
|
const existing = await prisma.marketplaceProduct.findFirst({
|
|
where: {
|
|
channel: item.channel,
|
|
shop: item.shop,
|
|
productName: item.productName,
|
|
variation: item.variation || "",
|
|
marketplaceSku: item.marketplaceSku || "",
|
|
},
|
|
});
|
|
|
|
if (existing) {
|
|
await prisma.marketplaceProduct.update({
|
|
where: { id: existing.id },
|
|
data: {
|
|
masterVariantId: localVariant.id,
|
|
totalSoldQty: item.totalSoldQty,
|
|
totalRevenue: item.totalRevenue,
|
|
avgPrice: item.avgPrice,
|
|
},
|
|
});
|
|
mappedCount++;
|
|
} else {
|
|
await prisma.marketplaceProduct.create({
|
|
data: {
|
|
channel: item.channel,
|
|
shop: item.shop,
|
|
productName: item.productName,
|
|
variation: item.variation || "",
|
|
marketplaceSku: item.marketplaceSku || "",
|
|
totalSoldQty: item.totalSoldQty,
|
|
totalRevenue: item.totalRevenue,
|
|
avgPrice: item.avgPrice,
|
|
masterVariantId: localVariant.id,
|
|
},
|
|
});
|
|
mappedCount++;
|
|
}
|
|
}
|
|
console.log(`✓ Synced ${mappedCount} marketplace product mappings to local DB.`);
|
|
}
|
|
} catch (err: any) {
|
|
console.warn("Marketplace sync warning:", err.message);
|
|
}
|
|
}
|
|
|
|
main()
|
|
.catch(console.error)
|
|
.finally(() => prisma.$disconnect());
|