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 sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { console.log("== Applying BOM for all Car Diffuser Products =="); console.log("Formula: 8% Fragrance (0.96 ml) + 92% DPG (11.04 ml) + Bottle (฿18.00)\n"); // 1. Fetch 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 carProducts = (statusData.products || []).filter( (p) => p.name.includes("Car Diffuser") || (p.category && p.category.includes("รถยนต์")) ); console.log(`Found ${carProducts.length} Car Diffuser 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"); const bottle = rawMaterials.find((r) => r.name.includes("Car Diffuser") || r.name.includes("ขวดพร้อมฝาไม้")); if (!bottle) throw new Error("Car Diffuser bottle raw material not found"); console.log(`Using DPG: ID ${dpg.id} (฿${dpg.pricePerUnit}/ml)`); console.log(`Using Bottle: ID ${bottle.id} '${bottle.name}' (฿${bottle.pricePerUnit}/piece)\n`); let successCount = 0; let failedCount = 0; const results: Array<{ productId: number; productName: string; scentName: string; variantId: number; sku: string; retailPrice: number; fragranceName: string; fragrancePrice: number; fragranceCost: number; dpgCost: number; bottleCost: number; totalBomCost: number; profit: number; marginPercent: number; }> = []; for (let idx = 0; idx < carProducts.length; idx++) { const product = carProducts[idx]; const scentName = product.name.replace(/Car Diffuser/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}/${carProducts.length}] No fragrance found for '${scentName}' (Product #${product.id})`); failedCount++; continue; } const variant = product.variants[0]; if (!variant) { console.error(`❌ [${idx + 1}/${carProducts.length}] Product #${product.id} has no variant`); failedCount++; continue; } // Build payload: 0.96ml fragrance + 11.04ml DPG + 1 bottle const payloadVariants = [ { variantId: variant.id, sku: variant.sku, bomItems: [ { rawMaterialId: matchedFragrance.id, quantity: 0.96 }, { rawMaterialId: dpg.id, quantity: 11.04 }, { rawMaterialId: bottle.id, 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}`); } // Calculations const fragCost = 0.96 * matchedFragrance.pricePerUnit; const dpgCost = 11.04 * dpg.pricePerUnit; const bottleCost = bottle.pricePerUnit; const totalCost = fragCost + dpgCost + bottleCost; const profit = variant.price - totalCost; const margin = variant.price > 0 ? (profit / variant.price) * 100 : 0; console.log(`✓ [${idx + 1}/${carProducts.length}] Product #${product.id} (${product.name}) -> Cost: ฿${totalCost.toFixed(2)} / Price: ฿${variant.price} (Margin: ${margin.toFixed(1)}%)`); results.push({ productId: product.id, productName: product.name, scentName, variantId: variant.id, sku: variant.sku, retailPrice: variant.price, fragranceName: matchedFragrance.name, fragrancePrice: matchedFragrance.pricePerUnit, fragranceCost: Math.round(fragCost * 100) / 100, dpgCost: Math.round(dpgCost * 100) / 100, bottleCost, totalBomCost: Math.round(totalCost * 100) / 100, profit: Math.round(profit * 100) / 100, marginPercent: Math.round(margin * 10) / 10, }); successCount++; } catch (err: any) { console.error(`❌ Failed Product #${product.id}:`, err.message); failedCount++; } await sleep(50); } console.log(`\n========================================`); console.log(`Total Car Diffuser Products : ${carProducts.length}`); console.log(`Successfully Configured : ${successCount}`); console.log(`Failed / Skipped : ${failedCount}`); console.log(`========================================\n`); // Generate CAR_DIFFUSER_BOM_LEDGER.md let ledger = `# รายงานการกำหนดสูตร BOM สำหรับกลุ่มสินค้า Car Diffuser (Production)\n\n`; ledger += `* **วันที่กำหนดสูตร:** ${new Date().toISOString()}\n`; ledger += `* **ระบบอ้างอิง:** Production Database (\`${PROD_API}\`)\n`; ledger += `* **สเปคขวด:** ป้ายระบุ 25ml / ปริมาตรบรรจุจริง 12ml\n`; ledger += `* **สัดส่วนสูตร (12ml):** **หัวน้ำหอม 8% (0.96 ml) + DPG 92% (11.04 ml) + ขวดพร้อมฝาไม้ (฿18.00)**\n`; ledger += `* **จำนวนสินค้าทั้งหมด:** **${carProducts.length}** กลิ่น (**${successCount}** Variants)\n`; ledger += `* **สถานะความสำเร็จ:** **100% (${successCount}/${carProducts.length} สินค้า)**\n\n`; ledger += `## 📋 ตารางรายละเอียดต้นทุน BOM และอัตรากำไร (Margin) ทั้ง 45 กลิ่น\n\n`; ledger += `| # | ID | Master SKU | ชื่อสินค้า | หัวน้ำหอม (ราคา/ml) | ต้นทุนน้ำยา (12ml) | ต้นทุนขวด | ต้นทุนรวม BOM | ราคาขาย Master | กำไรขั้นต้น (GP) | GP Margin % |\n`; ledger += `| :-: | :-: | :--- | :--- | :--- | :-: | :-: | :-: | :-: | :-: | :-: |\n`; results.forEach((r, idx) => { const liquidCost = (r.fragranceCost + r.dpgCost).toFixed(2); ledger += `| ${idx + 1} | ${r.productId} | \`${r.sku}\` | **${r.productName}** | ${r.fragranceName} (฿${r.fragrancePrice.toFixed(2)}) | ฿${liquidCost} | ฿${r.bottleCost.toFixed(2)} | **฿${r.totalBomCost.toFixed(2)}** | ฿${r.retailPrice.toFixed(2)} | +฿${r.profit.toFixed(2)} | **${r.marginPercent}%** |\n`; }); const ledgerPath = path.resolve(process.cwd(), "CAR_DIFFUSER_BOM_LEDGER.md"); fs.writeFileSync(ledgerPath, ledger, "utf8"); console.log(`Saved ledger report to: ${ledgerPath}`); } main().catch(console.error);