File size: 7,695 Bytes
469b970
f860279
 
469b970
f860279
492565a
f860279
 
469b970
6a25ae8
492565a
6a25ae8
 
 
 
 
 
 
 
 
 
f860279
469b970
 
 
 
 
 
 
f860279
469b970
f860279
 
469b970
 
 
e0ed47d
469b970
 
 
e0ed47d
469b970
 
f860279
e0ed47d
469b970
 
f860279
469b970
 
f860279
469b970
f860279
 
 
469b970
e0ed47d
469b970
 
 
e0ed47d
469b970
 
e0ed47d
469b970
 
e0ed47d
 
 
 
469b970
 
a757299
 
 
 
6a25ae8
 
 
 
469b970
a757299
6a25ae8
e0ed47d
 
 
 
6a25ae8
 
469b970
21ad827
f860279
 
469b970
e0ed47d
469b970
 
e0ed47d
469b970
e0ed47d
469b970
 
 
f860279
e0ed47d
6a25ae8
7e5f69c
 
 
 
3e6bc31
 
 
469b970
e0ed47d
6a25ae8
 
21ad827
 
 
1e40870
21ad827
1e40870
 
492565a
1e40870
 
492565a
1e40870
 
 
 
d897af7
1e40870
 
 
 
 
 
 
 
21ad827
 
1e40870
21ad827
1e40870
 
 
 
6a25ae8
e0ed47d
21ad827
 
 
1e40870
21ad827
 
 
 
f860279
1e40870
 
f860279
 
469b970
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/** 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 };