158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
const PROD_API = "https://api.homaroma.shop/api";
|
|
|
|
interface RawMaterialRef {
|
|
id: number;
|
|
name: string;
|
|
pricePerUnit: number;
|
|
unit: string;
|
|
}
|
|
|
|
interface VariantPlan {
|
|
id: number;
|
|
sku: string;
|
|
option: string;
|
|
price: number;
|
|
hasBom: boolean;
|
|
totalBomCost: number;
|
|
}
|
|
|
|
interface ProductPlan {
|
|
index: number;
|
|
productId: number;
|
|
productName: string;
|
|
scentName: string;
|
|
category: string | null;
|
|
bomStatus: string;
|
|
configuredVariants: number;
|
|
totalVariants: number;
|
|
variants: VariantPlan[];
|
|
matchedFragrance: RawMaterialRef | null;
|
|
packagingReady: boolean;
|
|
}
|
|
|
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
const BOTTLE_MAP: Record<number, number> = {
|
|
30: 10, // ขวดพร้อมฝา Oilozone 30ml
|
|
60: 11, // ขวดพร้อมฝา Oilozone 60ml
|
|
85: 12, // ขวดพร้อมฝา Oilozone 85ml
|
|
120: 9, // ขวดพร้อมฝา Oilozone 120ml
|
|
};
|
|
|
|
const FORMULA_MAP: Record<number, { fragQty: number; dpgQty: number }> = {
|
|
30: { fragQty: 2.4, dpgQty: 27.6 },
|
|
60: { fragQty: 4.8, dpgQty: 55.2 },
|
|
85: { fragQty: 6.8, dpgQty: 78.2 },
|
|
120: { fragQty: 9.6, dpgQty: 110.4 },
|
|
};
|
|
|
|
function detectSize(variant: VariantPlan): number {
|
|
const opt = (variant.option || "").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.option})`);
|
|
}
|
|
|
|
async function main() {
|
|
const jsonPath = path.resolve(process.cwd(), "oil_ozone_products.json");
|
|
if (!fs.existsSync(jsonPath)) {
|
|
throw new Error(`File not found: ${jsonPath}`);
|
|
}
|
|
|
|
const products: ProductPlan[] = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
console.log(`Loaded ${products.length} products from ${jsonPath}`);
|
|
|
|
let successCount = 0;
|
|
let skippedCount = 0;
|
|
let failedCount = 0;
|
|
|
|
for (const product of products) {
|
|
console.log(`\n--------------------------------------------------`);
|
|
console.log(`[${product.index}/${products.length}] Product #${product.productId}: ${product.productName}`);
|
|
|
|
if (!product.matchedFragrance) {
|
|
console.error(`❌ Skipped: No matching fragrance found for scent '${product.scentName}'`);
|
|
failedCount++;
|
|
continue;
|
|
}
|
|
|
|
if (product.bomStatus === "CONFIGURED" && product.configuredVariants === product.totalVariants) {
|
|
console.log(`⚡ Already configured (${product.configuredVariants}/${product.totalVariants} variants). Skipping.`);
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
|
|
// Build variants payload
|
|
const variantsPayload = 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: product.matchedFragrance!.id,
|
|
name: product.matchedFragrance!.name,
|
|
quantity: formula.fragQty,
|
|
},
|
|
{
|
|
rawMaterialId: 2, // DPG
|
|
name: "DPG",
|
|
quantity: formula.dpgQty,
|
|
},
|
|
{
|
|
rawMaterialId: bottleId,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
};
|
|
});
|
|
|
|
console.log(`Applying BOM for ${variantsPayload.length} variants with fragrance [ID ${product.matchedFragrance.id}] ${product.matchedFragrance.name}...`);
|
|
|
|
try {
|
|
const res = await fetch(`${PROD_API}/products/${product.productId}/bom`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ variants: variantsPayload }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errText = await res.text();
|
|
throw new Error(`HTTP ${res.status}: ${errText}`);
|
|
}
|
|
|
|
const resData = (await res.json()) as any;
|
|
console.log(`✓ BOM saved successfully for #${product.productId}! Updated variants: ${resData.updatedVariants ?? variantsPayload.length}`);
|
|
successCount++;
|
|
} catch (err: any) {
|
|
console.error(`❌ Failed to update BOM for #${product.productId}: ${err.message}`);
|
|
failedCount++;
|
|
}
|
|
|
|
// Rate-limit pause
|
|
await sleep(200);
|
|
}
|
|
|
|
console.log(`\n==================================================`);
|
|
console.log(`BOM Application Summary:`);
|
|
console.log(` - Newly configured: ${successCount}`);
|
|
console.log(` - Already configured (skipped): ${skippedCount}`);
|
|
console.log(` - Failed: ${failedCount}`);
|
|
console.log(`==================================================\n`);
|
|
}
|
|
|
|
main().catch(console.error);
|