Spaces:
Sleeping
Sleeping
| /** engine/calculation.js - V10.6 Final Fix */ | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { calculateImposition } = require('./optimizer'); | |
| let materials = {}, productsDB = {}, pricesDB = {}, DOMAIN_TEMPLATES = { products: {} }; | |
| try { | |
| materials = JSON.parse(fs.readFileSync(path.join(__dirname, '../db/materials.json'), 'utf8')); | |
| productsDB = JSON.parse(fs.readFileSync(path.join(__dirname, '../db/products.json'), 'utf8')); | |
| pricesDB = JSON.parse(fs.readFileSync(path.join(__dirname, '../db/prices.json'), 'utf8')); | |
| DOMAIN_TEMPLATES = JSON.parse(fs.readFileSync(path.join(__dirname, '../db/domainTemplates.json'), 'utf8')); | |
| } catch (e) { | |
| console.error("Failed loading DB files in calculation.js", e); | |
| } | |
| // Fallbacks if prices.json is missing or corrupted | |
| const PRICES = pricesDB.digital_base ? pricesDB : { | |
| digital_base: { cost_per_click_color: 0.35, setup_fee: 20, min_price: 50 }, | |
| wide_base: { cost_per_sqm: 50, min_price: 100 }, | |
| margins: { digital_multiplier: 2.5, wide_multiplier: 3.0, waste_factor: 0.05, min_waste_sheets: 10, profit_warning_threshold: 0.30 } | |
| }; | |
| function parseSize(sizeStr) { | |
| if (!sizeStr) return { w: 210, h: 297 }; | |
| const sizes = { 'A4': { w: 210, h: 297 }, 'A5': { w: 148, h: 210 }, 'bc': { w: 90, h: 50 }, '13x18': { w: 130, h: 180 } }; | |
| if (sizes[sizeStr]) return sizes[sizeStr]; | |
| if (sizeStr.includes('x')) { | |
| const parts = sizeStr.split('x'); | |
| return { w: parseFloat(parts[0]) * 10, h: parseFloat(parts[1]) * 10 }; | |
| } | |
| return sizes['A4']; | |
| } | |
| function calculate_custom_job(cart, params) { | |
| const productKey = params.product || 'flyer'; | |
| const productConfig = productsDB[productKey] || {}; | |
| // --- FORCE FIX: ืืืืื ืืืืื ืฉื ืคืืจืื ืจืื --- | |
| const WIDE_PRODUCTS = ['rollup', 'canvas', 'sticker', 'banner', 'sign', 'shimson']; | |
| let engineType = productConfig.engine; | |
| if (WIDE_PRODUCTS.includes(productKey)) { | |
| engineType = 'wide'; | |
| } | |
| if (!engineType) engineType = 'digital'; | |
| // ------------------------------------------- | |
| if (engineType === 'wide') { | |
| return calculateWideFormat(cart, params, productKey); | |
| } else { | |
| return calculateDigital(cart, params, productKey); | |
| } | |
| } | |
| function calculateDigital(cart, params, productKey) { | |
| console.log(`\n๐งฎ [CALCULATION] Starting Digital Calc for: ${productKey}`); | |
| const qty = parseInt(params.qty) || 100; | |
| const sizeObj = parseSize(params.size); | |
| const paperKey = params.paper_type || 'offset_80'; | |
| let safePaperKey = paperKey; | |
| if (productKey === 'bc' && paperKey.includes('matte')) safePaperKey = 'matte_350'; | |
| const paperData = materials.papers[safePaperKey] || materials.papers['offset_80']; | |
| const impResult = calculateImposition(sizeObj.w, sizeObj.h); | |
| console.log(`๐ [CALCULATION] Size: ${sizeObj.w}x${sizeObj.h}mm, Paper: ${safePaperKey} (Cost/Sheet: โช${paperData.cost_sheet})`); | |
| console.log(`โ๏ธ [CALCULATION] Imposition: ${impResult.ups} ups (Yield: ${impResult.efficiency})`); | |
| if (impResult.ups === 0) throw new Error("ืืืฆืจ ืืืื ืืื ืืืืื ื"); | |
| const pages = parseInt(params.pages) || 1; | |
| const clicksPerPage = 2; // Duplex assumption | |
| const rawSheets = Math.ceil((qty * (pages / 2)) / impResult.ups); | |
| const wasteSheets = Math.max(PRICES.margins.min_waste_sheets, Math.ceil(rawSheets * PRICES.margins.waste_factor)); | |
| const totalSheets = rawSheets + wasteSheets; | |
| // Core Costs | |
| const costPaper = totalSheets * paperData.cost_sheet; | |
| const costClicks = (totalSheets * 2) * PRICES.digital_base.cost_per_click_color; | |
| const totalCost = costPaper + costClicks + PRICES.digital_base.setup_fee; | |
| console.log(`๐ต [CALCULATION] Sheets Needed: ${totalSheets} (Includes waste). Raw Paper Cost: โช${costPaper.toFixed(2)}`); | |
| console.log(`๐ต [CALCULATION] Total Production Cost (w/ clicks & setup): โช${totalCost.toFixed(2)}`); | |
| const finalPrice = Math.max(PRICES.digital_base.min_price, Math.ceil(totalCost * PRICES.margins.digital_multiplier)); | |
| console.log(`๐ [CALCULATION] Final Client Price (x${PRICES.margins.digital_multiplier} Markup): โช${finalPrice}\n`); | |
| return buildResult(cart, productKey, params, finalPrice, qty, `${qty} ืื', ${paperData.name}`, totalCost); | |
| } | |
| function calculateWideFormat(cart, params, productKey) { | |
| console.log(`\n๐งฎ [CALCULATION] Starting Wide Format Calc for: ${productKey}`); | |
| const qty = parseInt(params.qty) || 1; | |
| let totalSqm = 0; | |
| if (productKey === 'sticker' && !params.size) { | |
| totalSqm = qty; | |
| } else { | |
| const sizeObj = parseSize(params.size); | |
| totalSqm = (sizeObj.w * sizeObj.h / 1000000) * qty; | |
| } | |
| let costPerSqm = PRICES.wide_base.cost_per_sqm; | |
| if (productKey === 'alucobond') { | |
| costPerSqm = PRICES.wide_base.cost_per_sqm_alucobond; | |
| } | |
| let totalCost = (totalSqm * costPerSqm) + (PRICES.wide_base.setup_fee || 0); | |
| // ืืืืืจ ืืืงืื ืืืืกืก ืชืืื ืขื ืืื ืืืื ืื ืขืืืช ืืืืจ ืืืืคืืช (ืื ืืืื ืืืกื ืฉื ืกืืจืืค ืขืืืช ืืืงืื, ืืืจืช ืืฉืื ืืงืื ืืืคืื ืืืืืช ืฉืงืืื) | |
| let finalPrice = Math.max(PRICES.wide_base.min_price, Math.ceil((totalSqm * costPerSqm) * PRICES.margins.wide_multiplier)); | |
| console.log(`๐ [CALCULATION] Total SQM: ${totalSqm.toFixed(2)}. Cost/SQM: โช${costPerSqm}`); | |
| console.log(`๐ [CALCULATION] Final Client Price (x${PRICES.margins.wide_multiplier} Markup): โช${finalPrice}\n`); | |
| return buildResult(cart, productKey, params, finalPrice, qty, `${qty} ืื' ืคืืจืื ืจืื`, totalCost); | |
| } | |
| const { assembleProductionItem } = require('./integrity'); | |
| function buildResult(cart, product, params, price, qty, desc, cost) { | |
| // 1. Initial Calc Object (Pre-Hardening) | |
| const productMeta = DOMAIN_TEMPLATES.products[product] || {}; | |
| const calcResult = { | |
| product, | |
| displayName: productMeta.label || product, | |
| client_price: price, | |
| unit_price: (price / qty).toFixed(2), | |
| production_cost: cost?.toFixed(2), | |
| description: desc, | |
| qty: qty, // Spec v5.7.4 Qty mapping | |
| engine: params.engine || 'digital' // Pass through engine type | |
| }; | |
| // 2. DOMAIN PIPELINE PHASE 3: Assemble Production Item (IDs, Hashing, Normalization, Freezing) | |
| const hardenedItem = assembleProductionItem(calcResult, params); | |
| // 3. Margin Analysis (Audit Log only, logic moved to hardenedItem check if needed) | |
| // We still keep the console logs for debugging as per previous work | |
| let totalCartPrice = 0; | |
| let totalCartCost = 0; | |
| const virtualCart = [...(cart || []), hardenedItem]; | |
| for (const c of virtualCart) { | |
| // Handle both production items and legacy items if any | |
| totalCartPrice += parseFloat(c.pricing_snapshot?.client_price || c.client_price || 0); | |
| totalCartCost += parseFloat(c.pricing_snapshot?.production_cost || c.production_cost || 0); | |
| } | |
| if (totalCartPrice > 0) { | |
| const cartMargin = (totalCartPrice - totalCartCost) / totalCartPrice; | |
| if (cartMargin < PRICES.margins.profit_warning_threshold) { | |
| console.log(`\x1b[41m\x1b[37m ๐จ TOTAL CART MARGIN WARN \x1b[0m Overall Cart Profit Margin is ${Math.round(cartMargin * 100)}%`); | |
| } else { | |
| console.log(`\x1b[32mโ TOTAL CART MARGIN SAFE \x1b[0m Overall Cart Profit Margin is ${Math.round(cartMargin * 100)}%`); | |
| } | |
| } | |
| // Return the single item. The Controller will handle pushToSessionCart() to enforce uniqueness. | |
| return hardenedItem; | |
| } | |
| module.exports = { calculate_custom_job }; |