formula-parser-payroll 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -0
- package/dist/database/database-connector.d.ts +60 -0
- package/dist/database/database-connector.d.ts.map +1 -0
- package/dist/database/database-connector.js +9 -0
- package/dist/database/database-connector.js.map +1 -0
- package/dist/database/helpers.d.ts +44 -0
- package/dist/database/helpers.d.ts.map +1 -0
- package/dist/database/helpers.js +120 -0
- package/dist/database/helpers.js.map +1 -0
- package/dist/database/index.d.ts +11 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +29 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/payroll-formula.service.d.ts +118 -0
- package/dist/database/payroll-formula.service.d.ts.map +1 -0
- package/dist/database/payroll-formula.service.js +794 -0
- package/dist/database/payroll-formula.service.js.map +1 -0
- package/dist/database/types.d.ts +117 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +9 -0
- package/dist/database/types.js.map +1 -0
- package/dist/formula-engine.d.ts +18 -0
- package/dist/formula-engine.d.ts.map +1 -0
- package/dist/formula-engine.js +356 -0
- package/dist/formula-engine.js.map +1 -0
- package/dist/formula-parser.d.ts +60 -0
- package/dist/formula-parser.d.ts.map +1 -0
- package/dist/formula-parser.js +366 -0
- package/dist/formula-parser.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/nestjs/database-connector.d.ts +60 -0
- package/dist/nestjs/database-connector.d.ts.map +1 -0
- package/dist/nestjs/database-connector.js +9 -0
- package/dist/nestjs/database-connector.js.map +1 -0
- package/dist/nestjs/helpers.d.ts +44 -0
- package/dist/nestjs/helpers.d.ts.map +1 -0
- package/dist/nestjs/helpers.js +120 -0
- package/dist/nestjs/helpers.js.map +1 -0
- package/dist/nestjs/index.d.ts +11 -0
- package/dist/nestjs/index.d.ts.map +1 -0
- package/dist/nestjs/index.js +29 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/nestjs/payroll-formula.service.d.ts +91 -0
- package/dist/nestjs/payroll-formula.service.d.ts.map +1 -0
- package/dist/nestjs/payroll-formula.service.js +640 -0
- package/dist/nestjs/payroll-formula.service.js.map +1 -0
- package/dist/nestjs/types.d.ts +117 -0
- package/dist/nestjs/types.d.ts.map +1 -0
- package/dist/nestjs/types.js +9 -0
- package/dist/nestjs/types.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* NestJS Payroll Formula Parser Service
|
|
4
|
+
*
|
|
5
|
+
* This service provides database-integrated formula parsing.
|
|
6
|
+
* It fetches employee data and component values from the database
|
|
7
|
+
* and then uses the core formula parser.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.PayrollFormulaService = void 0;
|
|
11
|
+
const formula_engine_1 = require("../formula-engine");
|
|
12
|
+
const formula_parser_1 = require("../formula-parser");
|
|
13
|
+
const helpers_1 = require("./helpers");
|
|
14
|
+
/**
|
|
15
|
+
* Payroll Formula Parser Service
|
|
16
|
+
*
|
|
17
|
+
* Use this service when you need to parse formulas with just formula and empId.
|
|
18
|
+
* This service handles fetching employee data and component values from the database.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const service = new PayrollFormulaService();
|
|
23
|
+
* const result = await service.parseByEmpId({
|
|
24
|
+
* formula: 'SALARY * 0.1 + AL_001',
|
|
25
|
+
* empId: 'EMP001',
|
|
26
|
+
* auth: authContext,
|
|
27
|
+
* });
|
|
28
|
+
* console.log(result.result);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
class PayrollFormulaService {
|
|
32
|
+
constructor() {
|
|
33
|
+
// Performance optimization: Cache for reserved words and component lists
|
|
34
|
+
this.reservedWordsCache = new Map();
|
|
35
|
+
this.componentListCache = new Map();
|
|
36
|
+
this.CACHE_TTL = 5 * 60 * 1000; // 5 minutes cache
|
|
37
|
+
this.CHUNK_SIZE = 50; // Process employees in chunks of 50 to prevent memory overflow
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse formula by employee ID(s)
|
|
41
|
+
* This method fetches all required data from database and evaluates the formula
|
|
42
|
+
*
|
|
43
|
+
* When listEmpId is provided: returns Record<empId, hasil>
|
|
44
|
+
* When empId is provided: returns FormulaParseResultLegacy
|
|
45
|
+
*/
|
|
46
|
+
async parseByEmpId(input) {
|
|
47
|
+
const { formula: rawFormula, empId, listEmpId, auth } = input;
|
|
48
|
+
console.log('>>> PayrollFormulaService.parseByEmpId CALLED', { rawFormula, listEmpIdLen: listEmpId?.length });
|
|
49
|
+
// If listEmpId is provided, process multiple employees IN PARALLEL WITH CHUNKING
|
|
50
|
+
if (listEmpId && listEmpId.length > 0) {
|
|
51
|
+
const resultData = {};
|
|
52
|
+
// OPTIMIZATION: Process employees in chunks to prevent memory overflow
|
|
53
|
+
// For large employee lists (1000+), processing all at once can overwhelm memory
|
|
54
|
+
const chunks = this.chunkArray(listEmpId, this.CHUNK_SIZE);
|
|
55
|
+
console.log(`>>> Processing ${listEmpId.length} employees in ${chunks.length} chunks of ${this.CHUNK_SIZE}`);
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
// Process each chunk in parallel
|
|
58
|
+
const promises = chunk.map(emp => this.parseForSingleEmployee(rawFormula, emp, auth));
|
|
59
|
+
const results = await Promise.all(promises);
|
|
60
|
+
// Map results back to employee IDs
|
|
61
|
+
chunk.forEach((emp, idx) => {
|
|
62
|
+
resultData[emp] = results[idx].hasil;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return resultData;
|
|
66
|
+
}
|
|
67
|
+
// Single employee processing
|
|
68
|
+
if (!empId) {
|
|
69
|
+
return {
|
|
70
|
+
hasil: 0,
|
|
71
|
+
message: 'Either empId or listEmpId must be provided',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return this.parseForSingleEmployee(rawFormula, empId, auth);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check if a formula is valid
|
|
78
|
+
* This ports the logic from payroll-server's formulaValidtyCheck
|
|
79
|
+
*/
|
|
80
|
+
async formulaValidityCheck(input) {
|
|
81
|
+
const { formula: rawFormula, auth } = input;
|
|
82
|
+
try {
|
|
83
|
+
if (!rawFormula) {
|
|
84
|
+
throw new Error('Formula cannot be empty');
|
|
85
|
+
}
|
|
86
|
+
const formula = rawFormula.replace(/"/g, `'`);
|
|
87
|
+
// 1. Get reserved words from DB
|
|
88
|
+
const getWordList = await this.getReservedWords(auth);
|
|
89
|
+
const reservedWords = new Set(getWordList.words2); // words2 contains allowdeductCode
|
|
90
|
+
// 2. Extra keywords that are always valid
|
|
91
|
+
// Added more from server logic
|
|
92
|
+
const extraValid = new Set([
|
|
93
|
+
'IF', 'AND', 'OR', 'NOT', 'TRUE', 'FALSE', 'SALARY',
|
|
94
|
+
'MIN', 'MAX', 'MOD', 'ROUND', 'ROUNDDOWN', 'ROUNDUP', 'TRUNC',
|
|
95
|
+
'DATE', 'DAYS', 'TODAY', 'DATETIME_NOW', 'ISDATE', 'DAY', 'MONTH', 'YEAR',
|
|
96
|
+
'DAYSINMONTH', 'DAYOFWEEK', 'ISNUMERIC', 'HOUR', 'MINUTE', 'SECOND',
|
|
97
|
+
'DATEDIFF', 'DATEADD', 'CONCATENATE', 'CONCATENATESKIPNULL', 'FINDLIST', 'CREATEDATE',
|
|
98
|
+
'LENGTHOFSERVICE', 'CHILDDEPENDENTS',
|
|
99
|
+
'CUSTOMFIELD', // Base word
|
|
100
|
+
]);
|
|
101
|
+
// 3. Handle String Literals first!
|
|
102
|
+
// Anything inside quotes should NOT be validated as a variable.
|
|
103
|
+
// Replace quoted strings with placeholders
|
|
104
|
+
const formulaNoStrings = formula.replace(/'[^']*'/g, 'STRING_LITERAL');
|
|
105
|
+
// 4. Extract all alpha tokens (potential variables/functions)
|
|
106
|
+
// This regex matches words like GRADE, CUSTOMFIELD5, AL_001, etc.
|
|
107
|
+
// We use the formula WITHOUT strings to check tokens.
|
|
108
|
+
const tokens = formulaNoStrings.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) || [];
|
|
109
|
+
for (const token of tokens) {
|
|
110
|
+
const upperToken = token.toUpperCase();
|
|
111
|
+
// Skip placeholders
|
|
112
|
+
if (upperToken === 'STRING_LITERAL')
|
|
113
|
+
continue;
|
|
114
|
+
// Skip if it's a reserved word or a known function/keyword
|
|
115
|
+
if (reservedWords.has(upperToken) || extraValid.has(upperToken)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Special handling for CUSTOMFIELD1 - CUSTOMFIELD10
|
|
119
|
+
if (/^CUSTOMFIELD\d+$/.test(upperToken)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Skip if it's a payroll component code (AL_xxx, DE_xxx, NE_xxx)
|
|
123
|
+
if (/^(AL|DE|NE)_\d+$/i.test(token)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Potential typo or invalid keyword
|
|
127
|
+
throw new Error(`Failed not defined reserve word or formula is not valid: ${token}`);
|
|
128
|
+
}
|
|
129
|
+
// 5. Final syntax check by attempting a dummy parse
|
|
130
|
+
// We need to replace all valid tokens with dummy values, otherwise the parser will complain about #NAME?
|
|
131
|
+
let dummyFormula = formula;
|
|
132
|
+
// Sort tokens by length (desc) to avoid partial replacement
|
|
133
|
+
const sortedTokens = [...new Set(tokens)].sort((a, b) => b.length - a.length);
|
|
134
|
+
for (const token of sortedTokens) {
|
|
135
|
+
// Replace token with "0" (as string) or 0 (as number)
|
|
136
|
+
// Since we don't know the type, let's use 0 which is safe for most math/logic
|
|
137
|
+
// Use regex for word boundary replacement
|
|
138
|
+
const regex = new RegExp(`\\b${token}\\b`, 'g');
|
|
139
|
+
dummyFormula = dummyFormula.replace(regex, '0');
|
|
140
|
+
}
|
|
141
|
+
// Restore string literals (they were replaced by 'STRING_LITERAL' in token check step? No, that was a copy)
|
|
142
|
+
// wait, 'tokens' were extracted from 'formulaNoStrings'.
|
|
143
|
+
// 'dummyFormula' starts from original 'formula' (which has strings).
|
|
144
|
+
// If we replace tokens in original formula, we might accidentally replace content inside strings!
|
|
145
|
+
// But we know 'tokens' are extracted from outside strings.
|
|
146
|
+
// Safer approach:
|
|
147
|
+
// We already validated all tokens are "legal".
|
|
148
|
+
// The only reason to run parseFormula is to check parentheses balance, commas in functions, etc.
|
|
149
|
+
// So let's run parseFormula on the dummy-filled string.
|
|
150
|
+
// But we must be careful not to replace text inside quotes.
|
|
151
|
+
// E.g. IF(GRADE = "GRADE", ...) -> The second GRADE is string.
|
|
152
|
+
// Strategy:
|
|
153
|
+
// 1. Hide strings placeholders
|
|
154
|
+
const regexKutip = /'[^']*'/g;
|
|
155
|
+
let match;
|
|
156
|
+
const placeholders = {};
|
|
157
|
+
let pIdx = 0;
|
|
158
|
+
let formulaWithPlaceholders = formula.replace(regexKutip, (m) => {
|
|
159
|
+
const key = `__STR_${pIdx++}__`;
|
|
160
|
+
placeholders[key] = m; // equivalent to "0" for parsing safety?
|
|
161
|
+
// Actually, for syntax check, a string literal is just a value.
|
|
162
|
+
// Let's replace it with "0" (string "0") to be safe?
|
|
163
|
+
// Or just keep it as a string literal but simple one like 'S'
|
|
164
|
+
return "'S'";
|
|
165
|
+
});
|
|
166
|
+
// 2. Replace variables in non-string part with 0
|
|
167
|
+
// We must NOT replace Functions (IF, MAX, etc) with 0. Only variables.
|
|
168
|
+
const functionsToPreserve = new Set([
|
|
169
|
+
'IF', 'AND', 'OR', 'NOT',
|
|
170
|
+
'MIN', 'MAX', 'MOD', 'ROUND', 'ROUNDDOWN', 'ROUNDUP', 'TRUNC',
|
|
171
|
+
'DATE', 'DAYS', 'TODAY', 'DATETIME_NOW', 'ISDATE', 'DAY', 'MONTH', 'YEAR',
|
|
172
|
+
'DAYSINMONTH', 'DAYOFWEEK', 'ISNUMERIC', 'HOUR', 'MINUTE', 'SECOND',
|
|
173
|
+
'DATEDIFF', 'DATEADD', 'CONCATENATE', 'CONCATENATESKIPNULL', 'FINDLIST', 'CREATEDATE',
|
|
174
|
+
'LENGTHOFSERVICE'
|
|
175
|
+
]);
|
|
176
|
+
for (const token of sortedTokens) {
|
|
177
|
+
const upperToken = token.toUpperCase();
|
|
178
|
+
// If it is a function, DO NOT replace it.
|
|
179
|
+
if (functionsToPreserve.has(upperToken)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// `TRUE` and `FALSE` are boolean literals, parser handles them. No need to replace.
|
|
183
|
+
if (upperToken === 'TRUE' || upperToken === 'FALSE') {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const regex = new RegExp(`\\b${token}\\b`, 'g');
|
|
187
|
+
formulaWithPlaceholders = formulaWithPlaceholders.replace(regex, '0');
|
|
188
|
+
}
|
|
189
|
+
// 3. Test parse
|
|
190
|
+
try {
|
|
191
|
+
const result = (0, formula_engine_1.parseFormula)(formulaWithPlaceholders);
|
|
192
|
+
// We ignore the result value, just checking for throw
|
|
193
|
+
}
|
|
194
|
+
catch (parseError) {
|
|
195
|
+
// If it's #NAME?, it means we missed a token?
|
|
196
|
+
// Any other error is syntax error.
|
|
197
|
+
const err = parseError;
|
|
198
|
+
if (err.message.includes('#NAME?')) {
|
|
199
|
+
// This can happen if hot-formula-parser doesn't support a specific function
|
|
200
|
+
// that we thought was valid in extraValid (e.g. LENGTHOFSERVICE is custom registered).
|
|
201
|
+
// But Custom functions SHOULD be registered in formula-engine.ts
|
|
202
|
+
// If we validated token as legal, but parser says #NAME, it might be a function name we didn't replace?
|
|
203
|
+
// Functions like 'IF', 'ROUND' are in extraValid.
|
|
204
|
+
// We should NOT replace function names with 0!
|
|
205
|
+
// Ah! My token extraction logic extracts everything including IF, ROUND.
|
|
206
|
+
// But my validation loop "continues" (skips error) if it is in extraValid.
|
|
207
|
+
// So 'IF' is in tokens?
|
|
208
|
+
// Yes: const tokens = formulaNoStrings.match(...)
|
|
209
|
+
// Wait, I should ONLY replace "Variables", not "Functions".
|
|
210
|
+
throw parseError; // Rethrow to see details if needed
|
|
211
|
+
}
|
|
212
|
+
throw parseError;
|
|
213
|
+
}
|
|
214
|
+
return { result: 'success' };
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Clear all caches (useful for testing or when data is updated)
|
|
222
|
+
*/
|
|
223
|
+
clearCache() {
|
|
224
|
+
this.reservedWordsCache.clear();
|
|
225
|
+
this.componentListCache.clear();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Clear cache for specific company
|
|
229
|
+
*/
|
|
230
|
+
clearCacheForCompany(companyId, taxCountry) {
|
|
231
|
+
const cacheKey = `${companyId}_${taxCountry}`;
|
|
232
|
+
this.reservedWordsCache.delete(cacheKey);
|
|
233
|
+
this.componentListCache.delete(cacheKey);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parse formula for a single employee
|
|
237
|
+
* Internal method used by parseByEmpId
|
|
238
|
+
*/
|
|
239
|
+
async parseForSingleEmployee(rawFormula, empId, auth) {
|
|
240
|
+
try {
|
|
241
|
+
// Normalize formula
|
|
242
|
+
const formula = rawFormula.replace(/"/g, `'`);
|
|
243
|
+
// OPTIMIZATION: Pre-extract actual component codes from formula
|
|
244
|
+
// This is more accurate than just checking if components exist
|
|
245
|
+
const componentMatches = formula.match(/\b(AL_\d+|DE_\d+|NE_\d+|SALARY)\b/gi) || [];
|
|
246
|
+
const uniqueComponents = new Set(componentMatches.map(c => c.toUpperCase()));
|
|
247
|
+
const hasComponents = uniqueComponents.size > 0;
|
|
248
|
+
let allCompo = [];
|
|
249
|
+
let empCompo = [];
|
|
250
|
+
let compoCodes = [];
|
|
251
|
+
if (hasComponents) {
|
|
252
|
+
// Step 1: Get list of component codes, but filter by what's actually in formula
|
|
253
|
+
const listCompo = await this.getComponentList(auth);
|
|
254
|
+
// OPTIMIZATION: Only include components that are actually in the formula
|
|
255
|
+
compoCodes = listCompo
|
|
256
|
+
.map(v => v.allowdeductCode)
|
|
257
|
+
.filter(code => uniqueComponents.has(code.toUpperCase()));
|
|
258
|
+
console.log(`>>> Detected ${uniqueComponents.size} unique components in formula, ${compoCodes.length} exist in DB`);
|
|
259
|
+
}
|
|
260
|
+
// Step 2: Fetch all required data in parallel
|
|
261
|
+
// Only fetch components if they are actually in the formula
|
|
262
|
+
const currencyCode = (0, helpers_1.getCurrencyCode)(auth.userData.taxCountry);
|
|
263
|
+
const [allCompoRes, empCompoRes, empData, getWordList] = await Promise.all([
|
|
264
|
+
hasComponents ? this.getAllComponents(auth, compoCodes, currencyCode) : Promise.resolve([]),
|
|
265
|
+
hasComponents ? this.getEmployeeComponents(auth, empId, compoCodes, currencyCode) : Promise.resolve([]),
|
|
266
|
+
this.getEmployeeData(auth, empId),
|
|
267
|
+
this.getReservedWords(auth),
|
|
268
|
+
]);
|
|
269
|
+
allCompo = allCompoRes;
|
|
270
|
+
empCompo = empCompoRes;
|
|
271
|
+
if (!empData) {
|
|
272
|
+
return {
|
|
273
|
+
hasil: 0,
|
|
274
|
+
message: `Employee not found: ${empId}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// Step 3: Build object values from components
|
|
278
|
+
const objectVal = {};
|
|
279
|
+
if (hasComponents) {
|
|
280
|
+
// Initialize all components to 0
|
|
281
|
+
for (const compo of allCompo) {
|
|
282
|
+
objectVal[compo.allowdeductCode] = '0';
|
|
283
|
+
}
|
|
284
|
+
// Initialize employee components to 0 first
|
|
285
|
+
for (const empCompoItem of empCompo) {
|
|
286
|
+
objectVal[empCompoItem.allowdeductCode] = '0';
|
|
287
|
+
}
|
|
288
|
+
// OPTIMIZATION: Chunked parallel decryption for employee components
|
|
289
|
+
// Process in chunks of 20 to prevent overwhelming the system
|
|
290
|
+
const itemsToDecrypt = empCompo.filter(item => item.allowdeductValue);
|
|
291
|
+
if (itemsToDecrypt.length > 0) {
|
|
292
|
+
const DECRYPT_CHUNK_SIZE = 20;
|
|
293
|
+
const decryptChunks = this.chunkArray(itemsToDecrypt, DECRYPT_CHUNK_SIZE);
|
|
294
|
+
for (const chunk of decryptChunks) {
|
|
295
|
+
const decryptPromises = chunk.map(async (item) => ({
|
|
296
|
+
code: item.allowdeductCode,
|
|
297
|
+
value: await (0, helpers_1.decryptValue)(item.allowdeductValue, auth, empId)
|
|
298
|
+
}));
|
|
299
|
+
const decryptedValues = await Promise.all(decryptPromises);
|
|
300
|
+
for (const { code, value } of decryptedValues) {
|
|
301
|
+
objectVal[code] = value.toString();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Step 4: Extract required words from formula
|
|
307
|
+
const requiredWords = this.extractRequiredWordsFromFormula(formula, getWordList.words2);
|
|
308
|
+
// Step 5: Batch load organizational data
|
|
309
|
+
const dataCache = await this.batchLoadFormulaData(auth, empData, requiredWords);
|
|
310
|
+
// Step 6: Process all words using cached data
|
|
311
|
+
const employeeDataConverted = this.convertToEmployeeData(empData);
|
|
312
|
+
const orgDataConverted = this.convertToOrganizationalData(dataCache);
|
|
313
|
+
for (const word of requiredWords) {
|
|
314
|
+
// Process personal data
|
|
315
|
+
(0, formula_parser_1.processPersonalData)(word, objectVal, employeeDataConverted);
|
|
316
|
+
// Process organizational data
|
|
317
|
+
(0, formula_parser_1.processOrganizationalData)(word, objectVal, orgDataConverted, formula);
|
|
318
|
+
// Process custom fields
|
|
319
|
+
(0, formula_parser_1.processCustomFields)(word, objectVal, orgDataConverted);
|
|
320
|
+
}
|
|
321
|
+
// Step 7: Handle formula replacement (same as original logic)
|
|
322
|
+
let processedFormula = formula;
|
|
323
|
+
// Handle quoted strings
|
|
324
|
+
const regexKutip = /\"(.*?)\"/g;
|
|
325
|
+
let match;
|
|
326
|
+
const objectX = {};
|
|
327
|
+
let arr = 0;
|
|
328
|
+
const replacements = {};
|
|
329
|
+
while ((match = regexKutip.exec(processedFormula))) {
|
|
330
|
+
replacements[`${match[1]}`] = 'PART' + arr;
|
|
331
|
+
objectX['PART' + arr] = `${match[1]}`;
|
|
332
|
+
arr++;
|
|
333
|
+
}
|
|
334
|
+
const hasReplacements = Object.values(replacements).some(value => value !== undefined && value !== '');
|
|
335
|
+
if (hasReplacements) {
|
|
336
|
+
const regex = new RegExp(Object.keys(replacements).join('|'), 'g');
|
|
337
|
+
processedFormula = processedFormula.replace(regex, m => replacements[m]);
|
|
338
|
+
}
|
|
339
|
+
// Extract and replace keywords
|
|
340
|
+
const formula2 = processedFormula.match(/[a-zA-Z0-9_]+|[+\-*/]/g) || [];
|
|
341
|
+
let keys = Object.keys(objectVal);
|
|
342
|
+
keys = (0, formula_parser_1.sortKeyByLengthDesc)(keys);
|
|
343
|
+
// Build replacement map
|
|
344
|
+
const replacementMap = new Map();
|
|
345
|
+
for (const wordnya of formula2) {
|
|
346
|
+
if (keys.includes(wordnya) && wordnya !== 'LENGTHOFSERVICE' && !replacementMap.has(wordnya)) {
|
|
347
|
+
replacementMap.set(wordnya, objectVal[wordnya].toString());
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Single-pass replacement
|
|
351
|
+
if (replacementMap.size > 0) {
|
|
352
|
+
const pattern = Array.from(replacementMap.keys())
|
|
353
|
+
.map(word => `\\b${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
|
|
354
|
+
.join('|');
|
|
355
|
+
const regex = new RegExp(pattern, 'g');
|
|
356
|
+
processedFormula = processedFormula.replace(regex, m => replacementMap.get(m) || m);
|
|
357
|
+
}
|
|
358
|
+
// Restore quoted strings
|
|
359
|
+
const keys2 = Object.keys(objectX);
|
|
360
|
+
if (keys2.length > 0) {
|
|
361
|
+
keys2.forEach(key => {
|
|
362
|
+
processedFormula = processedFormula.replace(new RegExp(key, 'g'), `${objectX[key]}`);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// Step 8: Parse the formula
|
|
366
|
+
const result = (0, formula_engine_1.parseFormula)(processedFormula);
|
|
367
|
+
// console.log('\n=== DEBUG PAYROLL-FORMULA-PARSER ===');
|
|
368
|
+
// console.log('Emp ID:', empId);
|
|
369
|
+
// console.log('Raw Formula:', rawFormula);
|
|
370
|
+
// console.log('Processed Formula:', processedFormula);
|
|
371
|
+
// console.log('Result:', result);
|
|
372
|
+
// console.log('Resolved Variables:', JSON.stringify(objectVal, null, 2));
|
|
373
|
+
// console.log('====================================\n');
|
|
374
|
+
return {
|
|
375
|
+
hasil: result,
|
|
376
|
+
message: 'success',
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
console.log(error);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Parse formula for multiple employees
|
|
386
|
+
*/
|
|
387
|
+
async parseForMultipleEmployees(formula, empIds, auth) {
|
|
388
|
+
const results = {};
|
|
389
|
+
for (const empId of empIds) {
|
|
390
|
+
results[empId] = await this.parseForSingleEmployee(formula, empId, auth);
|
|
391
|
+
}
|
|
392
|
+
return results;
|
|
393
|
+
}
|
|
394
|
+
// ==================== Private Methods ====================
|
|
395
|
+
/**
|
|
396
|
+
* Get list of component codes
|
|
397
|
+
*/
|
|
398
|
+
async getComponentList(auth) {
|
|
399
|
+
// OPTIMIZATION: Cache component list (rarely changes)
|
|
400
|
+
const cacheKey = `${auth.userData.companyId}_${auth.userData.taxCountry}`;
|
|
401
|
+
const cached = this.componentListCache.get(cacheKey);
|
|
402
|
+
if (cached && (Date.now() - cached.timestamp < this.CACHE_TTL)) {
|
|
403
|
+
return cached.data;
|
|
404
|
+
}
|
|
405
|
+
let query = `
|
|
406
|
+
SELECT allowdeduct_code as allowdeductCode
|
|
407
|
+
FROM tpympayallowdeduct
|
|
408
|
+
WHERE company_id = ?
|
|
409
|
+
AND (
|
|
410
|
+
(allowdeduct_code LIKE 'AL_%' AND allowdeducttype = 'A' AND allowdeduct_code REGEXP 'AL_[0-9]+')
|
|
411
|
+
OR (allowdeduct_code LIKE 'DE_%' AND allowdeducttype = 'D' AND allowdeduct_code REGEXP 'DE_[0-9]+')
|
|
412
|
+
OR (allowdeduct_code LIKE 'NE_%' AND allowdeducttype = 'N' AND allowdeduct_code REGEXP 'NE_[0-9]+')
|
|
413
|
+
OR allowdeduct_code = 'SALARY'
|
|
414
|
+
)
|
|
415
|
+
`;
|
|
416
|
+
if (auth.userData.taxCountry === 'TH') {
|
|
417
|
+
query += ` AND allowdeduct_code NOT IN ('NE_002', 'NE_003')`;
|
|
418
|
+
}
|
|
419
|
+
const result = await auth.connFin.query(query, [auth.userData.companyId]);
|
|
420
|
+
// Cache the result
|
|
421
|
+
this.componentListCache.set(cacheKey, { data: result, timestamp: Date.now() });
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get all components for company
|
|
426
|
+
*/
|
|
427
|
+
async getAllComponents(auth, compoCodes, currencyCode) {
|
|
428
|
+
if (compoCodes.length === 0)
|
|
429
|
+
return [];
|
|
430
|
+
const placeholders = compoCodes.map(() => '?').join(',');
|
|
431
|
+
const query = `
|
|
432
|
+
SELECT allowdeduct_code as allowdeductCode
|
|
433
|
+
FROM tpympayallowdeduct
|
|
434
|
+
WHERE company_id = ?
|
|
435
|
+
AND allowdeduct_code IN (${placeholders})
|
|
436
|
+
AND currency_code = ?
|
|
437
|
+
`;
|
|
438
|
+
return auth.connFin.query(query, [auth.userData.companyId, ...compoCodes, currencyCode]);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get employee component values
|
|
442
|
+
*/
|
|
443
|
+
async getEmployeeComponents(auth, empId, compoCodes, currencyCode) {
|
|
444
|
+
if (compoCodes.length === 0)
|
|
445
|
+
return [];
|
|
446
|
+
const placeholders = compoCodes.map(() => '?').join(',');
|
|
447
|
+
const query = `
|
|
448
|
+
SELECT allowdeduct_code as allowdeductCode, allowdeduct_value as allowdeductValue
|
|
449
|
+
FROM tpydempallowdeduct
|
|
450
|
+
WHERE emp_id = ?
|
|
451
|
+
AND company_id = ?
|
|
452
|
+
AND allowdeduct_code IN (${placeholders})
|
|
453
|
+
AND currency_code = ?
|
|
454
|
+
`;
|
|
455
|
+
return auth.connFin.query(query, [empId, auth.userData.companyId, ...compoCodes, currencyCode]);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* OPTIMIZATION: Batch get employee components for multiple employees
|
|
459
|
+
* This reduces database roundtrips when processing multiple employees
|
|
460
|
+
*/
|
|
461
|
+
async getBatchEmployeeComponents(auth, empIds, compoCodes, currencyCode) {
|
|
462
|
+
if (empIds.length === 0 || compoCodes.length === 0)
|
|
463
|
+
return new Map();
|
|
464
|
+
const empPlaceholders = empIds.map(() => '?').join(',');
|
|
465
|
+
const compoPlaceholders = compoCodes.map(() => '?').join(',');
|
|
466
|
+
const query = `
|
|
467
|
+
SELECT emp_id as empId, allowdeduct_code as allowdeductCode, allowdeduct_value as allowdeductValue
|
|
468
|
+
FROM tpydempallowdeduct
|
|
469
|
+
WHERE emp_id IN (${empPlaceholders})
|
|
470
|
+
AND company_id = ?
|
|
471
|
+
AND allowdeduct_code IN (${compoPlaceholders})
|
|
472
|
+
AND currency_code = ?
|
|
473
|
+
`;
|
|
474
|
+
const results = await auth.connFin.query(query, [...empIds, auth.userData.companyId, ...compoCodes, currencyCode]);
|
|
475
|
+
// Group by employee ID
|
|
476
|
+
const grouped = new Map();
|
|
477
|
+
for (const row of results) {
|
|
478
|
+
if (!grouped.has(row.empId)) {
|
|
479
|
+
grouped.set(row.empId, []);
|
|
480
|
+
}
|
|
481
|
+
grouped.get(row.empId).push({
|
|
482
|
+
allowdeductCode: row.allowdeductCode,
|
|
483
|
+
allowdeductValue: row.allowdeductValue,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return grouped;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get employee data with relations
|
|
490
|
+
*/
|
|
491
|
+
async getEmployeeData(auth, empId) {
|
|
492
|
+
const query = `
|
|
493
|
+
SELECT
|
|
494
|
+
ec.emp_id as empId,
|
|
495
|
+
ec.emp_no as empNo,
|
|
496
|
+
ec.start_date as startDate,
|
|
497
|
+
ec.end_date as endDate,
|
|
498
|
+
ec.position_id as positionId,
|
|
499
|
+
ec.cost_code as costCode,
|
|
500
|
+
ec.grade_code as gradeCode,
|
|
501
|
+
ec.work_location_code as workLocationCode,
|
|
502
|
+
ec.job_status_code as jobStatusCode,
|
|
503
|
+
ecg.join_date as joinDate,
|
|
504
|
+
ecg.fulljoin_date as fulljoinDate,
|
|
505
|
+
ecg.permanent_date as permanentDate,
|
|
506
|
+
ecg.pension_date as pensionDate,
|
|
507
|
+
ecg.terminate_date as terminateDate,
|
|
508
|
+
ep.gender as gender,
|
|
509
|
+
edp.birthdate as birthdate,
|
|
510
|
+
edp.maritalstatus as maritalstatus,
|
|
511
|
+
edp.religion_code as religionCode,
|
|
512
|
+
ejs.jobstatusname_en as jobstatusnameEn
|
|
513
|
+
FROM teodempcompany ec
|
|
514
|
+
LEFT JOIN teodempcompanygroup ecg ON ec.emp_id = ecg.emp_id
|
|
515
|
+
LEFT JOIN teomemppersonal ep ON ec.emp_id = ep.emp_id
|
|
516
|
+
LEFT JOIN teodemppersonal edp ON ec.emp_id = edp.emp_id
|
|
517
|
+
LEFT JOIN teomjobstatus ejs ON ec.job_status_code = ejs.jobstatuscode AND ec.company_id = ejs.company_id
|
|
518
|
+
WHERE ec.emp_id = ? AND ec.company_id = ?
|
|
519
|
+
`;
|
|
520
|
+
const results = await auth.connFin.query(query, [empId, auth.userData.companyId]);
|
|
521
|
+
return results[0] || null;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* OPTIMIZATION: Batch get employee data for multiple employees
|
|
525
|
+
* This reduces database roundtrips significantly
|
|
526
|
+
*/
|
|
527
|
+
async getBatchEmployeeData(auth, empIds) {
|
|
528
|
+
if (empIds.length === 0)
|
|
529
|
+
return new Map();
|
|
530
|
+
const placeholders = empIds.map(() => '?').join(',');
|
|
531
|
+
const query = `
|
|
532
|
+
SELECT
|
|
533
|
+
ec.emp_id as empId,
|
|
534
|
+
ec.emp_no as empNo,
|
|
535
|
+
ec.start_date as startDate,
|
|
536
|
+
ec.end_date as endDate,
|
|
537
|
+
ec.position_id as positionId,
|
|
538
|
+
ec.cost_code as costCode,
|
|
539
|
+
ec.grade_code as gradeCode,
|
|
540
|
+
ec.work_location_code as workLocationCode,
|
|
541
|
+
ec.job_status_code as jobStatusCode,
|
|
542
|
+
ecg.join_date as joinDate,
|
|
543
|
+
ecg.fulljoin_date as fulljoinDate,
|
|
544
|
+
ecg.permanent_date as permanentDate,
|
|
545
|
+
ecg.pension_date as pensionDate,
|
|
546
|
+
ecg.terminate_date as terminateDate,
|
|
547
|
+
ep.gender as gender,
|
|
548
|
+
edp.birthdate as birthdate,
|
|
549
|
+
edp.maritalstatus as maritalstatus,
|
|
550
|
+
edp.religion_code as religionCode,
|
|
551
|
+
ejs.jobstatusname_en as jobstatusnameEn
|
|
552
|
+
FROM teodempcompany ec
|
|
553
|
+
LEFT JOIN teodempcompanygroup ecg ON ec.emp_id = ecg.emp_id
|
|
554
|
+
LEFT JOIN teomemppersonal ep ON ec.emp_id = ep.emp_id
|
|
555
|
+
LEFT JOIN teodemppersonal edp ON ec.emp_id = edp.emp_id
|
|
556
|
+
LEFT JOIN teomjobstatus ejs ON ec.job_status_code = ejs.jobstatuscode AND ec.company_id = ejs.company_id
|
|
557
|
+
WHERE ec.emp_id IN (${placeholders}) AND ec.company_id = ?
|
|
558
|
+
`;
|
|
559
|
+
const results = await auth.connFin.query(query, [...empIds, auth.userData.companyId]);
|
|
560
|
+
// Map by employee ID
|
|
561
|
+
const mapped = new Map();
|
|
562
|
+
for (const row of results) {
|
|
563
|
+
mapped.set(row.empId, row);
|
|
564
|
+
}
|
|
565
|
+
return mapped;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get reserved words list for formula processing
|
|
569
|
+
*/
|
|
570
|
+
async getReservedWords(auth) {
|
|
571
|
+
// OPTIMIZATION: Cache reserved words (rarely changes, huge performance boost!)
|
|
572
|
+
const cacheKey = `${auth.userData.companyId}_${auth.userData.taxCountry}`;
|
|
573
|
+
const cached = this.reservedWordsCache.get(cacheKey);
|
|
574
|
+
if (cached && (Date.now() - cached.timestamp < this.CACHE_TTL)) {
|
|
575
|
+
return cached.data;
|
|
576
|
+
}
|
|
577
|
+
const query = `
|
|
578
|
+
SELECT word
|
|
579
|
+
FROM tsfmreserveword
|
|
580
|
+
WHERE (ISNULL(taxcountry) OR taxcountry = '' OR taxcountry = ?)
|
|
581
|
+
AND (company_id = 1 OR company_id = ?)
|
|
582
|
+
AND module IN ('GENERAL', 'PAYROLL')
|
|
583
|
+
AND (category = 'EMPLOYEE' OR category = 'PAYFORMULA' OR category = 'CLAIMFORMULA')
|
|
584
|
+
GROUP BY word
|
|
585
|
+
`;
|
|
586
|
+
const results = await auth.connFin.query(query, [auth.userData.taxCountry, auth.userData.companyId]);
|
|
587
|
+
const words2 = results.map((r) => r.word);
|
|
588
|
+
const result = {
|
|
589
|
+
words2,
|
|
590
|
+
arrayAcr: [],
|
|
591
|
+
claimTypes2: [],
|
|
592
|
+
attStatuses: [],
|
|
593
|
+
leaveTypes: [],
|
|
594
|
+
payvarMaster: [],
|
|
595
|
+
arrayDiscp: [],
|
|
596
|
+
attintfWord: [],
|
|
597
|
+
};
|
|
598
|
+
// Cache the result
|
|
599
|
+
this.reservedWordsCache.set(cacheKey, { data: result, timestamp: Date.now() });
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Extract required words from formula
|
|
604
|
+
*/
|
|
605
|
+
extractRequiredWordsFromFormula(formula, wordList) {
|
|
606
|
+
const required = new Set();
|
|
607
|
+
if (!formula)
|
|
608
|
+
return required;
|
|
609
|
+
for (const word of wordList) {
|
|
610
|
+
if (formula.includes(word)) {
|
|
611
|
+
required.add(word);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return required;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Batch load all required data for formula parsing
|
|
618
|
+
*/
|
|
619
|
+
async batchLoadFormulaData(auth, empData, requiredWords) {
|
|
620
|
+
const cache = {};
|
|
621
|
+
const queries = [];
|
|
622
|
+
const queryMap = [];
|
|
623
|
+
// Position data
|
|
624
|
+
if ((0, helpers_1.needsPositionData)(requiredWords) && empData.positionId) {
|
|
625
|
+
queryMap.push('position');
|
|
626
|
+
queries.push(auth.conn.query(`
|
|
627
|
+
SELECT p.pos_name_en as posNameEn, p.pos_code as posCode,
|
|
628
|
+
d.pos_name_en as deptNameEn, d.pos_code as deptCode
|
|
629
|
+
FROM teomposition p
|
|
630
|
+
LEFT JOIN teomposition d ON p.dept_id = d.position_id AND p.company_id = d.company_id
|
|
631
|
+
WHERE p.position_id = ? AND p.company_id = ?
|
|
632
|
+
`, [empData.positionId, auth.userData.companyId]));
|
|
633
|
+
}
|
|
634
|
+
// Cost Center data
|
|
635
|
+
if ((0, helpers_1.needsCostCenterData)(requiredWords) && empData.costCode) {
|
|
636
|
+
queryMap.push('costCenter');
|
|
637
|
+
queries.push(auth.conn.query(`
|
|
638
|
+
SELECT costcenter_code as costcenterCode, costcenter_name_en as costcenterNameEn
|
|
639
|
+
FROM teomcostcenter
|
|
640
|
+
WHERE costcenter_code = ? AND company_id = ?
|
|
641
|
+
`, [empData.costCode, auth.userData.companyId]));
|
|
642
|
+
}
|
|
643
|
+
// Grade data
|
|
644
|
+
if ((0, helpers_1.needsGradeData)(requiredWords) && empData.gradeCode) {
|
|
645
|
+
queryMap.push('grade');
|
|
646
|
+
queries.push(auth.conn.query(`
|
|
647
|
+
SELECT grade_code as gradeCode, grade_name as gradeName
|
|
648
|
+
FROM teomjobgrade
|
|
649
|
+
WHERE grade_code = ? AND company_id = ?
|
|
650
|
+
`, [empData.gradeCode, auth.userData.companyId]));
|
|
651
|
+
}
|
|
652
|
+
// Work Location data
|
|
653
|
+
if ((0, helpers_1.needsWorkLocationData)(requiredWords) && empData.workLocationCode) {
|
|
654
|
+
queryMap.push('workLocation');
|
|
655
|
+
queries.push(auth.conn.query(`
|
|
656
|
+
SELECT worklocation_code as worklocationCode, worklocation_name as worklocationName
|
|
657
|
+
FROM teomworklocation
|
|
658
|
+
WHERE worklocation_code = ?
|
|
659
|
+
`, [empData.workLocationCode]));
|
|
660
|
+
}
|
|
661
|
+
// Custom Field data
|
|
662
|
+
if ((0, helpers_1.needsCustomFieldData)(requiredWords)) {
|
|
663
|
+
queryMap.push('customField');
|
|
664
|
+
queries.push(auth.conn.query(`
|
|
665
|
+
SELECT customfield1, customfield2, customfield3, customfield4, customfield5,
|
|
666
|
+
customfield6, customfield7, customfield8, customfield9, customfield10
|
|
667
|
+
FROM teodempcustomfield
|
|
668
|
+
WHERE emp_id = ? AND company_code = ?
|
|
669
|
+
`, [empData.empId, auth.userData.companyCode]));
|
|
670
|
+
}
|
|
671
|
+
// Employment Status
|
|
672
|
+
if (requiredWords.has('EMPLOYMENTSTATUS')) {
|
|
673
|
+
queryMap.push('employmentStatus');
|
|
674
|
+
queries.push(auth.connFin.query(`
|
|
675
|
+
SELECT employmentstatus_code as employmentStatusCode
|
|
676
|
+
FROM teodemploymenthistory
|
|
677
|
+
WHERE emp_id = ?
|
|
678
|
+
AND effectivedt = (
|
|
679
|
+
SELECT MAX(effectivedt)
|
|
680
|
+
FROM teodemploymenthistory
|
|
681
|
+
WHERE emp_id = ? AND company_id = ?
|
|
682
|
+
)
|
|
683
|
+
`, [empData.empId, empData.empId, auth.userData.companyId]));
|
|
684
|
+
}
|
|
685
|
+
// Child Dependents
|
|
686
|
+
if (requiredWords.has('CHILDDEPENDENTS')) {
|
|
687
|
+
queryMap.push('childDependents');
|
|
688
|
+
queries.push(auth.conn.query(`
|
|
689
|
+
SELECT COUNT(*) as count
|
|
690
|
+
FROM teodempfamily f
|
|
691
|
+
INNER JOIN teomfamilyrelation r ON f.relationship = r.relationship_code AND r.ischild = 1
|
|
692
|
+
WHERE f.emp_id = ? AND f.dependentsts = 1
|
|
693
|
+
`, [empData.empId]));
|
|
694
|
+
}
|
|
695
|
+
// Execute all queries in parallel
|
|
696
|
+
const results = await Promise.all(queries);
|
|
697
|
+
// Map results back to cache
|
|
698
|
+
results.forEach((result, index) => {
|
|
699
|
+
const key = queryMap[index];
|
|
700
|
+
if (key === 'position' && result[0]) {
|
|
701
|
+
cache.position = {
|
|
702
|
+
posNameEn: result[0].posNameEn,
|
|
703
|
+
posCode: result[0].posCode,
|
|
704
|
+
dept: {
|
|
705
|
+
posNameEn: result[0].deptNameEn,
|
|
706
|
+
posCode: result[0].deptCode,
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
else if (key === 'costCenter' && result[0]) {
|
|
711
|
+
cache.costCenter = result[0];
|
|
712
|
+
}
|
|
713
|
+
else if (key === 'grade' && result[0]) {
|
|
714
|
+
cache.grade = result[0];
|
|
715
|
+
}
|
|
716
|
+
else if (key === 'workLocation' && result[0]) {
|
|
717
|
+
cache.workLocation = result[0];
|
|
718
|
+
}
|
|
719
|
+
else if (key === 'customField' && result[0]) {
|
|
720
|
+
cache.customField = result[0];
|
|
721
|
+
}
|
|
722
|
+
else if (key === 'employmentStatus' && result[0]) {
|
|
723
|
+
cache.employmentStatus = result[0].employmentStatusCode;
|
|
724
|
+
}
|
|
725
|
+
else if (key === 'childDependents' && result[0]) {
|
|
726
|
+
cache.childDependents = result[0].count;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
return cache;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Convert raw employee data to EmployeeData interface
|
|
733
|
+
*/
|
|
734
|
+
convertToEmployeeData(empData) {
|
|
735
|
+
return {
|
|
736
|
+
empId: empData.empId,
|
|
737
|
+
empNo: empData.empNo,
|
|
738
|
+
startDate: empData.startDate,
|
|
739
|
+
endDate: empData.endDate,
|
|
740
|
+
positionId: empData.positionId,
|
|
741
|
+
costCode: empData.costCode,
|
|
742
|
+
gradeCode: empData.gradeCode,
|
|
743
|
+
workLocationCode: empData.workLocationCode,
|
|
744
|
+
jobStatusCode: empData.jobStatusCode,
|
|
745
|
+
personal: {
|
|
746
|
+
birthdate: empData.birthdate,
|
|
747
|
+
maritalstatus: empData.maritalstatus,
|
|
748
|
+
religionCode: empData.religionCode,
|
|
749
|
+
gender: empData.gender,
|
|
750
|
+
},
|
|
751
|
+
companyGroup: {
|
|
752
|
+
joinDate: empData.joinDate,
|
|
753
|
+
fulljoinDate: empData.fulljoinDate,
|
|
754
|
+
permanentDate: empData.permanentDate,
|
|
755
|
+
pensionDate: empData.pensionDate,
|
|
756
|
+
terminateDate: empData.terminateDate,
|
|
757
|
+
},
|
|
758
|
+
jobStatus: {
|
|
759
|
+
jobstatusnameEn: empData.jobstatusnameEn,
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Convert cache to OrganizationalData interface
|
|
765
|
+
*/
|
|
766
|
+
convertToOrganizationalData(cache) {
|
|
767
|
+
return {
|
|
768
|
+
position: cache.position ? {
|
|
769
|
+
posNameEn: cache.position.posNameEn,
|
|
770
|
+
posCode: cache.position.posCode,
|
|
771
|
+
dept: cache.position.dept,
|
|
772
|
+
} : undefined,
|
|
773
|
+
costCenter: cache.costCenter,
|
|
774
|
+
grade: cache.grade,
|
|
775
|
+
workLocation: cache.workLocation,
|
|
776
|
+
customField: cache.customField,
|
|
777
|
+
employmentStatus: cache.employmentStatus,
|
|
778
|
+
childDependents: cache.childDependents,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* OPTIMIZATION: Utility to chunk array for batched processing
|
|
783
|
+
* This prevents memory overflow when processing thousands of items
|
|
784
|
+
*/
|
|
785
|
+
chunkArray(array, chunkSize) {
|
|
786
|
+
const chunks = [];
|
|
787
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
788
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
789
|
+
}
|
|
790
|
+
return chunks;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
exports.PayrollFormulaService = PayrollFormulaService;
|
|
794
|
+
//# sourceMappingURL=payroll-formula.service.js.map
|