pini-print-bot / engine /calculation.js
dotandru's picture
feat(v5.7.4): Semantic Closure Gate, Delete unused files, PDF Fixes
d897af7
Raw
History Blame Contribute Delete
7.7 kB
/** 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 };