1409 lines
40 KiB
JavaScript
1409 lines
40 KiB
JavaScript
// ===================================================================
|
|
// CONSTANTS
|
|
// ===================================================================
|
|
const MAXIMUM_SLOTS = 20;
|
|
const MAXIMUM_CRON_JOB = 10;
|
|
|
|
// ===================================================================
|
|
// GENERAL
|
|
// ===================================================================
|
|
|
|
/**
|
|
*
|
|
* Extract parameter mapping from string
|
|
*
|
|
* @param {String} pstr parameters in string expect format like `var1=1,var2=2,debug`
|
|
* @returns Map
|
|
*/
|
|
function getParamsMapFromString(pstr) {
|
|
var pmap = new Map();
|
|
let psplt = pstr.split(",");
|
|
for (ps of psplt) {
|
|
let comp = ps.split("=");
|
|
if (comp.length > 1) {
|
|
pmap.set(comp[0], comp[1]);
|
|
} else if (pmap.has("others")) {
|
|
pmap["others"].append(comp);
|
|
} else {
|
|
pmap["others"] = [comp];
|
|
}
|
|
}
|
|
return pmap;
|
|
}
|
|
|
|
// ===================================================================
|
|
// PROFILE PRICE
|
|
// ===================================================================
|
|
|
|
class RecipePriceProfile {
|
|
constructor(productCode, name, nameTH, price) {
|
|
this.productCode = productCode;
|
|
this.name = name;
|
|
this.nameTH = nameTH;
|
|
this.price = price;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Function} fn callback function for price
|
|
* @returns int
|
|
*/
|
|
test(fn) {
|
|
return fn(this.price);
|
|
}
|
|
|
|
/**
|
|
* Fill empty into array so that its size is matched the expected length
|
|
* @param {Array} arr incompleted array
|
|
* @param {int} expect_len expected length of array
|
|
* @returns Array
|
|
*/
|
|
fill_until_length(arr, expect_len) {
|
|
// Log.debug(`arr dbg: ${JSON.stringify(arr)}`);
|
|
if (arr.length < expect_len) {
|
|
for (let m = arr.length; m < expect_len; m++) {
|
|
arr.push(" ");
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
/**
|
|
* Build data into expected format
|
|
* @param {int} index index of callback, this does modify the price. If expected index did not exist, the price is not modified.
|
|
* @param {Function} then_fn modify function to price after created template.
|
|
* @returns Array
|
|
*/
|
|
build_slot(index, then_fn) {
|
|
let debug = index == 1 && this.productCode.includes("12-02-03-0039");
|
|
let ret = [
|
|
this.productCode,
|
|
this.name,
|
|
this.nameTH,
|
|
" ",
|
|
this.test(getSlotFunctionByIndex(index)),
|
|
];
|
|
if (debug) Log.debug(`build_slot${index}: ${JSON.stringify(ret)}`);
|
|
if (then_fn != undefined) {
|
|
ret = then_fn(ret);
|
|
if (debug) Log.debug(`build_slot${index}.then: ${JSON.stringify(ret)}`);
|
|
}
|
|
|
|
ret = this.fill_until_length(ret, 12);
|
|
if (debug) Log.debug(`build_slot${index}.fill: ${JSON.stringify(ret)}`);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Describe the values mapping
|
|
* @returns Object
|
|
*/
|
|
describe() {
|
|
return {
|
|
productCode: this.productCode,
|
|
name: this.name,
|
|
nameTH: this.nameTH,
|
|
price: this.price,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate array of price slot sheets up to `MAXIMUM_SLOTS`
|
|
* @returns Array
|
|
*/
|
|
function getPriceSlotNames() {
|
|
let prefix_slot = "PriceSlot";
|
|
let price_names = [];
|
|
for (let i = 1; i <= MAXIMUM_SLOTS; i++) {
|
|
let curr_name = prefix_slot + i.toString();
|
|
price_names.push(curr_name);
|
|
}
|
|
return price_names;
|
|
}
|
|
|
|
/**
|
|
* Get price modifying functions by index.
|
|
*
|
|
* `-1` = roundup
|
|
*
|
|
* `1-20` = price slot callback if expected slot function is implemented
|
|
*
|
|
* If a function by index is not implemented or unsupported, this will return its unmodified price
|
|
*
|
|
* `Updated: 8/4/25`
|
|
*
|
|
* @param {int} index index of callback
|
|
* @returns Function(int)
|
|
*/
|
|
function getSlotFunctionByIndex(index) {
|
|
switch (index) {
|
|
case -1:
|
|
return (pack) => {
|
|
if (typeof pack[4] == "string") return pack;
|
|
pack[4] = Math.round(pack[4]);
|
|
let test10 = pack[4] % 10;
|
|
if (test10 == 5 || test10 == 0) {
|
|
return pack;
|
|
} else {
|
|
if (test10 < 5) {
|
|
let diff = 5 - test10;
|
|
pack[4] = pack[4] + diff;
|
|
return pack;
|
|
} else if (test10 > 5) {
|
|
let diff = 10 - test10;
|
|
pack[4] = pack[4] + diff;
|
|
return pack;
|
|
}
|
|
}
|
|
};
|
|
// -------------------------------------
|
|
// SLOT FUNCTIONS
|
|
// -------------------------------------
|
|
// increase 15%
|
|
case 1:
|
|
return (price) => price * 1.15;
|
|
// increase 25%
|
|
case 2:
|
|
return (price) => price * 1.25;
|
|
// increase 30%
|
|
case 3:
|
|
return (price) => price * 1.3;
|
|
// increase 35%
|
|
case 4:
|
|
return (price) => price * 1.35;
|
|
// increase 45%
|
|
case 5:
|
|
return (price) => price * 1.45;
|
|
case 6:
|
|
return (price) => price - 5;
|
|
case 8:
|
|
return (price) => "HIDE";
|
|
case 9:
|
|
return (price) => price * 0.60;
|
|
default:
|
|
return (price) => price;
|
|
}
|
|
}
|
|
|
|
// =====================================================================
|
|
// EXPORT PROFILE PRICE
|
|
// =====================================================================
|
|
|
|
const ProfilePrice = {
|
|
RecipePriceProfile: RecipePriceProfile,
|
|
getPriceSlotNames: getPriceSlotNames,
|
|
getSlotFunctionByIndex: getSlotFunctionByIndex,
|
|
// infos
|
|
version: 3,
|
|
version_name: "ObiShou-PP",
|
|
changelogs: [
|
|
"2/4/25 initialized version 2, fix timeout on trigger",
|
|
"8/4/25 initialized version 3, express server",
|
|
],
|
|
maximum_slots: MAXIMUM_SLOTS,
|
|
};
|
|
|
|
// =====================================================
|
|
// LOGGER
|
|
// =====================================================
|
|
|
|
const { createLogger, format, transports } = require("winston");
|
|
require("winston-daily-rotate-file");
|
|
|
|
const file_rotate_transport = new transports.DailyRotateFile({
|
|
filename: "log-%DATE%.log",
|
|
datePattern: "YYYY-MM-DD",
|
|
zippedArchive: true,
|
|
maxSize: "20m",
|
|
maxFiles: "14d",
|
|
dirname: "logs",
|
|
format: format.combine(
|
|
format.colorize({
|
|
all: false,
|
|
}),
|
|
format.timestamp({ format: "DD-MM-YYYY HH:mm:ss" }),
|
|
format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
|
|
),
|
|
});
|
|
|
|
/**
|
|
* common logger instance
|
|
*/
|
|
const logger = createLogger({
|
|
level: "debug",
|
|
format: format.combine(
|
|
format.colorize(),
|
|
format.timestamp({ format: "DD-MM-YYYY HH:mm:ss" }),
|
|
format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),
|
|
),
|
|
transports: [file_rotate_transport, new transports.Console()],
|
|
});
|
|
|
|
/**
|
|
* `Log` reference to created instance,
|
|
* use this for logging
|
|
*/
|
|
const Log = {
|
|
info: (msg) => logger.info(msg),
|
|
debug: (msg) => logger.debug(msg),
|
|
warn: (msg) => logger.warn(msg),
|
|
err: (msg) => logger.error(msg),
|
|
};
|
|
|
|
// ======================================================================
|
|
// CONCURRENT
|
|
// ======================================================================
|
|
|
|
async function runTasksConcurrent(concurrent_tasks, ok, error) {
|
|
return await Promise.all(concurrent_tasks)
|
|
.then(
|
|
(result) => {
|
|
return ok(result);
|
|
},
|
|
(reject) => {
|
|
return error(reject);
|
|
},
|
|
)
|
|
.catch((err) => {
|
|
return error(err);
|
|
});
|
|
}
|
|
|
|
const Concurrent = {
|
|
runTasks: runTasksConcurrent,
|
|
};
|
|
|
|
// ======================================================================
|
|
// SAVE TO FILE
|
|
// ======================================================================
|
|
const fs = require("fs");
|
|
function saveJsonToFile(filename, content) {
|
|
let formatted = JSON.stringify(content, null, 2);
|
|
fs.writeFile(filename, formatted, "utf8", (err) => {
|
|
if (err) {
|
|
Log.err(`save json error: ${err.stack}`);
|
|
} else {
|
|
Log.info(`save ${filename}:${formatted.length}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ======================================================================
|
|
// GOOGLE
|
|
// ======================================================================
|
|
|
|
const { google, sheets_v4 } = require("googleapis");
|
|
|
|
function authorize() {
|
|
const oauthClient = new google.auth.GoogleAuth({
|
|
credentials: {
|
|
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
|
|
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/gm, "\n"),
|
|
},
|
|
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
|
|
});
|
|
return oauthClient;
|
|
}
|
|
|
|
function getCountrySpreadSheetId(cnt) {
|
|
switch (cnt) {
|
|
case "tha":
|
|
return process.env.TB_THA_SHEET;
|
|
case "mys":
|
|
return process.env.TB_MYS_SHEET;
|
|
case "sgp":
|
|
return process.env.TB_SGP_SHEET;
|
|
case "uae":
|
|
return process.env.TB_UAE_SHEET;
|
|
|
|
default:
|
|
return process.env.TEST_SHEET_ID;
|
|
}
|
|
}
|
|
|
|
const GoogleFunctions = {
|
|
auth: authorize,
|
|
SpreadSheets: (auth) => google.sheets({ version: "v4", auth }),
|
|
GetCountrySpreadSheet: getCountrySpreadSheetById,
|
|
getAllSheetNamesByCountry: getAllSheetNamesByCountry,
|
|
getCountrySheetByName: getCountrySheetByName,
|
|
getPriceSlotValues: getPriceSlotValues,
|
|
syncProfilePrice: syncProfilePrice,
|
|
};
|
|
|
|
async function getTestSpreadSheet(sheet) {
|
|
const test = await sheet.spreadsheets.get({
|
|
spreadsheetId: process.env.TEST_SHEET_ID,
|
|
});
|
|
|
|
Log.debug(`test spreadsheet: ${test.status}`);
|
|
}
|
|
|
|
async function getCountrySpreadSheetById(sheet, country, callback) {
|
|
const result = await sheet.spreadsheets.get({
|
|
spreadsheetId: getCountrySpreadSheetId(country),
|
|
});
|
|
Log.debug(`try get ${country} --> ${result.status}`);
|
|
if (callback != undefined) {
|
|
return await callback(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function getAllSheetNamesByCountry(sheet, country_short, callback) {
|
|
const sheets_mapping = await GoogleFunctions.GetCountrySpreadSheet(
|
|
sheet,
|
|
country_short,
|
|
(sheetDetail) => callback(sheetDetail),
|
|
).then((ok) => {
|
|
Log.debug(`ok! ${JSON.stringify(ok)}`);
|
|
return ok;
|
|
});
|
|
|
|
return sheets_mapping;
|
|
}
|
|
|
|
async function getCountrySheetByName(
|
|
sheet,
|
|
country_short,
|
|
sheet_name,
|
|
callback,
|
|
) {
|
|
const values = await sheet.spreadsheets.values
|
|
.get({
|
|
spreadsheetId: getCountrySpreadSheetId(country_short),
|
|
range: sheet_name,
|
|
})
|
|
.then((res) => {
|
|
Log.debug(`length of each line: ${res.data.values[0].length}`);
|
|
return res.data.values;
|
|
});
|
|
if (callback != undefined) {
|
|
return callback(values);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
async function getPriceSlotValues(sheet, country_short) {
|
|
// Warning: large operation
|
|
let expected_sheets = getPriceSlotNames();
|
|
let result = {};
|
|
|
|
// add source
|
|
let price_sheet_value = await getCountrySheetByName(
|
|
sheet,
|
|
country_short,
|
|
"price",
|
|
);
|
|
|
|
let price_aot_sheet_value = await getCountrySheetByName(
|
|
sheet,
|
|
country_short,
|
|
"TaobinAOTPrice2",
|
|
);
|
|
|
|
if (price_sheet_value != undefined) {
|
|
result["price"] = price_sheet_value;
|
|
}
|
|
if (price_aot_sheet_value != undefined) {
|
|
result["TaobinAOTPrice2"] = price_aot_sheet_value;
|
|
}
|
|
|
|
for (let _sheet of expected_sheets) {
|
|
try {
|
|
let sheet_value = await getCountrySheetByName(
|
|
sheet,
|
|
country_short,
|
|
_sheet,
|
|
);
|
|
if (sheet_value != undefined) {
|
|
result[_sheet] = sheet_value;
|
|
Log.debug(`${_sheet} ok!`);
|
|
}
|
|
} catch (err) {
|
|
Log.err(`error while fetching price slots: ${err}`);
|
|
}
|
|
}
|
|
|
|
Log.debug(`price slots: ${Object.keys(result)}`);
|
|
return result;
|
|
}
|
|
|
|
// REFERENCE
|
|
//
|
|
// const {google} = require('googleapis');
|
|
// const sheets = google.sheets({version: 'v4'});
|
|
// const auth = new google.auth.JWT (credentials.client_email, null, credentials.private_key, ['https://www.googleapis.com/auth/spreadsheets']);
|
|
|
|
// function updateGoogleSheet (spreadsheetId) {
|
|
|
|
// /* Written by Amit Agarwal */
|
|
// /* Web: ctrlq.org Email: amit@labnol.org */
|
|
|
|
// var data = [
|
|
// {
|
|
// range: "Sheet1!A1", // Update single cell
|
|
// values: [
|
|
// ["A1"]
|
|
// ]
|
|
// },
|
|
// {
|
|
// range: "Sheet1!B1:B3", // Update a column
|
|
// values: [
|
|
// ["B1"],["B2"],["B3"]
|
|
// ]
|
|
// },
|
|
// {
|
|
// range: "Sheet1!C1:E1", // Update a row
|
|
// values: [
|
|
// ["C1","D1","E1"]
|
|
// ]
|
|
// },
|
|
// {
|
|
// range: "Sheet1!F1:H2", // Update a 2d range
|
|
// values: [
|
|
// ["F1", "F2"],
|
|
// ["H1", "H2"]
|
|
// ]
|
|
// }];
|
|
|
|
// var resource = {
|
|
// spreadsheetId: spreadsheetId,
|
|
// auth: auth,
|
|
// valueInputOption: "USER_ENTERED",
|
|
// data: data
|
|
// };
|
|
|
|
// sheets.spreadsheets.values.batchUpdate (resource);
|
|
|
|
// }
|
|
|
|
function numberToColumn(n) {
|
|
let result = "";
|
|
while (n > 0) {
|
|
// Adjust by subtracting 1 to account for Excel's 1-based indexing
|
|
n--;
|
|
// Get remainder that maps directly to a letter (0 -> A, 1 -> B, ..., 25 -> Z)
|
|
let remainder = n % 26;
|
|
// Convert remainder to letter (65 is the char code for 'A')
|
|
result = String.fromCharCode(65 + remainder) + result;
|
|
// Move to the next digit in the base-26 system
|
|
n = Math.floor(n / 26);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
class PriceSlotStatus {
|
|
constructor(name, changed, added) {
|
|
this.name = name;
|
|
this.changed = changed;
|
|
this.added = added;
|
|
|
|
this.gen_changed = {};
|
|
this.gen_added = {};
|
|
}
|
|
|
|
createUpdateDataFormat(source_last_row_index) {
|
|
let data_for_update = [];
|
|
let data_for_append = [];
|
|
let change_fn = (c) => {
|
|
let range_name = `${this.name}!`;
|
|
let first = c.column[0];
|
|
let updated = [];
|
|
if (c.column.length == 1) {
|
|
range_name += `${first}${c.index}`;
|
|
updated.push(c.row[c.change_col[0]]);
|
|
} else if (c.column.length > 1) {
|
|
let last = c.column[c.column.length - 1];
|
|
range_name += `${first}${c.index}:${last}${c.index}`;
|
|
let change_idx_first = c.change_col[0];
|
|
let change_idx_last = c.change_col[c.change_col.length - 1];
|
|
for (let ci = change_idx_first; ci <= change_idx_last; ci++) {
|
|
updated.push(c.row[ci]);
|
|
}
|
|
}
|
|
|
|
return {
|
|
range: range_name,
|
|
values: [updated],
|
|
};
|
|
};
|
|
this.changed.forEach((cl) => {
|
|
let change_value = change_fn(cl);
|
|
data_for_update.push(change_value);
|
|
});
|
|
|
|
this.added.forEach((al) => data_for_append.push(al.row));
|
|
|
|
this.gen_changed = {
|
|
result: data_for_update,
|
|
};
|
|
this.gen_added = {
|
|
last_idx: source_last_row_index,
|
|
result: data_for_append,
|
|
};
|
|
|
|
// Log.debug(`gen_changed: ${JSON.stringify(this.gen_changed)}`);
|
|
// Log.debug(`gen_added: ${JSON.stringify(this.gen_added)}`);
|
|
}
|
|
}
|
|
|
|
function from_price_process_result(price_slot_map) {
|
|
return new PriceSlotStatus(
|
|
price_slot_map.name,
|
|
price_slot_map.status.changed,
|
|
price_slot_map.status.added,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find differences between two 2D arrays
|
|
* @param {Array} array1
|
|
* @param {Array} array2
|
|
* @param {Function} isCategoryRowFn
|
|
* @returns
|
|
*/
|
|
function diff2DArraysCustom(array1, array2, isCategoryRowFn) {
|
|
const differences = [];
|
|
|
|
// Use provided function or default category detection
|
|
const categoryCheck =
|
|
isCategoryRowFn ||
|
|
function (row) {
|
|
if (!row || row.length !== 2) return false;
|
|
return (!row[0] || row[0] === "") && row[1] && row[1] !== "";
|
|
};
|
|
|
|
// Helper function to find similar row in array
|
|
function findSimilarRow(targetRow, searchArray, startIndex = 0) {
|
|
for (let i = startIndex; i < searchArray.length; i++) {
|
|
if (categoryCheck(searchArray[i])) continue;
|
|
|
|
// First element must match exactly
|
|
if (targetRow[0] !== searchArray[i][0]) continue;
|
|
|
|
// Calculate similarity for remaining columns
|
|
const maxCols = Math.max(targetRow.length, searchArray[i].length);
|
|
let matches = 1; // First column already matches
|
|
|
|
for (let j = 1; j < maxCols; j++) {
|
|
if (targetRow[j] === searchArray[i][j]) {
|
|
matches++;
|
|
}
|
|
}
|
|
|
|
// Consider it similar if more than 50% of columns match (including the required first column)
|
|
if (matches / maxCols > 0.5) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Helper function to check if a row is empty
|
|
function isEmptyRow(row) {
|
|
if (!row || row.length === 0) return true;
|
|
// Check if all elements are empty, null, undefined, or whitespace
|
|
return row.every(
|
|
(cell) => !cell || (typeof cell === "string" && cell.trim() === ""),
|
|
);
|
|
}
|
|
|
|
let i = 0,
|
|
j = 0;
|
|
|
|
while (i < array1.length || j < array2.length) {
|
|
const row1 = array1[i] || null;
|
|
const row2 = array2[j] || null;
|
|
|
|
Log.debug(`Comparing rows ${i} and ${j} --- \n${row1} vs ${row2}`);
|
|
|
|
// Skip category rows
|
|
if (row1 && (categoryCheck(row1) || isEmptyRow(row1))) {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (row2 && (categoryCheck(row2) || isEmptyRow(row2))) {
|
|
j++;
|
|
continue;
|
|
}
|
|
|
|
// End of first array - remaining rows in second array are added
|
|
if (i >= array1.length) {
|
|
differences.push({
|
|
type: "added",
|
|
rowIndex: j,
|
|
originalIndex: i,
|
|
row: row2,
|
|
description: `Row added at position ${j}`,
|
|
});
|
|
j++;
|
|
continue;
|
|
}
|
|
|
|
// End of second array - remaining rows in first array are removed
|
|
if (j >= array2.length) {
|
|
// differences.push({
|
|
// type: "removed",
|
|
// rowIndex: i,
|
|
// originalIndex: i,
|
|
// row: row1,
|
|
// description: `Row removed from position ${i}`,
|
|
// });
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Compare current rows
|
|
const maxCols = Math.max(row1.length, row2.length);
|
|
const rowDifferences = [];
|
|
|
|
for (let k = 0; k < maxCols; k++) {
|
|
const val1 = row1[k];
|
|
const val2 = row2[k];
|
|
|
|
if (val1 !== val2) {
|
|
rowDifferences.push({
|
|
columnIndex: k,
|
|
oldValue: val1,
|
|
newValue: val2,
|
|
});
|
|
}
|
|
}
|
|
|
|
// If rows are identical, move both pointers
|
|
if (rowDifferences.length === 0) {
|
|
i++;
|
|
j++;
|
|
continue;
|
|
}
|
|
|
|
// Check if this might be an insert/delete scenario
|
|
// Look ahead to see if row1 appears later in array2
|
|
const row1FoundLater = findSimilarRow(row1, array2, j + 1);
|
|
// Look ahead to see if row2 appears later in array1
|
|
const row2FoundLater = findSimilarRow(row2, array1, i + 1);
|
|
|
|
if (row2FoundLater !== -1 && row1FoundLater === -1) {
|
|
// row2 is inserted - row1 appears later in array1
|
|
differences.push({
|
|
type: "inserted",
|
|
rowIndex: j,
|
|
originalIndex: i,
|
|
row: row2,
|
|
description: `Row inserted at position ${j}`,
|
|
});
|
|
j++;
|
|
} else if (row1FoundLater !== -1 && row2FoundLater === -1) {
|
|
// row1 is deleted - row2 appears later in array2
|
|
// differences.push({
|
|
// type: "removed",
|
|
// rowIndex: i,
|
|
// originalIndex: i,
|
|
// row: row1,
|
|
// description: `Row removed from position ${i}`,
|
|
// });
|
|
i++;
|
|
} else {
|
|
// Rows are different - this is a modification
|
|
// differences.push({
|
|
// type: "modified",
|
|
// rowIndex: i,
|
|
// originalIndex: i,
|
|
// changes: rowDifferences,
|
|
// description: `Row ${i} has ${rowDifferences.length} difference(s)`,
|
|
// });
|
|
i++;
|
|
j++;
|
|
}
|
|
}
|
|
|
|
return differences;
|
|
}
|
|
|
|
/**
|
|
* Update price task, compare new updated values modified from function with current value
|
|
*
|
|
* 17/4/25 - update for additional parameters
|
|
*
|
|
* @param {String} _sheet sheet name
|
|
* @param {Object} curr_sheet_from_price_slot_map current values of sheet `_sheet`
|
|
* @param {Object} price_slot_mapping_split_by_modified initialized map to stored result of sheet `_sheet` with
|
|
* @param {Object} all_price_sheets all sheet new prices, where values are updated
|
|
* @param {CallableFunction} searchProductCodeRow search function (to find product code on another sheet)
|
|
* @returns `{ changed: [], added: [] }`
|
|
*/
|
|
function createPriceUpdateProcessTask(
|
|
_sheet,
|
|
curr_sheet_from_price_slot_map,
|
|
price_slot_mapping_split_by_modified,
|
|
all_price_sheets,
|
|
searchProductCodeRow,
|
|
) {
|
|
// check source and update
|
|
const getSourceAndUpdate = (current_from_all_price) => {
|
|
let params = current_from_all_price[0][8];
|
|
Log.debug(
|
|
`checking if param config: ${JSON.stringify(current_from_all_price[0])}`,
|
|
);
|
|
if (params) {
|
|
let pmap = getParamsMapFromString(params);
|
|
return pmap;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
var current_price_slot_sheet = all_price_sheets[_sheet];
|
|
var pconfig = getSourceAndUpdate(current_price_slot_sheet);
|
|
price_slot_mapping_split_by_modified[_sheet] = {
|
|
changed: [],
|
|
added: [],
|
|
};
|
|
let source =
|
|
_sheet == "PriceSlot8"
|
|
? current_price_slot_sheet
|
|
: curr_sheet_from_price_slot_map;
|
|
|
|
// let source = curr_sheet_from_price_slot_map;
|
|
|
|
let update, notExistInSource;
|
|
let indexOfUpdate = 0;
|
|
let indexOfSource = 0;
|
|
// for any sheet with "source=" and "update="
|
|
if (pconfig) {
|
|
let skipByParam = false;
|
|
// check reject by skip param
|
|
if (pconfig.has("skip")) {
|
|
let skip_val = pconfig.get("skip");
|
|
Log.debug(`Skip by param --> ${skip_val}`);
|
|
if (skip_val == "true") {
|
|
notExistInSource = undefined;
|
|
source = [];
|
|
Log.warn(`Skip at sheet ${_sheet}`);
|
|
skipByParam = true;
|
|
}
|
|
}
|
|
|
|
if (!skipByParam) {
|
|
source = all_price_sheets[pconfig.get("source")];
|
|
update = all_price_sheets[pconfig.get("update")];
|
|
|
|
indexOfSource = pconfig.get("targetSource");
|
|
indexOfUpdate = pconfig.get("targetUpdate");
|
|
|
|
Log.debug(
|
|
`Found config from sheet, source: [${indexOfSource}], update: [${indexOfUpdate}], `,
|
|
);
|
|
|
|
// add new product code into source
|
|
const sourcePdOnly = source
|
|
.filter((x) => x[indexOfSource] && x[indexOfSource].includes("-"))
|
|
.map((x) => x[indexOfSource]);
|
|
const updatePdOnly = update
|
|
.filter((x) => x[indexOfUpdate] && x[indexOfUpdate].includes("-"))
|
|
.map((x) => x[indexOfUpdate]);
|
|
const currentPdOnly = current_price_slot_sheet
|
|
.filter((x) => x[0] && x[0].includes("-"))
|
|
.map((x) => x[0]);
|
|
|
|
//
|
|
notExistInSource = updatePdOnly.filter(
|
|
(x) => !sourcePdOnly.includes(x) && !currentPdOnly.includes(x),
|
|
);
|
|
Log.debug(
|
|
`Need append ${_sheet} : ${JSON.stringify(notExistInSource)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ===========================================================
|
|
// NEED REWORK NOW, V2
|
|
// ===========================================================
|
|
for (let [index, row] of Object(source).entries()) {
|
|
if (row[indexOfSource] == undefined) {
|
|
continue;
|
|
}
|
|
let search_result = searchProductCodeRow(row[0], _sheet, pconfig);
|
|
// if (_sheet == "PriceSlot8") {
|
|
// Log.debug(
|
|
// `searchResult ${row[indexOfSource]}: ${search_result} ${JSON.stringify(pconfig)} ${search_result !== undefined} must_true: ${row[indexOfSource].includes("-")}`,
|
|
// );
|
|
// }
|
|
if (row[indexOfSource].includes("-") && search_result == undefined) {
|
|
let payload = {
|
|
index: index + 2,
|
|
row: row,
|
|
};
|
|
if (_sheet != "PriceSlot8") {
|
|
Log.debug(
|
|
`${_sheet} -> Payload Appended for ${JSON.stringify(payload)}`,
|
|
);
|
|
price_slot_mapping_split_by_modified[_sheet].added.push(payload);
|
|
} else {
|
|
payload.row[payload.row.length - 1] = "HIDE";
|
|
price_slot_mapping_split_by_modified[_sheet].added.push(payload);
|
|
// Log.debug(`not found ${JSON.stringify(payload)}`);
|
|
// p8hide += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update loop
|
|
// clone first
|
|
let current_price_slot_in_google_sheet = current_price_slot_sheet;
|
|
let updated_from_generated = source.toReversed();
|
|
|
|
// NOTE: PriceSlot8 needs extra processes
|
|
while (updated_from_generated.length > 0 && _sheet != "PriceSlot8") {
|
|
let current_updated = updated_from_generated.pop();
|
|
if (current_updated == undefined) {
|
|
continue;
|
|
}
|
|
|
|
let saved_index = -1;
|
|
let saved_current_shown;
|
|
// check by current
|
|
for (
|
|
let i = 0;
|
|
i < current_price_slot_in_google_sheet.length && current_updated;
|
|
i++
|
|
) {
|
|
// same pd first then name similarity
|
|
let has_same_pd =
|
|
current_updated[0] == current_price_slot_in_google_sheet[i][0];
|
|
let has_same_name =
|
|
current_updated[1] == current_price_slot_in_google_sheet[i][1];
|
|
|
|
if (has_same_pd && has_same_name) {
|
|
// current_price_slot_in_google_sheet[i] = current_updated;
|
|
// updated_from_generated.push(current_updated);
|
|
saved_index = i;
|
|
saved_current_shown = current_price_slot_in_google_sheet[i];
|
|
break;
|
|
} else if (has_same_pd) {
|
|
// current_price_slot_in_google_sheet[i] = current_updated;
|
|
// updated_from_generated.push(current_updated);
|
|
saved_index = i;
|
|
saved_current_shown = current_price_slot_in_google_sheet[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (saved_index > -1 && saved_current_shown && current_updated) {
|
|
let need_update_column = [];
|
|
let change_col = [];
|
|
// diff now
|
|
for (
|
|
let i = 0;
|
|
i < saved_current_shown.length && current_updated;
|
|
i++
|
|
) {
|
|
if (
|
|
saved_current_shown[i] == undefined ||
|
|
current_updated[i] == undefined
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
saved_current_shown[i].toString() !==
|
|
current_updated[i].toString() &&
|
|
_sheet != "PriceSlot8"
|
|
) {
|
|
need_update_column.push(numberToColumn(i + 1));
|
|
change_col.push(i);
|
|
} else if (
|
|
_sheet == "PriceSlot8" &&
|
|
i == 4 &&
|
|
saved_current_shown[i].toString() !== current_updated[i].toString()
|
|
) {
|
|
need_update_column.push(numberToColumn(i + 1));
|
|
change_col.push(i);
|
|
}
|
|
}
|
|
|
|
// Build an update payload
|
|
if (need_update_column.length > 0) {
|
|
let payload = {
|
|
index: saved_index + 1,
|
|
row: current_updated,
|
|
old_row: saved_current_shown,
|
|
column: need_update_column,
|
|
change_col: change_col,
|
|
};
|
|
if (_sheet == "PriceSlot8") {
|
|
payload.row[4] = payload.old_row[4];
|
|
}
|
|
Log.debug(
|
|
`${_sheet} -> Payload Updated for ${_sheet}: ${JSON.stringify(payload)}`,
|
|
);
|
|
price_slot_mapping_split_by_modified[_sheet].changed.push(payload);
|
|
}
|
|
}
|
|
}
|
|
|
|
Log.debug(
|
|
`check length source.${_sheet} ${source.length} by_all ${all_price_sheets[_sheet].length}`,
|
|
);
|
|
|
|
// ===========================================================
|
|
// WIP DO NOT TOUCH
|
|
// ===========================================================
|
|
|
|
if (update && notExistInSource && notExistInSource.length > 0) {
|
|
Log.debug(
|
|
`Has new update by config params, new pd = ${notExistInSource.length}`,
|
|
);
|
|
for (let pd of notExistInSource) {
|
|
let latest =
|
|
price_slot_mapping_split_by_modified[_sheet].added[
|
|
price_slot_mapping_split_by_modified[_sheet].added.length - 1
|
|
];
|
|
// Log.debug(JSON.stringify(price_slot_mapping_split_by_modified[_sheet]));
|
|
Log.debug(
|
|
`get latest for ${pd} ${_sheet}--> ${JSON.stringify(latest)}`,
|
|
);
|
|
let latest_idx = latest.index;
|
|
let latest_row = update.find(
|
|
(x) => x[indexOfUpdate] && x[indexOfUpdate].includes(pd),
|
|
);
|
|
if (_sheet == "PriceSlot8") {
|
|
latest_row[latest_row.length - 1] = "HIDE";
|
|
}
|
|
let payload = {
|
|
index: latest_idx + 2,
|
|
row: latest_row,
|
|
};
|
|
Log.debug(
|
|
`${_sheet} -> Payload Appended for ${JSON.stringify(payload)}`,
|
|
);
|
|
price_slot_mapping_split_by_modified[_sheet].added.push(payload);
|
|
}
|
|
}
|
|
|
|
resolve({
|
|
name: _sheet,
|
|
status: price_slot_mapping_split_by_modified[_sheet],
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Sync price from sheet `price` to other `PriceSlot` sheets.
|
|
*
|
|
* For debugging, set `is_test_mode` to `undefined` (for checking value process)
|
|
* or `true` for setting values to test sheet. Otherwise, set to `false`.
|
|
*
|
|
* @param {sheets_v4.Sheets} sheet sheet instance that already authed
|
|
* @param {String} country_short country short string
|
|
* @param {Boolean} is_test_mode toggle output to test sheet
|
|
*/
|
|
async function syncProfilePrice(sheet, country_short, is_test_mode) {
|
|
var all_price_sheets = await getPriceSlotValues(sheet, country_short);
|
|
var source_price_sheet = all_price_sheets["price"];
|
|
var price_slot_map = new Map();
|
|
|
|
const sheetKeys = Object.keys(all_price_sheets);
|
|
|
|
sheetKeys.forEach((name) => {
|
|
price_slot_map[name] = [];
|
|
});
|
|
|
|
let unique_pd = new Set();
|
|
|
|
for (let row of source_price_sheet) {
|
|
let isHeaderRow = (rw) => rw[1].includes("Name") && rw[5].includes("Price");
|
|
if (row[0] != undefined && row[0].toString().includes("-")) {
|
|
let curr = new RecipePriceProfile(row[0], row[1], row[2], row[4]);
|
|
|
|
// Log.debug(`Processing recipe: ${JSON.stringify(curr.describe())}`);
|
|
|
|
if (!unique_pd.has(row[0])) {
|
|
unique_pd.add(row[0]);
|
|
}
|
|
|
|
for (let i = 1; i < MAXIMUM_SLOTS + 1; i++) {
|
|
let current_price_slot = `PriceSlot${i}`;
|
|
let built_slot_result = curr.build_slot(i, getSlotFunctionByIndex(-1));
|
|
|
|
if (!sheetKeys.includes(current_price_slot)) {
|
|
continue;
|
|
}
|
|
|
|
// add header if length is 0
|
|
if (
|
|
isHeaderRow(all_price_sheets[current_price_slot][0]) &&
|
|
price_slot_map[current_price_slot].size == 0
|
|
) {
|
|
price_slot_map[current_price_slot].push(
|
|
all_price_sheets[current_price_slot][0],
|
|
);
|
|
} else {
|
|
price_slot_map[current_price_slot].push(built_slot_result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const getSourceSheet = (sheetName, config) => {
|
|
if (config) return all_price_sheets[config.source];
|
|
return sheetName === "PriceSlot8"
|
|
? all_price_sheets["TaobinAOTPrice2"]
|
|
: all_price_sheets[sheetName];
|
|
};
|
|
|
|
const isValidRow = (row, sheetName) => {
|
|
if (!row?.length || (sheetName === "PriceSlot8" && !row[1])) return false;
|
|
return true;
|
|
};
|
|
|
|
const matchProductCode = (row, pd, sheetName) => {
|
|
const index = sheetName === "PriceSlot8" ? 1 : 0;
|
|
try {
|
|
return row[index]?.includes(pd);
|
|
} catch (err) {
|
|
Log.err(`${sheetName}.search(${pd}): ${row} -> ${err.stack}`);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const searchProductCodeRow = (pd, sheetName, config) => {
|
|
const source = getSourceSheet(sheetName, config);
|
|
if (!source) {
|
|
return undefined;
|
|
}
|
|
return source.findLast(
|
|
(row) =>
|
|
isValidRow(row, sheetName) && matchProductCode(row, pd, sheetName),
|
|
);
|
|
};
|
|
|
|
// if exist --> price change?
|
|
// if not exist --> append & need highlight
|
|
let concurrent_tasks = [];
|
|
let price_slot_mapping_split_by_modified = {}; // TODO: needs optimization
|
|
|
|
Log.debug(`price length: ${unique_pd.size}`);
|
|
|
|
for (let _sheet of sheetKeys) {
|
|
var curr_sheet_from_price_slot_map = price_slot_map[_sheet];
|
|
|
|
if (
|
|
_sheet == "price" ||
|
|
all_price_sheets[_sheet][0].includes("ServiceType") ||
|
|
_sheet == "TaobinAOTPrice2"
|
|
) {
|
|
Log.debug("skip by unsupported price slot format!");
|
|
continue;
|
|
}
|
|
|
|
// fn
|
|
concurrent_tasks.push(
|
|
createPriceUpdateProcessTask(
|
|
_sheet,
|
|
curr_sheet_from_price_slot_map,
|
|
price_slot_mapping_split_by_modified,
|
|
all_price_sheets,
|
|
searchProductCodeRow,
|
|
),
|
|
);
|
|
}
|
|
|
|
let result_from_prom = await Concurrent.runTasks(
|
|
concurrent_tasks,
|
|
(ok) => {
|
|
saveJsonToFile("./priceslot.updated.json", {
|
|
timestamp: Date.now(),
|
|
data: ok,
|
|
});
|
|
return ok;
|
|
},
|
|
(err) => {
|
|
Log.err(`Promise error! ${err.stack}`);
|
|
return [];
|
|
},
|
|
);
|
|
|
|
// TODO: update value to sheet
|
|
|
|
// result_from_prom.forEach(async (price_slot_change) => {
|
|
// let profileSlotStatus = from_price_process_result(price_slot_change);
|
|
// let last_row_idx = all_price_sheets[profileSlotStatus.name].length;
|
|
// Log.debug(`${profileSlotStatus.name}.lastIdx: ${last_row_idx}`);
|
|
// profileSlotStatus.createUpdateDataFormat(last_row_idx);
|
|
|
|
// // NOTE: range must contain sheet name
|
|
|
|
// let testWithinFirst2 =
|
|
// profileSlotStatus.name == "PriceSlot1" ||
|
|
// profileSlotStatus.name == "PriceSlot2";
|
|
// if (is_test_mode && testWithinFirst2) {
|
|
// // last PriceSlot1 idx = 1004
|
|
// // last PriceSlot2 idx = 1004
|
|
|
|
// const append_result = await sheet.spreadsheets.values.append({
|
|
// range: `${profileSlotStatus.name}!A${profileSlotStatus.gen_added.last_idx}`,
|
|
// spreadsheetId: getCountrySpreadSheetId(
|
|
// is_test_mode ? "test" : country_short
|
|
// ),
|
|
// valueInputOption: "USER_ENTERED",
|
|
// requestBody: {
|
|
// majorDimension: "ROWS",
|
|
// values: profileSlotStatus.gen_added.result,
|
|
// },
|
|
// });
|
|
|
|
// Log.debug(
|
|
// `append to ${profileSlotStatus.name}: ${JSON.stringify(
|
|
// append_result.status
|
|
// )}`
|
|
// );
|
|
|
|
// const update_result = await sheet.spreadsheets.values.batchUpdate({
|
|
// spreadsheetId: getCountrySpreadSheetId(
|
|
// is_test_mode ? "test" : country_short
|
|
// ),
|
|
// requestBody: {
|
|
// valueInputOption: "USER_ENTERED",
|
|
// data: profileSlotStatus.gen_changed.result,
|
|
// },
|
|
// });
|
|
|
|
// Log.debug(
|
|
// `update to ${profileSlotStatus.name}: ${JSON.stringify(
|
|
// update_result.status
|
|
// )}`
|
|
// );
|
|
// }
|
|
// });
|
|
|
|
await _finalizeSyncProfilePrice(
|
|
sheet,
|
|
all_price_sheets,
|
|
result_from_prom,
|
|
country_short,
|
|
is_test_mode,
|
|
);
|
|
|
|
// save
|
|
|
|
// ref: https://github.com/googleapis/google-api-nodejs-client
|
|
|
|
// update
|
|
// https://cloud.google.com/workflows/docs/reference/googleapis/sheets/v4/spreadsheets.values/update
|
|
|
|
// batch update
|
|
// https://cloud.google.com/workflows/docs/reference/googleapis/sheets/v4/spreadsheets.values/batchUpdate
|
|
|
|
// append
|
|
// https://cloud.google.com/workflows/docs/reference/googleapis/sheets/v4/spreadsheets.values/append
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {sheets_v4.Sheets} sheet sheet instance
|
|
* @param {Map} all_price_sheets all fetched price sheets
|
|
* @param {Array} result_from_prom result of processed promise, expect format of `{ changed: [], added: [] }[]`
|
|
* @param {String} country_short country short string
|
|
* @param {Boolean} is_test_mode toggle output test mode
|
|
*/
|
|
async function _finalizeSyncProfilePrice(
|
|
sheet,
|
|
all_price_sheets,
|
|
result_from_prom,
|
|
country_short,
|
|
is_test_mode,
|
|
) {
|
|
const batchOps = result_from_prom.reduce(
|
|
(acc, price_slot_change) => {
|
|
let profileSlotStatus = from_price_process_result(price_slot_change);
|
|
let last_row_idx = all_price_sheets[profileSlotStatus.name].length;
|
|
Log.debug(`${profileSlotStatus.name}.lastIdx: ${last_row_idx}`);
|
|
profileSlotStatus.createUpdateDataFormat(last_row_idx);
|
|
|
|
// let testWithinFirst2 = profileSlotStatus.name == "PriceSlot1";
|
|
acc.append.push({
|
|
range: `${profileSlotStatus.name}!A${profileSlotStatus.gen_added.last_idx}`,
|
|
values: profileSlotStatus.gen_added.result,
|
|
});
|
|
acc.update.push(...profileSlotStatus.gen_changed.result);
|
|
|
|
return acc;
|
|
},
|
|
{ append: [], update: [] },
|
|
);
|
|
|
|
if (is_test_mode) {
|
|
Log.debug(`test mode: ${is_test_mode}`);
|
|
// save result
|
|
saveJsonToFile("./priceslot.test.json", batchOps);
|
|
return;
|
|
}
|
|
|
|
// Log.debug(`debug batchops: ${JSON.stringify(batchOps)}`);
|
|
if (!is_test_mode && batchOps.append.length) {
|
|
let result_append = await Promise.all(
|
|
batchOps.append.map((ops) =>
|
|
sheet.spreadsheets.values.append({
|
|
range: ops.range,
|
|
spreadsheetId: getCountrySpreadSheetId(
|
|
is_test_mode ? "test" : country_short,
|
|
),
|
|
valueInputOption: "USER_ENTERED",
|
|
requestBody: {
|
|
majorDimension: "ROWS",
|
|
values: ops.values,
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
// Log.debug(`debug batchops append: ${JSON.stringify(result_append)}`);
|
|
saveJsonToFile("./priceslot.append.final.json", result_append);
|
|
} else {
|
|
Log.warn("No append");
|
|
}
|
|
|
|
if (!is_test_mode && batchOps.update.length) {
|
|
let result_sheet_update = await sheet.spreadsheets.values.batchUpdate({
|
|
spreadsheetId: getCountrySpreadSheetId(
|
|
is_test_mode ? "test" : country_short,
|
|
),
|
|
requestBody: {
|
|
valueInputOption: "USER_ENTERED",
|
|
data: batchOps.update,
|
|
},
|
|
});
|
|
// Log.debug(`debug batchops update: ${JSON.stringify(result_sheet_update)}`);
|
|
saveJsonToFile("./priceslot.update.final.json", result_sheet_update);
|
|
} else {
|
|
Log.warn("No update");
|
|
}
|
|
}
|
|
|
|
// special
|
|
|
|
// ======================================================================
|
|
// PLUGINS
|
|
// ======================================================================
|
|
|
|
class PluginsManager {
|
|
source = "./plugins";
|
|
// store file contents and eval later
|
|
|
|
/**
|
|
* Script Mappings, contained filename and script part
|
|
*
|
|
* @type {Map}
|
|
*/
|
|
scripts = {};
|
|
|
|
constructor(cronTask, CronJobs) {
|
|
this.cronTask = cronTask;
|
|
this.CronJobs = CronJobs;
|
|
}
|
|
|
|
load() {
|
|
try {
|
|
const files = fs.readdirSync(this.source);
|
|
files.forEach((file, index) => {
|
|
const data = fs.readFileSync(`${this.source}/${file}`, "utf8");
|
|
this.scripts[file] = data;
|
|
});
|
|
} catch (loadError) {
|
|
Log.err(`[pm] load error: ${loadError.stack}`);
|
|
}
|
|
}
|
|
|
|
update(new_file_only) {
|
|
try {
|
|
const files = fs.readdirSync(this.source);
|
|
for (let file of files) {
|
|
if (new_file_only && this.scripts[file] != undefined) {
|
|
// detect new file while iterate list of file
|
|
continue;
|
|
}
|
|
const data = fs.readFileSync(`${this.source}/${file}`, "utf8");
|
|
this.scripts[file] = data;
|
|
}
|
|
} catch (loadError) {
|
|
Log.err(`[pm] load error: ${loadError.stack}`);
|
|
}
|
|
}
|
|
|
|
checkScript() {
|
|
Log.info(`[pm] ${Object.keys(this.scripts)}`);
|
|
}
|
|
|
|
listRunnableScripts() {
|
|
const scriptNames = Object.keys(this.scripts);
|
|
let result = [];
|
|
for (let sn of scriptNames) {
|
|
if (this.scripts[sn] != undefined) {
|
|
result.push(sn);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Express} app an express app instance
|
|
* @param {Express} express another express app instance for non-cyclic
|
|
* @param {String} pluginName existed plugin name
|
|
* @param {Map} endpointMap endpoint mapping
|
|
*/
|
|
createEndpoint(app, express, pluginName, endpointMap) {
|
|
var router = express.Router();
|
|
Log.debug(`[pm] Creating endpoint for ${pluginName} ... `);
|
|
if (endpointMap == undefined) {
|
|
Log.debug(`[pm] undefined map detected! ${pluginName} ... `);
|
|
// do create default
|
|
let purePluginName = pluginName.toString().split(".")[0];
|
|
Log.debug(`[pm] endpoint name = ${purePluginName} ... `);
|
|
|
|
app.use(
|
|
`/${purePluginName}`,
|
|
router.get("/", function (req, res, next) {
|
|
res.send(`${purePluginName} does not have functions yet!`);
|
|
}),
|
|
);
|
|
|
|
Log.warn(
|
|
`[pm] Created default endpoint for ${purePluginName} --> /${purePluginName}`,
|
|
);
|
|
} else {
|
|
let purePluginName = pluginName.toString().split(".")[0];
|
|
|
|
const endpointNames = Object.keys(endpointMap);
|
|
for (let en of endpointNames) {
|
|
router.get(`/${en}`, endpointMap[en]);
|
|
Log.debug(`[pm] ... created ${purePluginName}/${en}`);
|
|
}
|
|
|
|
app.use(`/${purePluginName}`, router);
|
|
}
|
|
}
|
|
|
|
// runScript(name){
|
|
// try {
|
|
// return eval(this.scripts[name]);
|
|
// } catch(runScriptErr) {
|
|
// Log.err(`[pm] run script error: ${runScriptErr.stack}`);
|
|
// }
|
|
// }
|
|
}
|
|
|
|
module.exports = {
|
|
ProfilePrice,
|
|
logger,
|
|
Log,
|
|
GoogleFunctions,
|
|
Concurrent,
|
|
getTestSpreadSheet,
|
|
PluginsManager,
|
|
getCountrySheetByName,
|
|
diff2DArraysCustom,
|
|
saveJsonToFile,
|
|
};
|