-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathModels.js
More file actions
254 lines (222 loc) · 11.4 KB
/
Copy pathModels.js
File metadata and controls
254 lines (222 loc) · 11.4 KB
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/**
* Models.gs
* Defines reusable data models for the Order System
*/
/**
* Product Model Factory
* Creates a standardized Product object adhering to the Variable Product Model.
* Handles metadata inheritance (Parent -> Child) and Attribute vs Value logic.
*
* @param {Object} rawData - The raw values from the sheet row
* @param {Object|null} parentModel - The parent product model if this is a child
* @param {number} rowIndex - The 0-based index of the row in the sheet data
* @returns {Object} A standardized product object
*/
function createProductModel(rawData, parentModel, rowIndex) {
const isParent = String(rawData.node || "").toLowerCase() === 'parent';
const sheetRow = rowIndex + 2;
// Helper to parse numeric values safely
const parseNumber = (val, def = 0) => {
if (typeof val === 'number') return val;
let s = String(val || "").replace(/[^0-9.]/g, '');
let n = parseFloat(s);
return isNaN(n) ? def : n;
};
// THE CORE MODEL
const product = {
// Identification
isParent: isParent,
id: sheetRow,
groupId: isParent ? sheetRow : (parentModel ? parentModel.id : sheetRow),
// Structural Data (Base Info)
sku: rawData.sku || "",
ref: rawData.ref || "",
name: rawData.name || (parentModel ? parentModel.name : ""),
category: rawData.category || (parentModel ? parentModel.category : "Uncategorized"),
brand: rawData.brand || (parentModel ? parentModel.brand : ""),
// Inventory / Availability
// "0" disables product. Blank or anything else is available.
inventory: rawData.inventory,
isAvailable: String(rawData.inventory).trim() !== "0",
// Attributes (Definitions on Parent, Values on Child)
// Parent row variation columns store the "Attribute Name" (e.g., "Flavor")
// Child row variation columns store the "Selection" (e.g., "Blueberry")
variation: rawData.variation || "",
variation2: rawData.variation2 || "",
variation3: rawData.variation3 || "",
variation4: rawData.variation4 || "",
// Inherited Attribute Labels (Explicitly for UI Headers)
headerVariation: parentModel ? parentModel.variation : (isParent ? (rawData.variation || "Flavor") : "Flavor"),
headerVariation2: parentModel ? parentModel.variation2 : (isParent ? (rawData.variation2 || "Strength") : "Strength"),
headerVariation3: parentModel ? parentModel.variation3 : (isParent ? (rawData.variation3 || "Format") : "Format"),
headerVariation4: parentModel ? parentModel.variation4 : (isParent ? (rawData.variation4 || "Units") : "Units"),
// Pricing & Logistics
// If child price is 0 or empty, try inheriting from parent
price: parseNumber(rawData.price || (parentModel ? parentModel.price : 0), 0),
salePrice: parseNumber(rawData.salePrice || (parentModel ? parentModel.salePrice : 0), 0),
onSale: (function () {
const isTrue = (val) => {
if (val === true || val === 1 || val === '1') return true;
const s = String(val || "").trim().toLowerCase();
return s === "true" || s === "yes" || s === "x" || s === "on";
};
// STRICT RULE: If this is a child/variation, it ONLY respects the Parent's checkbox.
// If it's a standalone product or the Parent itself, it respects its own checkbox.
if (parentModel) {
// Return child's own value if explicitly set (truthy), otherwise inherit from parent
const childSale = isTrue(rawData.onSale);
return childSale || !!parentModel.onSale;
}
return isTrue(rawData.onSale);
})(),
// FORMAT vs UNITS LOGIC
// variation3 (Format) defines the physical product tier (Single vs Case/Carton)
// variation4 (Units) is the commission/piece multiplier
hasCase: (function () {
const v1 = String(rawData.variation || "").toLowerCase();
const v2 = String(rawData.variation2 || "").toLowerCase();
const v3 = String(rawData.variation3 || "").toLowerCase();
const v4 = String(rawData.variation4 || "").toLowerCase();
const units = parseInt(rawData.unitsPerCase) || 1;
// Directive v1.8.19: Expanded bulk detection (pk, ct, box, box, numbers > 1)
// FIXED v0.8.76: Added word boundaries \b to prevent partial matches (e.g. "Perfect" triggering "ct")
const caseRegex = /\b(case|carton|box|multi|pack|bulk|master|pk|ct|disp)\b/i;
const isNumericCase = (s) => {
const n = parseInt(s);
return !isNaN(n) && n > 1 && !s.includes("mg") && !s.includes("ml") && !s.includes("g");
};
return caseRegex.test(v1) || caseRegex.test(v2) || caseRegex.test(v3) || caseRegex.test(v4) ||
isNumericCase(v1) || isNumericCase(v2) || isNumericCase(v3) || isNumericCase(v4) ||
units > 1;
})(),
caseUnits: (parseInt(rawData.unitsPerCase) || 1),
unitsMultiplier: parseNumber(rawData.variation4 || 1, 1), // Used for Commission and Piece counts
unitsPerCase: (parseInt(rawData.unitsPerCase) || (parentModel ? parentModel.unitsPerCase : 1)),
// Commission Data (Inherit from Parent if child value is empty/zero, with bulletproof defaults)
commissionRate: (function () {
const childVal = parseNumber(rawData.commissionRate);
if (childVal > 0) return childVal;
if (parentModel && parentModel.commissionRate > 0) return parentModel.commissionRate;
return 1.5; // Ultimate fallback: $1.50
})(),
saleCommission: (function () {
const childVal = parseNumber(rawData.saleCommission);
if (childVal > 0) return childVal;
if (parentModel && parentModel.saleCommission > 0) return parentModel.saleCommission;
return 1.0; // Ultimate fallback: $1.00
})(),
// Content
description: rawData.description || (parentModel ? parentModel.description : ""),
image: rawData.image || (parentModel ? parentModel.image : ""),
// Styling & Branding (Directives v1.8.17: Comprehensive White/Zero-Width Filtering)
backgroundColor: (function () {
const raw = String(rawData.backgroundColor || "").trim().toLowerCase();
const isWhite = (raw === "#ffffff" || raw === "#fff" || raw === "white" || raw === "transparent" || !raw);
return isWhite ? "" : raw;
})(),
textColor: String(rawData.textColor || "").trim(),
// Grouping overrides: Always prefer Parent for Group Identity
groupName: (function () {
const pName = parentModel ? String(parentModel.name || "").trim() : "";
if (pName) return pName;
const rName = String(rawData.name || "").trim();
if (isParent) return rName || "Unnamed Group";
return rName || String(rawData.sku || "").trim() || "Unnamed Product";
})(),
groupColor: (function () {
const pBg = String(parentModel ? parentModel.backgroundColor : "").trim().toLowerCase();
const isPWhite = (pBg === "#ffffff" || pBg === "#fff" || pBg === "white" || !pBg);
if (!isPWhite) return parentModel.backgroundColor;
const rBg = String(rawData.backgroundColor || "").trim().toLowerCase();
const isRWhite = (rBg === "#ffffff" || rBg === "#fff" || rBg === "white" || !rBg);
if (isParent && !isRWhite) return rawData.backgroundColor;
return "";
})(),
groupTextColor: (parentModel && parentModel.textColor) ? parentModel.textColor : (rawData.textColor || ""),
// PDF & Export Ranges
pdfRangeName: rawData.pdfRangeName || (parentModel ? parentModel.pdfRangeName : ""),
singleRangeName: (rawData.pdfRangeName || (parentModel ? parentModel.pdfRangeName : (rawData.sku || "").split('-')[0])) + "_SINGLE",
multiRangeName: (rawData.pdfRangeName || (parentModel ? parentModel.pdfRangeName : (rawData.sku || "").split('-')[0])) + "_MULTI",
zoneVariation: rawData.zoneVariation || (parentModel ? parentModel.zoneVariation : ""),
orderQty: rawData.orderQty || 0,
// Order Form Routing — which ORDER_FORM_N sheet this product belongs to
// Inherit from parent if blank on child row. Defaults to "1" (ORDER_FORM_1).
orderFormNumber: (function () {
const raw = String(rawData.orderFormNumber || "").trim();
if (raw) return raw;
if (parentModel && parentModel.orderFormNumber) return parentModel.orderFormNumber;
return "1"; // default to ORDER_FORM_1
})(),
// Metadata
timestamp: new Date().getTime(),
version: "1.7.03"
};
return product;
}
/**
* Order Model Factory
* Standardizes Order data for fulfillment and regulatory tracking.
* Based on the WooCommerce Order structure.
*
* @param {Object} rawData - The raw order payload (from form or spreadsheet)
* @returns {Object} A standardized order object
*/
function createOrderModel(rawData) {
const timestampDate = new Date();
const formattedDate = Utilities.formatDate(timestampDate, Session.getScriptTimeZone(), "yyyy-MM-dd'T'HH:mm:ss");
const model = {
// Identification
id: rawData.id || ("ORD-" + timestampDate.getTime()),
number: rawData.number || String(rawData.id || timestampDate.getTime()),
status: rawData.status || "pending",
// Timestamps
date_created: rawData.date_created || formattedDate,
date_modified: rawData.date_modified || formattedDate,
// Client/Customer Information
customer_id: rawData.clientId || rawData.customer_id || 0,
// Billing & Shipping (Regulatory & Delivery)
billing: rawData.billing || {
first_name: rawData.clientName || "",
last_name: "",
address_1: rawData.clientAddress || "",
city: "",
state: "",
postcode: "",
country: "US",
email: "",
phone: ""
},
shipping: rawData.shipping || {
first_name: rawData.clientName || "",
last_name: "",
address_1: rawData.clientAddress || "",
city: "",
state: "",
postcode: "",
country: "US"
},
// Itemized List (The core order data)
line_items: (rawData.items || rawData.line_items || []).map(item => {
return {
id: item.id || 0,
sku: item.sku || "",
name: item.name || "",
product_id: item.product_id || 0,
variation_id: item.variation_id || 0,
quantity: parseInt(item.quantity) || 0,
subtotal: String(item.subtotal || "0.00"),
total: String(item.total || "0.00")
};
}),
// Fulfillment Extras
shipping_lines: rawData.shipping_lines || [],
coupon_lines: rawData.coupon_lines || [],
// Financial Summaries (Regulatory totals)
discount_total: String(rawData.discount_total || "0.00"),
shipping_total: String(rawData.shipping_total || "0.00"),
total: String(rawData.total || "0.00"),
// Custom Extension Point
meta_data: rawData.meta_data || []
};
return model;
}