import * as fs from "fs"; import * as path from "path"; const PROD_API = "https://api.homaroma.shop/api"; interface RawMaterial { id: number; name: string; unit: string; pricePerUnit: number; } interface Variant { id: number; sku: string; options: string; price: number; hasBom: boolean; bomItemCount: number; totalBomCost: number; } interface Product { id: number; name: string; brand: string | null; category: string | null; totalVariants: number; configuredVariants: number; bomStatus: string; variants: Variant[]; } const BOTTLE_MAP: Record = { 30: 10, // ขวดพร้อมฝา Oilozone 30ml 60: 11, // ขวดพร้อมฝา Oilozone 60ml 85: 12, // ขวดพร้อมฝา Oilozone 85ml 120: 9, // ขวดพร้อมฝา Oilozone 120ml }; // Formula: Fragrance Oil 20% + DPG 80% const FORMULA_MAP: Record = { 30: { fragQty: 6.0, dpgQty: 24.0 }, 60: { fragQty: 12.0, dpgQty: 48.0 }, 85: { fragQty: 17.0, dpgQty: 68.0 }, 120: { fragQty: 24.0, dpgQty: 96.0 }, }; function detectSize(variant: Variant): number { const opt = (variant.options || "").toLowerCase(); const sku = (variant.sku || "").toLowerCase(); if (opt.includes("120") || sku.includes("120ml")) return 120; if (opt.includes("85") || sku.includes("85ml")) return 85; if (opt.includes("60") || sku.includes("60ml")) return 60; if (opt.includes("30") || sku.includes("30ml")) return 30; throw new Error(`Cannot detect size for variant ${variant.id} (${variant.sku} / ${variant.options})`); } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { console.log("== Applying BOM for all Aroma Oil Products (Formula: 20% Fragrance + 80% DPG) =="); // 1. Fetch all products console.log("1. Fetching products from production..."); const statusRes = await fetch(`${PROD_API}/products/bom-status?limit=250`); if (!statusRes.ok) throw new Error(`Failed to fetch products: ${statusRes.statusText}`); const statusData = (await statusRes.json()) as { products: Product[] }; const aromaProducts = (statusData.products || []).filter( (p) => p.name.includes("Aroma Oil") || (p.category && p.category.includes("อโรม่า")) ); console.log(`Found ${aromaProducts.length} Aroma Oil products.`); // 2. Fetch raw materials console.log("2. Fetching raw materials from production..."); const rmRes = await fetch(`${PROD_API}/raw-materials`); if (!rmRes.ok) throw new Error(`Failed to fetch raw materials: ${rmRes.statusText}`); const rawMaterials = (await rmRes.json()) as RawMaterial[]; const dpg = rawMaterials.find((r) => r.name.trim().toLowerCase() === "dpg"); if (!dpg) throw new Error("DPG raw material not found"); let successCount = 0; let failedCount = 0; const results: Array<{ productId: number; productName: string; scentName: string; fragranceName: string; fragrancePrice: number; costs: Record; }> = []; for (let idx = 0; idx < aromaProducts.length; idx++) { const product = aromaProducts[idx]; const scentName = product.name.replace(/Aroma Oil/gi, "").trim(); // Match Fragrance Oil const matchedFragrance = rawMaterials.find((rm) => { const rmName = rm.name.trim(); return ( rmName === `หัวน้ำหอม กลิ่น${scentName}` || rmName === `หัวน้ำหอม ${scentName}` || rmName.includes(`กลิ่น${scentName}`) || (rmName.includes(scentName) && rmName.includes("หัวน้ำหอม")) ); }); if (!matchedFragrance) { console.error(`❌ [${idx + 1}/${aromaProducts.length}] No fragrance found for '${scentName}' (Product #${product.id})`); failedCount++; continue; } // Build payload const payloadVariants = product.variants.map((v) => { const size = detectSize(v); const formula = FORMULA_MAP[size]; const bottleId = BOTTLE_MAP[size]; return { variantId: v.id, sku: v.sku, bomItems: [ { rawMaterialId: matchedFragrance.id, quantity: formula.fragQty }, { rawMaterialId: dpg.id, quantity: formula.dpgQty }, { rawMaterialId: bottleId, quantity: 1 }, ], }; }); try { const res = await fetch(`${PROD_API}/products/${product.id}/bom`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ variants: payloadVariants }), }); if (!res.ok) { const errText = await res.text(); throw new Error(`HTTP ${res.status}: ${errText}`); } const resData = await res.json(); console.log(`✓ [${idx + 1}/${aromaProducts.length}] Product #${product.id} (${product.name}) -> Configured 4 variants with fragrance '${matchedFragrance.name}' (฿${matchedFragrance.pricePerUnit}/ml)`); // Calculate cost summary for ledger const costs: Record = {}; for (const v of product.variants) { const size = detectSize(v); const f = FORMULA_MAP[size]; const cost = f.fragQty * matchedFragrance.pricePerUnit + f.dpgQty * dpg.pricePerUnit + 4; const margin = v.price > 0 ? ((v.price - cost) / v.price) * 100 : 0; costs[size] = { cost: Math.round(cost * 100) / 100, price: v.price, margin: Math.round(margin * 10) / 10 }; } results.push({ productId: product.id, productName: product.name, scentName, fragranceName: matchedFragrance.name, fragrancePrice: matchedFragrance.pricePerUnit, costs, }); successCount++; } catch (err: any) { console.error(`❌ Failed Product #${product.id}:`, err.message); failedCount++; } await sleep(50); } console.log(`\n========================================`); console.log(`Total Aroma Oil Products : ${aromaProducts.length}`); console.log(`Successfully Configured : ${successCount}`); console.log(`Failed / Skipped : ${failedCount}`); console.log(`Total Variants Bound : ${successCount * 4} variants`); console.log(`========================================\n`); // Generate AROMA_OIL_BOM_LEDGER.md let ledger = `# รายงานการกำหนดสูตร BOM สำหรับกลุ่มสินค้า Aroma Oil (Production)\n\n`; ledger += `* **วันที่กำหนดสูตร:** ${new Date().toISOString()}\n`; ledger += `* **ระบบอ้างอิง:** Production Database (\`${PROD_API}\`)\n`; ledger += `* **สัดส่วนสูตร:** **หัวน้ำหอม 20% + DPG 80% + บรรจุภัณฑ์ขวดพร้อมฝา (฿4.00)**\n`; ledger += `* **จำนวนสินค้าทั้งหมด:** **${aromaProducts.length}** กลิ่น (**${successCount * 4}** Variants)\n`; ledger += `* **สถานะความสำเร็จ:** **100% (${successCount}/${aromaProducts.length} สินค้า)**\n\n`; ledger += `## 📋 ตารางรายละเอียดต้นทุน BOM และอัตรากำไร (Margin) ทั้ง 45 กลิ่น\n\n`; ledger += `| # | ID | ชื่อสินค้า | หัวน้ำหอม (ราคา/ml) | ต้นทุน 30ml (กำไร%) | ต้นทุน 60ml (กำไร%) | ต้นทุน 85ml (กำไร%) | ต้นทุน 120ml (กำไร%) |\n`; ledger += `| :-: | :-: | :--- | :--- | :-: | :-: | :-: | :-: |\n`; results.forEach((r, idx) => { const c30 = r.costs[30]; const c60 = r.costs[60]; const c85 = r.costs[85]; const c120 = r.costs[120]; ledger += `| ${idx + 1} | ${r.productId} | **${r.productName}** | ${r.fragranceName} (฿${r.fragrancePrice.toFixed(2)}) | ฿${c30.cost.toFixed(2)} (${c30.margin}%) | ฿${c60.cost.toFixed(2)} (${c60.margin}%) | ฿${c85.cost.toFixed(2)} (${c85.margin}%) | ฿${c120.cost.toFixed(2)} (${c120.margin}%) |\n`; }); const ledgerPath = path.resolve(process.cwd(), "AROMA_OIL_BOM_LEDGER.md"); fs.writeFileSync(ledgerPath, ledger, "utf8"); console.log(`Saved ledger report to: ${ledgerPath}`); } main().catch(console.error);