@classytic/payroll 2.7.5 → 2.8.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 +333 -323
- package/dist/attendance.calculator-BZcv2iii.d.ts +336 -0
- package/dist/calculators/index.d.ts +3 -299
- package/dist/calculators/index.js +154 -19
- package/dist/calculators/index.js.map +1 -1
- package/dist/core/index.d.ts +321 -0
- package/dist/core/index.js +1962 -0
- package/dist/core/index.js.map +1 -0
- package/dist/{employee-identity-Cq2wo9-2.d.ts → error-helpers-Bm6lMny2.d.ts} +257 -7
- package/dist/{index-DjB72l6e.d.ts → index-BKLkuSAs.d.ts} +248 -132
- package/dist/index.d.ts +418 -658
- package/dist/index.js +1179 -373
- package/dist/index.js.map +1 -1
- package/dist/payroll-states-DBt0XVm-.d.ts +598 -0
- package/dist/{prorating.calculator-C7sdFiG2.d.ts → prorating.calculator-C33fWBQf.d.ts} +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +95 -75
- package/dist/schemas/index.js.map +1 -1
- package/dist/{types-BVDjiVGS.d.ts → types-bZdAJueH.d.ts} +427 -12
- package/dist/utils/index.d.ts +17 -5
- package/dist/utils/index.js +185 -25
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,1962 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { Types } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
// src/core/result.ts
|
|
6
|
+
function ok(value) {
|
|
7
|
+
return { ok: true, value };
|
|
8
|
+
}
|
|
9
|
+
function err(error) {
|
|
10
|
+
return { ok: false, error };
|
|
11
|
+
}
|
|
12
|
+
function isOk(result) {
|
|
13
|
+
return result.ok === true;
|
|
14
|
+
}
|
|
15
|
+
function isErr(result) {
|
|
16
|
+
return result.ok === false;
|
|
17
|
+
}
|
|
18
|
+
function unwrap(result) {
|
|
19
|
+
if (isOk(result)) {
|
|
20
|
+
return result.value;
|
|
21
|
+
}
|
|
22
|
+
throw result.error;
|
|
23
|
+
}
|
|
24
|
+
function unwrapOr(result, defaultValue) {
|
|
25
|
+
if (isOk(result)) {
|
|
26
|
+
return result.value;
|
|
27
|
+
}
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
function unwrapOrElse(result, fn) {
|
|
31
|
+
if (isOk(result)) {
|
|
32
|
+
return result.value;
|
|
33
|
+
}
|
|
34
|
+
return fn(result.error);
|
|
35
|
+
}
|
|
36
|
+
function map(result, fn) {
|
|
37
|
+
if (isOk(result)) {
|
|
38
|
+
return ok(fn(result.value));
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function mapErr(result, fn) {
|
|
43
|
+
if (isErr(result)) {
|
|
44
|
+
return err(fn(result.error));
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function flatMap(result, fn) {
|
|
49
|
+
if (isOk(result)) {
|
|
50
|
+
return fn(result.value);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
async function tryCatch(fn, errorTransform) {
|
|
55
|
+
try {
|
|
56
|
+
const value = await fn();
|
|
57
|
+
return ok(value);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (errorTransform) {
|
|
60
|
+
return err(errorTransform(error));
|
|
61
|
+
}
|
|
62
|
+
return err(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function tryCatchSync(fn, errorTransform) {
|
|
66
|
+
try {
|
|
67
|
+
const value = fn();
|
|
68
|
+
return ok(value);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (errorTransform) {
|
|
71
|
+
return err(errorTransform(error));
|
|
72
|
+
}
|
|
73
|
+
return err(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function all(results) {
|
|
77
|
+
const values = [];
|
|
78
|
+
for (const result of results) {
|
|
79
|
+
if (isErr(result)) {
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
values.push(result.value);
|
|
83
|
+
}
|
|
84
|
+
return ok(values);
|
|
85
|
+
}
|
|
86
|
+
function match(result, handlers) {
|
|
87
|
+
if (isOk(result)) {
|
|
88
|
+
return handlers.ok(result.value);
|
|
89
|
+
}
|
|
90
|
+
return handlers.err(result.error);
|
|
91
|
+
}
|
|
92
|
+
async function fromPromise(promise, errorTransform) {
|
|
93
|
+
return tryCatch(() => promise, errorTransform);
|
|
94
|
+
}
|
|
95
|
+
function fromNullable(value, error) {
|
|
96
|
+
if (value === null || value === void 0) {
|
|
97
|
+
return err(error);
|
|
98
|
+
}
|
|
99
|
+
return ok(value);
|
|
100
|
+
}
|
|
101
|
+
var ResultClass = class _ResultClass {
|
|
102
|
+
constructor(result) {
|
|
103
|
+
this.result = result;
|
|
104
|
+
}
|
|
105
|
+
static ok(value) {
|
|
106
|
+
return new _ResultClass(ok(value));
|
|
107
|
+
}
|
|
108
|
+
static err(error) {
|
|
109
|
+
return new _ResultClass(err(error));
|
|
110
|
+
}
|
|
111
|
+
static async fromAsync(fn, errorTransform) {
|
|
112
|
+
const result = await tryCatch(fn, errorTransform);
|
|
113
|
+
return new _ResultClass(result);
|
|
114
|
+
}
|
|
115
|
+
isOk() {
|
|
116
|
+
return isOk(this.result);
|
|
117
|
+
}
|
|
118
|
+
isErr() {
|
|
119
|
+
return isErr(this.result);
|
|
120
|
+
}
|
|
121
|
+
unwrap() {
|
|
122
|
+
return unwrap(this.result);
|
|
123
|
+
}
|
|
124
|
+
unwrapOr(defaultValue) {
|
|
125
|
+
return unwrapOr(this.result, defaultValue);
|
|
126
|
+
}
|
|
127
|
+
map(fn) {
|
|
128
|
+
return new _ResultClass(map(this.result, fn));
|
|
129
|
+
}
|
|
130
|
+
mapErr(fn) {
|
|
131
|
+
return new _ResultClass(mapErr(this.result, fn));
|
|
132
|
+
}
|
|
133
|
+
flatMap(fn) {
|
|
134
|
+
return new _ResultClass(flatMap(this.result, fn));
|
|
135
|
+
}
|
|
136
|
+
match(handlers) {
|
|
137
|
+
return match(this.result, handlers);
|
|
138
|
+
}
|
|
139
|
+
toResult() {
|
|
140
|
+
return this.result;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var Result = {
|
|
144
|
+
ok,
|
|
145
|
+
err,
|
|
146
|
+
isOk,
|
|
147
|
+
isErr,
|
|
148
|
+
unwrap,
|
|
149
|
+
unwrapOr,
|
|
150
|
+
unwrapOrElse,
|
|
151
|
+
map,
|
|
152
|
+
mapErr,
|
|
153
|
+
flatMap,
|
|
154
|
+
tryCatch,
|
|
155
|
+
tryCatchSync,
|
|
156
|
+
all,
|
|
157
|
+
match,
|
|
158
|
+
fromPromise,
|
|
159
|
+
fromNullable
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/utils/logger.ts
|
|
163
|
+
var createConsoleLogger = () => ({
|
|
164
|
+
info: (message, meta) => {
|
|
165
|
+
if (meta) {
|
|
166
|
+
console.log(`[Payroll] INFO: ${message}`, meta);
|
|
167
|
+
} else {
|
|
168
|
+
console.log(`[Payroll] INFO: ${message}`);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
error: (message, meta) => {
|
|
172
|
+
if (meta) {
|
|
173
|
+
console.error(`[Payroll] ERROR: ${message}`, meta);
|
|
174
|
+
} else {
|
|
175
|
+
console.error(`[Payroll] ERROR: ${message}`);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
warn: (message, meta) => {
|
|
179
|
+
if (meta) {
|
|
180
|
+
console.warn(`[Payroll] WARN: ${message}`, meta);
|
|
181
|
+
} else {
|
|
182
|
+
console.warn(`[Payroll] WARN: ${message}`);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
debug: (message, meta) => {
|
|
186
|
+
if (process.env.NODE_ENV !== "production") {
|
|
187
|
+
if (meta) {
|
|
188
|
+
console.log(`[Payroll] DEBUG: ${message}`, meta);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`[Payroll] DEBUG: ${message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
var currentLogger = createConsoleLogger();
|
|
196
|
+
function getLogger() {
|
|
197
|
+
return {
|
|
198
|
+
info: (message, meta) => {
|
|
199
|
+
currentLogger.info(message, meta);
|
|
200
|
+
},
|
|
201
|
+
error: (message, meta) => {
|
|
202
|
+
currentLogger.error(message, meta);
|
|
203
|
+
},
|
|
204
|
+
warn: (message, meta) => {
|
|
205
|
+
currentLogger.warn(message, meta);
|
|
206
|
+
},
|
|
207
|
+
debug: (message, meta) => {
|
|
208
|
+
currentLogger.debug(message, meta);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/core/events.ts
|
|
214
|
+
var EventBus = class {
|
|
215
|
+
handlers = /* @__PURE__ */ new Map();
|
|
216
|
+
/**
|
|
217
|
+
* Register an event handler
|
|
218
|
+
*/
|
|
219
|
+
on(event, handler) {
|
|
220
|
+
if (!this.handlers.has(event)) {
|
|
221
|
+
this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
222
|
+
}
|
|
223
|
+
this.handlers.get(event).add(handler);
|
|
224
|
+
return () => this.off(event, handler);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Register a one-time event handler
|
|
228
|
+
*/
|
|
229
|
+
once(event, handler) {
|
|
230
|
+
const wrappedHandler = async (payload) => {
|
|
231
|
+
this.off(event, wrappedHandler);
|
|
232
|
+
await handler(payload);
|
|
233
|
+
};
|
|
234
|
+
return this.on(event, wrappedHandler);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Remove an event handler
|
|
238
|
+
*/
|
|
239
|
+
off(event, handler) {
|
|
240
|
+
const eventHandlers = this.handlers.get(event);
|
|
241
|
+
if (eventHandlers) {
|
|
242
|
+
eventHandlers.delete(handler);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Emit an event
|
|
247
|
+
*/
|
|
248
|
+
async emit(event, payload) {
|
|
249
|
+
const eventHandlers = this.handlers.get(event);
|
|
250
|
+
if (!eventHandlers || eventHandlers.size === 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const handlers = Array.from(eventHandlers);
|
|
254
|
+
await Promise.all(
|
|
255
|
+
handlers.map(async (handler) => {
|
|
256
|
+
try {
|
|
257
|
+
await handler(payload);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
getLogger().error(`Event handler error for ${event}`, {
|
|
260
|
+
error: error instanceof Error ? error.message : String(error)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Emit event synchronously (fire-and-forget)
|
|
268
|
+
*/
|
|
269
|
+
emitSync(event, payload) {
|
|
270
|
+
void this.emit(event, payload);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Remove all handlers for an event
|
|
274
|
+
*/
|
|
275
|
+
removeAllListeners(event) {
|
|
276
|
+
if (event) {
|
|
277
|
+
this.handlers.delete(event);
|
|
278
|
+
} else {
|
|
279
|
+
this.handlers.clear();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get listener count for an event
|
|
284
|
+
*/
|
|
285
|
+
listenerCount(event) {
|
|
286
|
+
return this.handlers.get(event)?.size ?? 0;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get all registered events
|
|
290
|
+
*/
|
|
291
|
+
eventNames() {
|
|
292
|
+
return Array.from(this.handlers.keys());
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
var defaultEventBus = null;
|
|
296
|
+
function getEventBus() {
|
|
297
|
+
if (!defaultEventBus) {
|
|
298
|
+
defaultEventBus = new EventBus();
|
|
299
|
+
}
|
|
300
|
+
return defaultEventBus;
|
|
301
|
+
}
|
|
302
|
+
function createEventBus() {
|
|
303
|
+
return new EventBus();
|
|
304
|
+
}
|
|
305
|
+
function resetEventBus() {
|
|
306
|
+
if (defaultEventBus) {
|
|
307
|
+
defaultEventBus.removeAllListeners();
|
|
308
|
+
}
|
|
309
|
+
defaultEventBus = null;
|
|
310
|
+
}
|
|
311
|
+
function onEmployeeHired(handler) {
|
|
312
|
+
return getEventBus().on("employee:hired", handler);
|
|
313
|
+
}
|
|
314
|
+
function onSalaryProcessed(handler) {
|
|
315
|
+
return getEventBus().on("salary:processed", handler);
|
|
316
|
+
}
|
|
317
|
+
function onPayrollCompleted(handler) {
|
|
318
|
+
return getEventBus().on("payroll:completed", handler);
|
|
319
|
+
}
|
|
320
|
+
function onMilestoneAchieved(handler) {
|
|
321
|
+
return getEventBus().on("milestone:achieved", handler);
|
|
322
|
+
}
|
|
323
|
+
var IdempotencyManager = class _IdempotencyManager {
|
|
324
|
+
cache;
|
|
325
|
+
static hasLoggedWarning = false;
|
|
326
|
+
constructor(options = {}) {
|
|
327
|
+
this.cache = new LRUCache({
|
|
328
|
+
max: options.max || 1e4,
|
|
329
|
+
// Store 10k keys
|
|
330
|
+
ttl: options.ttl || 1e3 * 60 * 60 * 24
|
|
331
|
+
// 24 hours default
|
|
332
|
+
});
|
|
333
|
+
if (!options.suppressWarning && !_IdempotencyManager.hasLoggedWarning && process.env.NODE_ENV === "production") {
|
|
334
|
+
_IdempotencyManager.hasLoggedWarning = true;
|
|
335
|
+
getLogger().warn(
|
|
336
|
+
"IdempotencyManager: Using in-memory cache. For horizontal scaling, implement database-backed idempotency. See @classytic/payroll documentation for implementation guidance.",
|
|
337
|
+
{ cacheMax: options.max || 1e4, cacheTTL: options.ttl || 864e5 }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Check if key exists and return cached result
|
|
343
|
+
*/
|
|
344
|
+
get(key) {
|
|
345
|
+
const cached = this.cache.get(key);
|
|
346
|
+
if (!cached) return null;
|
|
347
|
+
return {
|
|
348
|
+
value: cached.value,
|
|
349
|
+
cached: true,
|
|
350
|
+
createdAt: cached.createdAt
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Store result for idempotency key
|
|
355
|
+
*/
|
|
356
|
+
set(key, value) {
|
|
357
|
+
this.cache.set(key, {
|
|
358
|
+
value,
|
|
359
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Execute function with idempotency protection
|
|
364
|
+
*/
|
|
365
|
+
async execute(key, fn) {
|
|
366
|
+
const cached = this.get(key);
|
|
367
|
+
if (cached) {
|
|
368
|
+
return cached;
|
|
369
|
+
}
|
|
370
|
+
const value = await fn();
|
|
371
|
+
this.set(key, value);
|
|
372
|
+
return {
|
|
373
|
+
value,
|
|
374
|
+
cached: false,
|
|
375
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Clear a specific key
|
|
380
|
+
*/
|
|
381
|
+
delete(key) {
|
|
382
|
+
this.cache.delete(key);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Clear all keys
|
|
386
|
+
*/
|
|
387
|
+
clear() {
|
|
388
|
+
this.cache.clear();
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get cache stats
|
|
392
|
+
*/
|
|
393
|
+
stats() {
|
|
394
|
+
return {
|
|
395
|
+
size: this.cache.size,
|
|
396
|
+
max: this.cache.max
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
function generatePayrollIdempotencyKey(organizationId, employeeId, month, year, payrollRunType = "regular", periodStartDate) {
|
|
401
|
+
if (periodStartDate) {
|
|
402
|
+
const startDateStr = periodStartDate.toISOString().split("T")[0];
|
|
403
|
+
return `payroll:${organizationId}:${employeeId}:${year}-${month}:${startDateStr}:${payrollRunType}`;
|
|
404
|
+
}
|
|
405
|
+
return `payroll:${organizationId}:${employeeId}:${year}-${month}:${payrollRunType}`;
|
|
406
|
+
}
|
|
407
|
+
var WebhookManager = class {
|
|
408
|
+
webhooks = [];
|
|
409
|
+
deliveryLog = [];
|
|
410
|
+
maxLogSize;
|
|
411
|
+
storePayloads;
|
|
412
|
+
constructor(options) {
|
|
413
|
+
this.maxLogSize = options?.maxLogSize ?? 1e3;
|
|
414
|
+
this.storePayloads = options?.storePayloads ?? false;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Register a webhook
|
|
418
|
+
*/
|
|
419
|
+
register(config) {
|
|
420
|
+
this.webhooks.push({
|
|
421
|
+
retries: 3,
|
|
422
|
+
timeout: 3e4,
|
|
423
|
+
...config
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Remove a webhook
|
|
428
|
+
*/
|
|
429
|
+
unregister(url) {
|
|
430
|
+
this.webhooks = this.webhooks.filter((w) => w.url !== url);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Send webhook for event
|
|
434
|
+
*/
|
|
435
|
+
async send(event, payload) {
|
|
436
|
+
const matchingWebhooks = this.webhooks.filter((w) => w.events.includes(event));
|
|
437
|
+
const deliveries = matchingWebhooks.map(
|
|
438
|
+
(webhook) => this.deliver(webhook, event, payload)
|
|
439
|
+
);
|
|
440
|
+
await Promise.allSettled(deliveries);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Deliver webhook with retries
|
|
444
|
+
*/
|
|
445
|
+
async deliver(webhook, event, payload) {
|
|
446
|
+
const deliveryId = `${Date.now()}-${Math.random().toString(36)}`;
|
|
447
|
+
const delivery = {
|
|
448
|
+
id: deliveryId,
|
|
449
|
+
event,
|
|
450
|
+
url: webhook.url,
|
|
451
|
+
payload: this.storePayloads ? payload : void 0,
|
|
452
|
+
attempt: 0,
|
|
453
|
+
status: "pending"
|
|
454
|
+
};
|
|
455
|
+
this.deliveryLog.push(delivery);
|
|
456
|
+
this.pruneLog();
|
|
457
|
+
const maxRetries = webhook.retries || 3;
|
|
458
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
459
|
+
delivery.attempt = attempt;
|
|
460
|
+
try {
|
|
461
|
+
const controller = new AbortController();
|
|
462
|
+
const timeout = setTimeout(() => controller.abort(), webhook.timeout || 3e4);
|
|
463
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
464
|
+
const deliveredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
465
|
+
const requestBody = {
|
|
466
|
+
event,
|
|
467
|
+
payload,
|
|
468
|
+
deliveredAt
|
|
469
|
+
};
|
|
470
|
+
const headers = {
|
|
471
|
+
"Content-Type": "application/json",
|
|
472
|
+
"X-Payroll-Event": event,
|
|
473
|
+
"X-Payroll-Delivery": deliveryId,
|
|
474
|
+
"X-Payroll-Timestamp": timestamp.toString(),
|
|
475
|
+
...webhook.headers
|
|
476
|
+
};
|
|
477
|
+
if (webhook.secret) {
|
|
478
|
+
headers["X-Payroll-Signature"] = this.generateSignature(requestBody, webhook.secret, timestamp);
|
|
479
|
+
}
|
|
480
|
+
const response = await fetch(webhook.url, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers,
|
|
483
|
+
body: JSON.stringify(requestBody),
|
|
484
|
+
signal: controller.signal
|
|
485
|
+
});
|
|
486
|
+
clearTimeout(timeout);
|
|
487
|
+
delivery.response = {
|
|
488
|
+
status: response.status,
|
|
489
|
+
body: await response.text()
|
|
490
|
+
};
|
|
491
|
+
delivery.sentAt = /* @__PURE__ */ new Date();
|
|
492
|
+
if (response.ok) {
|
|
493
|
+
delivery.status = "sent";
|
|
494
|
+
return delivery;
|
|
495
|
+
}
|
|
496
|
+
const shouldRetry = response.status >= 500 || response.status === 429 || response.status === 408;
|
|
497
|
+
if (shouldRetry && attempt < maxRetries) {
|
|
498
|
+
const backoff = Math.pow(2, attempt) * 1e3;
|
|
499
|
+
const jitter = Math.random() * 1e3;
|
|
500
|
+
await this.sleep(backoff + jitter);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
delivery.status = "failed";
|
|
504
|
+
delivery.error = `HTTP ${response.status}`;
|
|
505
|
+
return delivery;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
delivery.error = error.message;
|
|
508
|
+
if (attempt < maxRetries) {
|
|
509
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
delivery.status = "failed";
|
|
513
|
+
return delivery;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return delivery;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Generate HMAC-SHA256 signature for webhook (Stripe-style)
|
|
520
|
+
*
|
|
521
|
+
* Format: t=<timestamp>,v1=<hmac_signature>
|
|
522
|
+
*
|
|
523
|
+
* The signed payload is: timestamp.JSON(requestBody)
|
|
524
|
+
* where requestBody = { event, payload, deliveredAt }
|
|
525
|
+
*
|
|
526
|
+
* Consumers should verify:
|
|
527
|
+
* 1. Timestamp is within tolerance (e.g., 5 minutes)
|
|
528
|
+
* 2. HMAC signature matches
|
|
529
|
+
*
|
|
530
|
+
* @example Verify signature (consumer side)
|
|
531
|
+
* ```typescript
|
|
532
|
+
* import crypto from 'crypto';
|
|
533
|
+
*
|
|
534
|
+
* const signature = req.headers['x-payroll-signature'];
|
|
535
|
+
* const timestamp = req.headers['x-payroll-timestamp'];
|
|
536
|
+
* const requestBody = req.body; // { event, payload, deliveredAt }
|
|
537
|
+
*
|
|
538
|
+
* // Check timestamp (replay protection)
|
|
539
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
540
|
+
* if (Math.abs(now - parseInt(timestamp)) > 300) {
|
|
541
|
+
* throw new Error('Signature expired');
|
|
542
|
+
* }
|
|
543
|
+
*
|
|
544
|
+
* // Verify signature
|
|
545
|
+
* const signedPayload = `${timestamp}.${JSON.stringify(requestBody)}`;
|
|
546
|
+
* const expectedSignature = crypto
|
|
547
|
+
* .createHmac('sha256', secret)
|
|
548
|
+
* .update(signedPayload)
|
|
549
|
+
* .digest('hex');
|
|
550
|
+
*
|
|
551
|
+
* const parts = signature.split(',');
|
|
552
|
+
* const providedSignature = parts.find(p => p.startsWith('v1='))?.split('=')[1];
|
|
553
|
+
*
|
|
554
|
+
* if (providedSignature !== expectedSignature) {
|
|
555
|
+
* throw new Error('Invalid signature');
|
|
556
|
+
* }
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
generateSignature(requestBody, secret, timestamp) {
|
|
560
|
+
const data = JSON.stringify(requestBody);
|
|
561
|
+
const signedPayload = `${timestamp}.${data}`;
|
|
562
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
563
|
+
hmac.update(signedPayload);
|
|
564
|
+
const signature = hmac.digest("hex");
|
|
565
|
+
return `t=${timestamp},v1=${signature}`;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Prune delivery log to stay within maxLogSize.
|
|
569
|
+
* Removes oldest entries first.
|
|
570
|
+
*/
|
|
571
|
+
pruneLog() {
|
|
572
|
+
if (this.deliveryLog.length > this.maxLogSize) {
|
|
573
|
+
this.deliveryLog = this.deliveryLog.slice(-this.maxLogSize);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Sleep for ms
|
|
578
|
+
*/
|
|
579
|
+
sleep(ms) {
|
|
580
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Get delivery log
|
|
584
|
+
*/
|
|
585
|
+
getDeliveries(options) {
|
|
586
|
+
let results = this.deliveryLog;
|
|
587
|
+
if (options?.event) {
|
|
588
|
+
results = results.filter((d) => d.event === options.event);
|
|
589
|
+
}
|
|
590
|
+
if (options?.status) {
|
|
591
|
+
results = results.filter((d) => d.status === options.status);
|
|
592
|
+
}
|
|
593
|
+
if (options?.limit) {
|
|
594
|
+
results = results.slice(-options.limit);
|
|
595
|
+
}
|
|
596
|
+
return results;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Clear delivery log
|
|
600
|
+
*/
|
|
601
|
+
clearLog() {
|
|
602
|
+
this.deliveryLog = [];
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Get all registered webhooks
|
|
606
|
+
*/
|
|
607
|
+
getWebhooks() {
|
|
608
|
+
return [...this.webhooks];
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/core/plugin.ts
|
|
613
|
+
var PluginManager = class {
|
|
614
|
+
constructor(context) {
|
|
615
|
+
this.context = context;
|
|
616
|
+
}
|
|
617
|
+
plugins = /* @__PURE__ */ new Map();
|
|
618
|
+
hooks = /* @__PURE__ */ new Map();
|
|
619
|
+
/**
|
|
620
|
+
* Register a plugin
|
|
621
|
+
*/
|
|
622
|
+
async register(plugin) {
|
|
623
|
+
if (this.plugins.has(plugin.name)) {
|
|
624
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
625
|
+
}
|
|
626
|
+
if (plugin.hooks) {
|
|
627
|
+
for (const [hookName, handler] of Object.entries(plugin.hooks)) {
|
|
628
|
+
if (handler) {
|
|
629
|
+
this.addHook(hookName, handler);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (plugin.init) {
|
|
634
|
+
await plugin.init(this.context);
|
|
635
|
+
}
|
|
636
|
+
this.plugins.set(plugin.name, plugin);
|
|
637
|
+
this.context.logger.debug(`Plugin "${plugin.name}" registered`);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Unregister a plugin
|
|
641
|
+
*/
|
|
642
|
+
async unregister(name) {
|
|
643
|
+
const plugin = this.plugins.get(name);
|
|
644
|
+
if (!plugin) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (plugin.destroy) {
|
|
648
|
+
await plugin.destroy();
|
|
649
|
+
}
|
|
650
|
+
this.plugins.delete(name);
|
|
651
|
+
this.context.logger.debug(`Plugin "${name}" unregistered`);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Add a hook handler
|
|
655
|
+
*/
|
|
656
|
+
addHook(hookName, handler) {
|
|
657
|
+
if (!this.hooks.has(hookName)) {
|
|
658
|
+
this.hooks.set(hookName, []);
|
|
659
|
+
}
|
|
660
|
+
this.hooks.get(hookName).push(handler);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Execute hooks for a given event
|
|
664
|
+
*/
|
|
665
|
+
async executeHooks(hookName, ...args) {
|
|
666
|
+
const handlers = this.hooks.get(hookName);
|
|
667
|
+
if (!handlers || handlers.length === 0) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
for (const handler of handlers) {
|
|
671
|
+
try {
|
|
672
|
+
await handler(...args);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
this.context.logger.error(`Hook "${hookName}" error:`, { error });
|
|
675
|
+
const errorHandlers = this.hooks.get("onError");
|
|
676
|
+
if (errorHandlers) {
|
|
677
|
+
for (const errorHandler of errorHandlers) {
|
|
678
|
+
try {
|
|
679
|
+
await errorHandler(error, hookName);
|
|
680
|
+
} catch (handlerError) {
|
|
681
|
+
getLogger().debug("Error handler threw an error", {
|
|
682
|
+
hook: hookName,
|
|
683
|
+
handlerError: handlerError instanceof Error ? handlerError.message : String(handlerError)
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Get registered plugin names
|
|
693
|
+
*/
|
|
694
|
+
getPluginNames() {
|
|
695
|
+
return Array.from(this.plugins.keys());
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Check if plugin is registered
|
|
699
|
+
*/
|
|
700
|
+
hasPlugin(name) {
|
|
701
|
+
return this.plugins.has(name);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
function definePlugin(definition) {
|
|
705
|
+
return definition;
|
|
706
|
+
}
|
|
707
|
+
var loggingPlugin = definePlugin({
|
|
708
|
+
name: "logging",
|
|
709
|
+
version: "1.0.0",
|
|
710
|
+
init: (context) => {
|
|
711
|
+
context.addHook("employee:hired", (payload) => {
|
|
712
|
+
context.logger.info("Employee hired", {
|
|
713
|
+
employeeId: payload.employee.employeeId,
|
|
714
|
+
position: payload.employee.position
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
context.addHook("salary:processed", (payload) => {
|
|
718
|
+
context.logger.info("Salary processed", {
|
|
719
|
+
employeeId: payload.employee.employeeId,
|
|
720
|
+
amount: payload.payroll.netAmount,
|
|
721
|
+
period: payload.payroll.period
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
context.addHook("employee:terminated", (payload) => {
|
|
725
|
+
context.logger.info("Employee terminated", {
|
|
726
|
+
employeeId: payload.employee.employeeId,
|
|
727
|
+
reason: payload.reason
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
},
|
|
731
|
+
hooks: {
|
|
732
|
+
onError: (error, context) => {
|
|
733
|
+
getLogger().error(`[Payroll Error] ${context}:`, { error: error.message });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
var metricsPlugin = definePlugin({
|
|
738
|
+
name: "metrics",
|
|
739
|
+
version: "1.0.0",
|
|
740
|
+
init: (context) => {
|
|
741
|
+
const metrics = {
|
|
742
|
+
employeesHired: 0,
|
|
743
|
+
employeesTerminated: 0,
|
|
744
|
+
salariesProcessed: 0,
|
|
745
|
+
totalPaid: 0,
|
|
746
|
+
errors: 0
|
|
747
|
+
};
|
|
748
|
+
context.addHook("employee:hired", () => {
|
|
749
|
+
metrics.employeesHired++;
|
|
750
|
+
});
|
|
751
|
+
context.addHook("employee:terminated", () => {
|
|
752
|
+
metrics.employeesTerminated++;
|
|
753
|
+
});
|
|
754
|
+
context.addHook("salary:processed", (payload) => {
|
|
755
|
+
metrics.salariesProcessed++;
|
|
756
|
+
metrics.totalPaid += payload.payroll.netAmount;
|
|
757
|
+
});
|
|
758
|
+
context.payroll.metrics = metrics;
|
|
759
|
+
},
|
|
760
|
+
hooks: {
|
|
761
|
+
onError: (error, context) => {
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
function createNotificationPlugin(options) {
|
|
766
|
+
return definePlugin({
|
|
767
|
+
name: "notification",
|
|
768
|
+
version: "1.0.0",
|
|
769
|
+
init: (context) => {
|
|
770
|
+
if (options.onHired) {
|
|
771
|
+
context.addHook("employee:hired", async (payload) => {
|
|
772
|
+
await options.onHired({
|
|
773
|
+
id: payload.employee.id,
|
|
774
|
+
name: payload.employee.position
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (options.onTerminated) {
|
|
779
|
+
context.addHook("employee:terminated", async (payload) => {
|
|
780
|
+
await options.onTerminated({
|
|
781
|
+
id: payload.employee.id,
|
|
782
|
+
name: payload.employee.name
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (options.onSalaryProcessed) {
|
|
787
|
+
context.addHook("salary:processed", async (payload) => {
|
|
788
|
+
await options.onSalaryProcessed({
|
|
789
|
+
employee: {
|
|
790
|
+
id: payload.employee.id,
|
|
791
|
+
name: payload.employee.name
|
|
792
|
+
},
|
|
793
|
+
amount: payload.payroll.netAmount
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
if (options.onMilestone) {
|
|
798
|
+
context.addHook("milestone:achieved", async (payload) => {
|
|
799
|
+
await options.onMilestone({
|
|
800
|
+
employee: {
|
|
801
|
+
id: payload.employee.id,
|
|
802
|
+
name: payload.employee.name
|
|
803
|
+
},
|
|
804
|
+
milestone: payload.milestone.message
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
var notificationPlugin = createNotificationPlugin({});
|
|
812
|
+
function toObjectId(id) {
|
|
813
|
+
if (id instanceof Types.ObjectId) return id;
|
|
814
|
+
return new Types.ObjectId(id);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/core/repository-plugins.ts
|
|
818
|
+
function multiTenantPlugin(organizationId) {
|
|
819
|
+
return {
|
|
820
|
+
name: "multi-tenant",
|
|
821
|
+
apply(repo) {
|
|
822
|
+
if (!organizationId) return;
|
|
823
|
+
const orgId = toObjectId(organizationId);
|
|
824
|
+
repo.on("before:create", async (context) => {
|
|
825
|
+
if (context.data) {
|
|
826
|
+
if (Array.isArray(context.data)) {
|
|
827
|
+
context.data = context.data.map((item) => ({
|
|
828
|
+
...item,
|
|
829
|
+
organizationId: orgId
|
|
830
|
+
// CRITICAL: Force override - never allow caller to set different orgId
|
|
831
|
+
}));
|
|
832
|
+
} else {
|
|
833
|
+
context.data = {
|
|
834
|
+
...context.data,
|
|
835
|
+
organizationId: orgId
|
|
836
|
+
// CRITICAL: Force override - never allow caller to set different orgId
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
repo.on("before:getAll", async (context) => {
|
|
842
|
+
context.filters = {
|
|
843
|
+
...context.filters || {},
|
|
844
|
+
organizationId: orgId
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
repo.on("before:getById", async (context) => {
|
|
848
|
+
context.filters = {
|
|
849
|
+
...context.filters || {},
|
|
850
|
+
organizationId: orgId
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
repo.on("before:getByQuery", async (context) => {
|
|
854
|
+
if (!context.query) {
|
|
855
|
+
context.query = {};
|
|
856
|
+
}
|
|
857
|
+
context.query.organizationId = orgId;
|
|
858
|
+
});
|
|
859
|
+
repo.on("before:update", async (context) => {
|
|
860
|
+
context.filters = {
|
|
861
|
+
...context.filters || {},
|
|
862
|
+
organizationId: orgId
|
|
863
|
+
};
|
|
864
|
+
});
|
|
865
|
+
repo.on("before:delete", async (context) => {
|
|
866
|
+
context.filters = {
|
|
867
|
+
...context.filters || {},
|
|
868
|
+
organizationId: orgId
|
|
869
|
+
};
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/utils/money.ts
|
|
876
|
+
function roundMoney(value, decimals = 2) {
|
|
877
|
+
const multiplier = Math.pow(10, decimals);
|
|
878
|
+
const scaled = value * multiplier;
|
|
879
|
+
const fraction = scaled - Math.floor(scaled);
|
|
880
|
+
if (Math.abs(fraction - 0.5) < 1e-10) {
|
|
881
|
+
const floor = Math.floor(scaled);
|
|
882
|
+
const rounded = floor % 2 === 0 ? floor : floor + 1;
|
|
883
|
+
return rounded / multiplier;
|
|
884
|
+
}
|
|
885
|
+
return Math.round(scaled) / multiplier;
|
|
886
|
+
}
|
|
887
|
+
function percentageOf(amount, percentage, decimals = 2) {
|
|
888
|
+
return roundMoney(amount * percentage / 100, decimals);
|
|
889
|
+
}
|
|
890
|
+
function prorateAmount(amount, ratio, decimals = 2) {
|
|
891
|
+
return roundMoney(amount * ratio, decimals);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/config.ts
|
|
895
|
+
var HRM_CONFIG = {
|
|
896
|
+
dataRetention: {
|
|
897
|
+
/**
|
|
898
|
+
* Default retention period for payroll records in seconds
|
|
899
|
+
*
|
|
900
|
+
* STANDARD APPROACH: expireAt field + configurable TTL index
|
|
901
|
+
*
|
|
902
|
+
* ## How It Works:
|
|
903
|
+
* 1. Set expireAt date on each payroll record
|
|
904
|
+
* 2. Call PayrollRecord.configureRetention() at app startup
|
|
905
|
+
* 3. MongoDB deletes documents when expireAt is reached
|
|
906
|
+
*
|
|
907
|
+
* ## Usage:
|
|
908
|
+
*
|
|
909
|
+
* @example Configure at initialization
|
|
910
|
+
* ```typescript
|
|
911
|
+
* await payroll.init({ ... });
|
|
912
|
+
* await PayrollRecord.configureRetention(0); // 0 = delete when expireAt reached
|
|
913
|
+
* ```
|
|
914
|
+
*
|
|
915
|
+
* @example Set expireAt per record
|
|
916
|
+
* ```typescript
|
|
917
|
+
* const expireAt = PayrollRecord.calculateExpireAt(7); // 7 years
|
|
918
|
+
* await PayrollRecord.updateOne({ _id }, { expireAt });
|
|
919
|
+
* ```
|
|
920
|
+
*
|
|
921
|
+
* ## Jurisdiction Requirements:
|
|
922
|
+
* - USA: 7 years → 220752000 seconds
|
|
923
|
+
* - EU/UK: 6 years → 189216000 seconds
|
|
924
|
+
* - Germany: 10 years → 315360000 seconds
|
|
925
|
+
* - India: 8 years → 252288000 seconds
|
|
926
|
+
*
|
|
927
|
+
* Set to 0 to disable TTL
|
|
928
|
+
*/
|
|
929
|
+
payrollRecordsTTL: 63072e3,
|
|
930
|
+
// 2 years - adjust per jurisdiction
|
|
931
|
+
exportWarningDays: 30,
|
|
932
|
+
archiveBeforeDeletion: true
|
|
933
|
+
},
|
|
934
|
+
payroll: {
|
|
935
|
+
defaultCurrency: "USD",
|
|
936
|
+
allowProRating: true,
|
|
937
|
+
attendanceIntegration: true,
|
|
938
|
+
autoDeductions: true,
|
|
939
|
+
overtimeEnabled: false,
|
|
940
|
+
overtimeMultiplier: 1.5
|
|
941
|
+
},
|
|
942
|
+
salary: {
|
|
943
|
+
minimumWage: 0,
|
|
944
|
+
maximumAllowances: 10,
|
|
945
|
+
maximumDeductions: 10,
|
|
946
|
+
defaultFrequency: "monthly"
|
|
947
|
+
},
|
|
948
|
+
employment: {
|
|
949
|
+
defaultProbationMonths: 3,
|
|
950
|
+
maxProbationMonths: 6,
|
|
951
|
+
allowReHiring: true,
|
|
952
|
+
trackEmploymentHistory: true
|
|
953
|
+
},
|
|
954
|
+
validation: {
|
|
955
|
+
requireBankDetails: false,
|
|
956
|
+
requireUserId: false,
|
|
957
|
+
// Modern: Allow guest employees by default
|
|
958
|
+
identityMode: "employeeId",
|
|
959
|
+
// Modern: Use human-readable IDs as primary
|
|
960
|
+
identityFallbacks: ["email", "userId"]
|
|
961
|
+
// Smart fallback chain
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
var ORG_ROLES = {
|
|
965
|
+
OWNER: {
|
|
966
|
+
key: "owner",
|
|
967
|
+
label: "Owner",
|
|
968
|
+
description: "Full organization access (set by Organization model)"
|
|
969
|
+
},
|
|
970
|
+
MANAGER: {
|
|
971
|
+
key: "manager",
|
|
972
|
+
label: "Manager",
|
|
973
|
+
description: "Management and administrative features"
|
|
974
|
+
},
|
|
975
|
+
TRAINER: {
|
|
976
|
+
key: "trainer",
|
|
977
|
+
label: "Trainer",
|
|
978
|
+
description: "Training and coaching features"
|
|
979
|
+
},
|
|
980
|
+
STAFF: {
|
|
981
|
+
key: "staff",
|
|
982
|
+
label: "Staff",
|
|
983
|
+
description: "General staff access to basic features"
|
|
984
|
+
},
|
|
985
|
+
INTERN: {
|
|
986
|
+
key: "intern",
|
|
987
|
+
label: "Intern",
|
|
988
|
+
description: "Limited access for interns"
|
|
989
|
+
},
|
|
990
|
+
CONSULTANT: {
|
|
991
|
+
key: "consultant",
|
|
992
|
+
label: "Consultant",
|
|
993
|
+
description: "Project-based consultant access"
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
Object.values(ORG_ROLES).map((role) => role.key);
|
|
997
|
+
function getPayPeriodsPerYear(frequency) {
|
|
998
|
+
const periodsMap = {
|
|
999
|
+
monthly: 12,
|
|
1000
|
+
bi_weekly: 26,
|
|
1001
|
+
weekly: 52,
|
|
1002
|
+
daily: 365,
|
|
1003
|
+
hourly: 2080
|
|
1004
|
+
// Assuming 40 hours/week * 52 weeks
|
|
1005
|
+
};
|
|
1006
|
+
return periodsMap[frequency];
|
|
1007
|
+
}
|
|
1008
|
+
function mergeConfig(customConfig) {
|
|
1009
|
+
if (!customConfig) return HRM_CONFIG;
|
|
1010
|
+
return {
|
|
1011
|
+
dataRetention: { ...HRM_CONFIG.dataRetention, ...customConfig.dataRetention },
|
|
1012
|
+
payroll: { ...HRM_CONFIG.payroll, ...customConfig.payroll },
|
|
1013
|
+
salary: { ...HRM_CONFIG.salary, ...customConfig.salary },
|
|
1014
|
+
employment: { ...HRM_CONFIG.employment, ...customConfig.employment },
|
|
1015
|
+
validation: {
|
|
1016
|
+
...HRM_CONFIG.validation,
|
|
1017
|
+
...customConfig.validation,
|
|
1018
|
+
// Ensure fallbacks is always EmployeeIdentityMode[]
|
|
1019
|
+
identityFallbacks: customConfig.validation?.identityFallbacks ?? HRM_CONFIG.validation.identityFallbacks
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/core/container.ts
|
|
1025
|
+
var Container = class {
|
|
1026
|
+
_models = null;
|
|
1027
|
+
_config = HRM_CONFIG;
|
|
1028
|
+
_singleTenant = null;
|
|
1029
|
+
_logger;
|
|
1030
|
+
_initialized = false;
|
|
1031
|
+
constructor() {
|
|
1032
|
+
this._logger = getLogger();
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Initialize container with configuration
|
|
1036
|
+
*/
|
|
1037
|
+
initialize(config) {
|
|
1038
|
+
if (this._initialized) {
|
|
1039
|
+
this._logger.warn("Container already initialized, re-initializing");
|
|
1040
|
+
}
|
|
1041
|
+
this._models = config.models;
|
|
1042
|
+
this._config = mergeConfig(config.config);
|
|
1043
|
+
this._singleTenant = config.singleTenant ?? null;
|
|
1044
|
+
if (config.logger) {
|
|
1045
|
+
this._logger = config.logger;
|
|
1046
|
+
}
|
|
1047
|
+
this._initialized = true;
|
|
1048
|
+
this._logger.info("Container initialized", {
|
|
1049
|
+
hasEmployeeModel: !!this._models.EmployeeModel,
|
|
1050
|
+
hasPayrollRecordModel: !!this._models.PayrollRecordModel,
|
|
1051
|
+
hasTransactionModel: !!this._models.TransactionModel,
|
|
1052
|
+
hasAttendanceModel: !!this._models.AttendanceModel,
|
|
1053
|
+
hasLeaveRequestModel: !!this._models.LeaveRequestModel,
|
|
1054
|
+
hasTaxWithholdingModel: !!this._models.TaxWithholdingModel,
|
|
1055
|
+
isSingleTenant: !!this._singleTenant
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Check if container is initialized
|
|
1060
|
+
*/
|
|
1061
|
+
isInitialized() {
|
|
1062
|
+
return this._initialized;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Reset container (useful for testing)
|
|
1066
|
+
*/
|
|
1067
|
+
reset() {
|
|
1068
|
+
this._models = null;
|
|
1069
|
+
this._config = HRM_CONFIG;
|
|
1070
|
+
this._singleTenant = null;
|
|
1071
|
+
this._initialized = false;
|
|
1072
|
+
this._logger.info("Container reset");
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Ensure container is initialized
|
|
1076
|
+
*/
|
|
1077
|
+
ensureInitialized() {
|
|
1078
|
+
if (!this._initialized || !this._models) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
"Payroll not initialized. Call Payroll.initialize() first."
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Get models container (strongly typed)
|
|
1086
|
+
*/
|
|
1087
|
+
getModels() {
|
|
1088
|
+
this.ensureInitialized();
|
|
1089
|
+
return this._models;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Get Employee model (strongly typed)
|
|
1093
|
+
*/
|
|
1094
|
+
getEmployeeModel() {
|
|
1095
|
+
this.ensureInitialized();
|
|
1096
|
+
return this._models.EmployeeModel;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Get PayrollRecord model (strongly typed)
|
|
1100
|
+
*/
|
|
1101
|
+
getPayrollRecordModel() {
|
|
1102
|
+
this.ensureInitialized();
|
|
1103
|
+
return this._models.PayrollRecordModel;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Get Transaction model (strongly typed)
|
|
1107
|
+
*/
|
|
1108
|
+
getTransactionModel() {
|
|
1109
|
+
this.ensureInitialized();
|
|
1110
|
+
return this._models.TransactionModel;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Get Attendance model (optional, strongly typed)
|
|
1114
|
+
*/
|
|
1115
|
+
getAttendanceModel() {
|
|
1116
|
+
this.ensureInitialized();
|
|
1117
|
+
return this._models.AttendanceModel ?? null;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Get LeaveRequest model (optional, strongly typed)
|
|
1121
|
+
*/
|
|
1122
|
+
getLeaveRequestModel() {
|
|
1123
|
+
this.ensureInitialized();
|
|
1124
|
+
return this._models.LeaveRequestModel ?? null;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Get TaxWithholding model (optional, strongly typed)
|
|
1128
|
+
*/
|
|
1129
|
+
getTaxWithholdingModel() {
|
|
1130
|
+
this.ensureInitialized();
|
|
1131
|
+
return this._models.TaxWithholdingModel ?? null;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Get configuration
|
|
1135
|
+
*/
|
|
1136
|
+
getConfig() {
|
|
1137
|
+
return this._config;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Get specific config section
|
|
1141
|
+
*/
|
|
1142
|
+
getConfigSection(section) {
|
|
1143
|
+
return this._config[section];
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Check if single-tenant mode
|
|
1147
|
+
*/
|
|
1148
|
+
isSingleTenant() {
|
|
1149
|
+
return !!this._singleTenant;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Get single-tenant config
|
|
1153
|
+
*/
|
|
1154
|
+
getSingleTenantConfig() {
|
|
1155
|
+
return this._singleTenant;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Get organization ID (for single-tenant mode)
|
|
1159
|
+
*/
|
|
1160
|
+
getOrganizationId() {
|
|
1161
|
+
if (!this._singleTenant || !this._singleTenant.organizationId) return null;
|
|
1162
|
+
return typeof this._singleTenant.organizationId === "string" ? this._singleTenant.organizationId : this._singleTenant.organizationId.toString();
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Get logger
|
|
1166
|
+
*/
|
|
1167
|
+
getLogger() {
|
|
1168
|
+
return this._logger;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Set logger
|
|
1172
|
+
*/
|
|
1173
|
+
setLogger(logger) {
|
|
1174
|
+
this._logger = logger;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Has attendance integration
|
|
1178
|
+
*/
|
|
1179
|
+
hasAttendanceIntegration() {
|
|
1180
|
+
return !!this._models?.AttendanceModel && this._config.payroll.attendanceIntegration;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Create operation context with defaults
|
|
1184
|
+
*
|
|
1185
|
+
* In single-tenant mode with autoInject enabled (default), automatically
|
|
1186
|
+
* injects the configured organizationId into the context.
|
|
1187
|
+
*
|
|
1188
|
+
* @throws Error if autoInject is enabled but no organizationId is configured
|
|
1189
|
+
*/
|
|
1190
|
+
createOperationContext(overrides) {
|
|
1191
|
+
const context = {};
|
|
1192
|
+
const isSingleTenant2 = !!this._singleTenant;
|
|
1193
|
+
const autoInjectEnabled = isSingleTenant2 && this._singleTenant?.autoInject !== false;
|
|
1194
|
+
if (autoInjectEnabled && !overrides?.organizationId) {
|
|
1195
|
+
const orgId = this.getOrganizationId();
|
|
1196
|
+
if (orgId) {
|
|
1197
|
+
context.organizationId = orgId;
|
|
1198
|
+
} else {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
"Single-tenant mode with autoInject enabled requires organizationId in configuration. Configure it via forSingleTenant({ organizationId: YOUR_ORG_ID }) or provide it explicitly."
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return { ...context, ...overrides };
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
var defaultContainer = null;
|
|
1208
|
+
function getContainer() {
|
|
1209
|
+
if (!defaultContainer) {
|
|
1210
|
+
defaultContainer = new Container();
|
|
1211
|
+
}
|
|
1212
|
+
return defaultContainer;
|
|
1213
|
+
}
|
|
1214
|
+
function initializeContainer(config) {
|
|
1215
|
+
getContainer().initialize(config);
|
|
1216
|
+
}
|
|
1217
|
+
function isContainerInitialized() {
|
|
1218
|
+
return defaultContainer?.isInitialized() ?? false;
|
|
1219
|
+
}
|
|
1220
|
+
function getModels() {
|
|
1221
|
+
return getContainer().getModels();
|
|
1222
|
+
}
|
|
1223
|
+
function getConfig() {
|
|
1224
|
+
return getContainer().getConfig();
|
|
1225
|
+
}
|
|
1226
|
+
function isSingleTenant() {
|
|
1227
|
+
return getContainer().isSingleTenant();
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/utils/date.ts
|
|
1231
|
+
function toUTCDateString(date) {
|
|
1232
|
+
const d = new Date(date);
|
|
1233
|
+
d.setHours(0, 0, 0, 0);
|
|
1234
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1235
|
+
}
|
|
1236
|
+
function isEffectiveForPeriod(item, periodStart, periodEnd) {
|
|
1237
|
+
const effectiveFrom = item.effectiveFrom ? new Date(item.effectiveFrom) : /* @__PURE__ */ new Date(0);
|
|
1238
|
+
const effectiveTo = item.effectiveTo ? new Date(item.effectiveTo) : /* @__PURE__ */ new Date("2099-12-31");
|
|
1239
|
+
return effectiveFrom <= periodEnd && effectiveTo >= periodStart;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/core/config.ts
|
|
1243
|
+
var DEFAULT_WORK_SCHEDULE = {
|
|
1244
|
+
workingDays: [1, 2, 3, 4, 5],
|
|
1245
|
+
// Monday to Friday
|
|
1246
|
+
hoursPerDay: 8
|
|
1247
|
+
};
|
|
1248
|
+
function countWorkingDays(startDate, endDate, options = {}) {
|
|
1249
|
+
const workDays = options.workingDays || DEFAULT_WORK_SCHEDULE.workingDays;
|
|
1250
|
+
const holidaySet = new Set(
|
|
1251
|
+
(options.holidays || []).map((d) => toUTCDateString(d))
|
|
1252
|
+
);
|
|
1253
|
+
let totalDays = 0;
|
|
1254
|
+
let workingDays = 0;
|
|
1255
|
+
let holidays = 0;
|
|
1256
|
+
let weekends = 0;
|
|
1257
|
+
const current = new Date(startDate);
|
|
1258
|
+
current.setHours(0, 0, 0, 0);
|
|
1259
|
+
const end = new Date(endDate);
|
|
1260
|
+
end.setHours(0, 0, 0, 0);
|
|
1261
|
+
while (current <= end) {
|
|
1262
|
+
totalDays++;
|
|
1263
|
+
const isHoliday = holidaySet.has(toUTCDateString(current));
|
|
1264
|
+
const isWorkDay = workDays.includes(current.getDay());
|
|
1265
|
+
if (isHoliday) {
|
|
1266
|
+
holidays++;
|
|
1267
|
+
} else if (isWorkDay) {
|
|
1268
|
+
workingDays++;
|
|
1269
|
+
} else {
|
|
1270
|
+
weekends++;
|
|
1271
|
+
}
|
|
1272
|
+
current.setDate(current.getDate() + 1);
|
|
1273
|
+
}
|
|
1274
|
+
return { totalDays, workingDays, weekends, holidays };
|
|
1275
|
+
}
|
|
1276
|
+
function calculateProration(hireDate, terminationDate, periodStart, periodEnd) {
|
|
1277
|
+
const hire = new Date(hireDate);
|
|
1278
|
+
hire.setHours(0, 0, 0, 0);
|
|
1279
|
+
const term = terminationDate ? new Date(terminationDate) : null;
|
|
1280
|
+
if (term) term.setHours(0, 0, 0, 0);
|
|
1281
|
+
const start = new Date(periodStart);
|
|
1282
|
+
start.setHours(0, 0, 0, 0);
|
|
1283
|
+
const end = new Date(periodEnd);
|
|
1284
|
+
end.setHours(0, 0, 0, 0);
|
|
1285
|
+
if (hire > end || term && term < start) {
|
|
1286
|
+
return { ratio: 0, reason: "not_active", isProrated: true };
|
|
1287
|
+
}
|
|
1288
|
+
const effectiveStart = hire > start ? hire : start;
|
|
1289
|
+
const effectiveEnd = term && term < end ? term : end;
|
|
1290
|
+
const totalDays = Math.ceil((end.getTime() - start.getTime()) / 864e5) + 1;
|
|
1291
|
+
const actualDays = Math.ceil((effectiveEnd.getTime() - effectiveStart.getTime()) / 864e5) + 1;
|
|
1292
|
+
const ratio = Math.min(1, Math.max(0, actualDays / totalDays));
|
|
1293
|
+
const isNewHire = hire > start;
|
|
1294
|
+
const isTermination = term !== null && term < end;
|
|
1295
|
+
let reason = "full";
|
|
1296
|
+
if (isNewHire && isTermination) {
|
|
1297
|
+
reason = "both";
|
|
1298
|
+
} else if (isNewHire) {
|
|
1299
|
+
reason = "new_hire";
|
|
1300
|
+
} else if (isTermination) {
|
|
1301
|
+
reason = "termination";
|
|
1302
|
+
}
|
|
1303
|
+
return { ratio, reason, isProrated: ratio < 1 };
|
|
1304
|
+
}
|
|
1305
|
+
function getPayPeriod(month, year, payDay = 28) {
|
|
1306
|
+
const startDate = new Date(year, month - 1, 1);
|
|
1307
|
+
const endDate = new Date(year, month, 0);
|
|
1308
|
+
const payDate = new Date(year, month - 1, Math.min(payDay, endDate.getDate()));
|
|
1309
|
+
return { startDate, endDate, payDate };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/calculators/attendance.calculator.ts
|
|
1313
|
+
function calculateAttendanceDeduction(input) {
|
|
1314
|
+
const { expectedWorkingDays, actualWorkingDays, dailyRate } = input;
|
|
1315
|
+
const expected = Math.max(0, expectedWorkingDays);
|
|
1316
|
+
const actual = Math.max(0, actualWorkingDays);
|
|
1317
|
+
const rate = Math.max(0, dailyRate);
|
|
1318
|
+
const absentDays = Math.max(0, expected - actual);
|
|
1319
|
+
const deductionAmount = roundMoney(absentDays * rate, 2);
|
|
1320
|
+
return {
|
|
1321
|
+
absentDays,
|
|
1322
|
+
deductionAmount,
|
|
1323
|
+
dailyRate: rate,
|
|
1324
|
+
hasDeduction: deductionAmount > 0
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function calculateDailyRate(monthlySalary, workingDays) {
|
|
1328
|
+
if (workingDays <= 0) return 0;
|
|
1329
|
+
return roundMoney(monthlySalary / workingDays, 2);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/utils/calculation.ts
|
|
1333
|
+
function sumBy(items, getter) {
|
|
1334
|
+
return items.reduce((total, item) => total + getter(item), 0);
|
|
1335
|
+
}
|
|
1336
|
+
function sumAllowances(allowances) {
|
|
1337
|
+
return sumBy(allowances, (a) => a.amount);
|
|
1338
|
+
}
|
|
1339
|
+
function sumDeductions(deductions) {
|
|
1340
|
+
return sumBy(deductions, (d) => d.amount);
|
|
1341
|
+
}
|
|
1342
|
+
function calculateGross(baseAmount, allowances) {
|
|
1343
|
+
return baseAmount + sumAllowances(allowances);
|
|
1344
|
+
}
|
|
1345
|
+
function calculateNet(gross, deductions) {
|
|
1346
|
+
return Math.max(0, gross - sumDeductions(deductions));
|
|
1347
|
+
}
|
|
1348
|
+
function applyTaxBrackets(amount, brackets) {
|
|
1349
|
+
let tax = 0;
|
|
1350
|
+
for (const bracket of brackets) {
|
|
1351
|
+
if (amount > bracket.min) {
|
|
1352
|
+
const taxableAmount = Math.min(amount, bracket.max) - bracket.min;
|
|
1353
|
+
tax += taxableAmount * bracket.rate;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return roundMoney(tax);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/calculators/prorating.calculator.ts
|
|
1360
|
+
function calculateProRating(input) {
|
|
1361
|
+
const { hireDate, terminationDate, periodStart, periodEnd, workingDays, holidays = [] } = input;
|
|
1362
|
+
const hire = new Date(hireDate);
|
|
1363
|
+
const termination = terminationDate ? new Date(terminationDate) : null;
|
|
1364
|
+
const effectiveStart = hire > periodStart ? hire : periodStart;
|
|
1365
|
+
const effectiveEnd = termination && termination < periodEnd ? termination : periodEnd;
|
|
1366
|
+
if (effectiveStart > periodEnd || termination && termination < periodStart) {
|
|
1367
|
+
const periodWorkingDays2 = countWorkingDays(periodStart, periodEnd, { workingDays, holidays }).workingDays;
|
|
1368
|
+
return {
|
|
1369
|
+
isProRated: true,
|
|
1370
|
+
ratio: 0,
|
|
1371
|
+
periodWorkingDays: periodWorkingDays2,
|
|
1372
|
+
effectiveWorkingDays: 0,
|
|
1373
|
+
effectiveStart: periodStart,
|
|
1374
|
+
effectiveEnd: periodStart
|
|
1375
|
+
// Effectively zero days
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
const periodWorkingDays = countWorkingDays(periodStart, periodEnd, { workingDays, holidays }).workingDays;
|
|
1379
|
+
const effectiveWorkingDays = countWorkingDays(effectiveStart, effectiveEnd, { workingDays, holidays }).workingDays;
|
|
1380
|
+
const ratio = periodWorkingDays > 0 ? Math.min(1, Math.max(0, effectiveWorkingDays / periodWorkingDays)) : 0;
|
|
1381
|
+
const isProRated = ratio < 1;
|
|
1382
|
+
return {
|
|
1383
|
+
isProRated,
|
|
1384
|
+
ratio,
|
|
1385
|
+
periodWorkingDays,
|
|
1386
|
+
effectiveWorkingDays,
|
|
1387
|
+
effectiveStart,
|
|
1388
|
+
effectiveEnd
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/calculators/salary.calculator.ts
|
|
1393
|
+
function calculateSalaryBreakdown(input) {
|
|
1394
|
+
const { employee, period, attendance, options = {}, config, taxBrackets, taxOptions, jurisdictionTaxConfig } = input;
|
|
1395
|
+
const comp = employee.compensation;
|
|
1396
|
+
const originalBaseAmount = comp.baseAmount;
|
|
1397
|
+
const proRating = calculateProRatingForSalary(
|
|
1398
|
+
employee.hireDate,
|
|
1399
|
+
employee.terminationDate || null,
|
|
1400
|
+
period.startDate,
|
|
1401
|
+
period.endDate,
|
|
1402
|
+
options,
|
|
1403
|
+
employee.workSchedule
|
|
1404
|
+
);
|
|
1405
|
+
let baseAmount = originalBaseAmount;
|
|
1406
|
+
if (proRating.isProRated && config.allowProRating && !options.skipProration) {
|
|
1407
|
+
baseAmount = prorateAmount(baseAmount, proRating.ratio);
|
|
1408
|
+
}
|
|
1409
|
+
const effectiveAllowances = (comp.allowances || []).filter((a) => isEffectiveForPeriod(a, period.startDate, period.endDate));
|
|
1410
|
+
const effectiveDeductions = (comp.deductions || []).filter((d) => isEffectiveForPeriod(d, period.startDate, period.endDate)).filter((d) => d.auto || d.recurring);
|
|
1411
|
+
const allowances = processAllowances(effectiveAllowances, originalBaseAmount, proRating, config, options.skipProration);
|
|
1412
|
+
const deductions = processDeductions(effectiveDeductions, originalBaseAmount, proRating, config, options.skipProration);
|
|
1413
|
+
if (!options.skipAttendance && config.attendanceIntegration && attendance) {
|
|
1414
|
+
const attendanceDeductionResult = calculateAttendanceDeductionFromData(
|
|
1415
|
+
attendance,
|
|
1416
|
+
baseAmount,
|
|
1417
|
+
proRating.effectiveWorkingDays
|
|
1418
|
+
);
|
|
1419
|
+
if (attendanceDeductionResult.hasDeduction) {
|
|
1420
|
+
deductions.push({
|
|
1421
|
+
type: "absence",
|
|
1422
|
+
amount: attendanceDeductionResult.deductionAmount,
|
|
1423
|
+
description: `Unpaid leave deduction (${attendanceDeductionResult.absentDays} days)`
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const grossSalary = calculateGross(baseAmount, allowances);
|
|
1428
|
+
const taxableAllowances = allowances.filter((a) => a.taxable);
|
|
1429
|
+
let taxableAmount = baseAmount + sumAllowances(taxableAllowances);
|
|
1430
|
+
const preTaxDeductionAmount = calculatePreTaxDeductions(
|
|
1431
|
+
effectiveDeductions,
|
|
1432
|
+
deductions,
|
|
1433
|
+
taxOptions,
|
|
1434
|
+
jurisdictionTaxConfig
|
|
1435
|
+
);
|
|
1436
|
+
taxableAmount = Math.max(0, taxableAmount - preTaxDeductionAmount);
|
|
1437
|
+
const frequency = employee.compensation?.frequency || "monthly";
|
|
1438
|
+
let taxAmount = 0;
|
|
1439
|
+
if (!options.skipTax && taxBrackets.length > 0 && config.autoDeductions) {
|
|
1440
|
+
taxAmount = calculateEnhancedTax(
|
|
1441
|
+
taxableAmount,
|
|
1442
|
+
taxBrackets,
|
|
1443
|
+
taxOptions,
|
|
1444
|
+
jurisdictionTaxConfig,
|
|
1445
|
+
frequency
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
if (taxAmount > 0) {
|
|
1449
|
+
deductions.push({
|
|
1450
|
+
type: "tax",
|
|
1451
|
+
amount: taxAmount,
|
|
1452
|
+
description: "Income tax"
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
const netSalary = calculateNet(grossSalary, deductions);
|
|
1456
|
+
return {
|
|
1457
|
+
baseAmount,
|
|
1458
|
+
allowances,
|
|
1459
|
+
deductions,
|
|
1460
|
+
grossSalary,
|
|
1461
|
+
netSalary,
|
|
1462
|
+
taxableAmount,
|
|
1463
|
+
taxAmount,
|
|
1464
|
+
workingDays: proRating.periodWorkingDays,
|
|
1465
|
+
actualDays: proRating.effectiveWorkingDays,
|
|
1466
|
+
proRatedAmount: proRating.isProRated && !options.skipProration ? baseAmount : 0,
|
|
1467
|
+
attendanceDeduction: attendance ? deductions.find((d) => d.type === "absence")?.amount || 0 : 0
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function calculateProRatingForSalary(hireDate, terminationDate, periodStart, periodEnd, options, employeeWorkSchedule) {
|
|
1471
|
+
const workingDays = options?.workSchedule?.workingDays || employeeWorkSchedule?.workingDays || [1, 2, 3, 4, 5];
|
|
1472
|
+
const holidays = options?.holidays || [];
|
|
1473
|
+
return calculateProRating({
|
|
1474
|
+
hireDate,
|
|
1475
|
+
terminationDate,
|
|
1476
|
+
periodStart,
|
|
1477
|
+
periodEnd,
|
|
1478
|
+
workingDays,
|
|
1479
|
+
holidays
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
function processAllowances(allowances, originalBaseAmount, proRating, config, skipProration) {
|
|
1483
|
+
return allowances.map((a) => {
|
|
1484
|
+
let amount = a.isPercentage && a.value !== void 0 ? percentageOf(originalBaseAmount, a.value) : a.amount;
|
|
1485
|
+
const originalAmount = amount;
|
|
1486
|
+
if (proRating.isProRated && config.allowProRating && !skipProration) {
|
|
1487
|
+
amount = prorateAmount(amount, proRating.ratio);
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
type: a.type,
|
|
1491
|
+
amount,
|
|
1492
|
+
taxable: a.taxable ?? true,
|
|
1493
|
+
originalAmount,
|
|
1494
|
+
isPercentage: a.isPercentage,
|
|
1495
|
+
value: a.value
|
|
1496
|
+
};
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
function processDeductions(deductions, originalBaseAmount, proRating, config, skipProration) {
|
|
1500
|
+
return deductions.map((d) => {
|
|
1501
|
+
let amount = d.isPercentage && d.value !== void 0 ? percentageOf(originalBaseAmount, d.value) : d.amount;
|
|
1502
|
+
const originalAmount = amount;
|
|
1503
|
+
if (proRating.isProRated && config.allowProRating && !skipProration) {
|
|
1504
|
+
amount = prorateAmount(amount, proRating.ratio);
|
|
1505
|
+
}
|
|
1506
|
+
return {
|
|
1507
|
+
type: d.type,
|
|
1508
|
+
amount,
|
|
1509
|
+
description: d.description,
|
|
1510
|
+
originalAmount,
|
|
1511
|
+
isPercentage: d.isPercentage,
|
|
1512
|
+
value: d.value
|
|
1513
|
+
};
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
function calculateAttendanceDeductionFromData(attendance, baseAmount, effectiveWorkingDays) {
|
|
1517
|
+
const expectedDays = attendance.expectedDays ?? effectiveWorkingDays;
|
|
1518
|
+
const actualDays = attendance.actualDays;
|
|
1519
|
+
if (actualDays === void 0) {
|
|
1520
|
+
return { hasDeduction: false, deductionAmount: 0, absentDays: 0 };
|
|
1521
|
+
}
|
|
1522
|
+
const dailyRate = calculateDailyRate(baseAmount, expectedDays);
|
|
1523
|
+
const result = calculateAttendanceDeduction({
|
|
1524
|
+
expectedWorkingDays: expectedDays,
|
|
1525
|
+
actualWorkingDays: actualDays,
|
|
1526
|
+
dailyRate
|
|
1527
|
+
});
|
|
1528
|
+
return {
|
|
1529
|
+
hasDeduction: result.hasDeduction,
|
|
1530
|
+
deductionAmount: result.deductionAmount,
|
|
1531
|
+
absentDays: result.absentDays
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function calculatePreTaxDeductions(effectiveDeductions, processedDeductions, taxOptions, jurisdictionTaxConfig) {
|
|
1535
|
+
let totalPreTax = 0;
|
|
1536
|
+
for (let i = 0; i < effectiveDeductions.length; i++) {
|
|
1537
|
+
const original = effectiveDeductions[i];
|
|
1538
|
+
const processed = processedDeductions[i];
|
|
1539
|
+
if (original.reducesTaxableIncome) {
|
|
1540
|
+
totalPreTax += processed?.amount || 0;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (jurisdictionTaxConfig?.preTaxDeductionTypes?.length) {
|
|
1544
|
+
const preTaxTypes = new Set(jurisdictionTaxConfig.preTaxDeductionTypes);
|
|
1545
|
+
for (let i = 0; i < effectiveDeductions.length; i++) {
|
|
1546
|
+
const original = effectiveDeductions[i];
|
|
1547
|
+
const processed = processedDeductions[i];
|
|
1548
|
+
if (original.reducesTaxableIncome) continue;
|
|
1549
|
+
if (preTaxTypes.has(original.type)) {
|
|
1550
|
+
totalPreTax += processed?.amount || 0;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (taxOptions?.preTaxDeductions?.length) {
|
|
1555
|
+
for (const deduction of taxOptions.preTaxDeductions) {
|
|
1556
|
+
totalPreTax += deduction.amount;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return roundMoney(totalPreTax);
|
|
1560
|
+
}
|
|
1561
|
+
function calculateEnhancedTax(periodTaxable, taxBrackets, taxOptions, jurisdictionTaxConfig, frequency = "monthly") {
|
|
1562
|
+
const periodsPerYear = getPayPeriodsPerYear(frequency);
|
|
1563
|
+
let annualTaxable = periodTaxable * periodsPerYear;
|
|
1564
|
+
const threshold = getApplicableThreshold(taxOptions, jurisdictionTaxConfig);
|
|
1565
|
+
if (threshold > 0) {
|
|
1566
|
+
annualTaxable = Math.max(0, annualTaxable - threshold);
|
|
1567
|
+
}
|
|
1568
|
+
let annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
|
|
1569
|
+
if (taxOptions?.taxCredits?.length && annualTax > 0) {
|
|
1570
|
+
annualTax = applyTaxCredits(annualTax, taxOptions.taxCredits);
|
|
1571
|
+
}
|
|
1572
|
+
return roundMoney(annualTax / periodsPerYear);
|
|
1573
|
+
}
|
|
1574
|
+
function getApplicableThreshold(taxOptions, jurisdictionTaxConfig) {
|
|
1575
|
+
if (taxOptions?.standardDeductionOverride !== void 0) {
|
|
1576
|
+
return taxOptions.standardDeductionOverride;
|
|
1577
|
+
}
|
|
1578
|
+
if (taxOptions?.taxpayerCategory) {
|
|
1579
|
+
const category = taxOptions.taxpayerCategory;
|
|
1580
|
+
if (taxOptions.thresholdOverrides?.[category] !== void 0) {
|
|
1581
|
+
return taxOptions.thresholdOverrides[category];
|
|
1582
|
+
}
|
|
1583
|
+
if (jurisdictionTaxConfig?.thresholdsByCategory?.[category] !== void 0) {
|
|
1584
|
+
return jurisdictionTaxConfig.thresholdsByCategory[category];
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (taxOptions?.applyStandardDeduction && jurisdictionTaxConfig?.standardDeduction) {
|
|
1588
|
+
return jurisdictionTaxConfig.standardDeduction;
|
|
1589
|
+
}
|
|
1590
|
+
return 0;
|
|
1591
|
+
}
|
|
1592
|
+
function applyTaxCredits(annualTax, taxCredits) {
|
|
1593
|
+
let remainingTax = annualTax;
|
|
1594
|
+
for (const credit of taxCredits) {
|
|
1595
|
+
if (remainingTax <= 0) break;
|
|
1596
|
+
let creditAmount = credit.amount;
|
|
1597
|
+
if (credit.maxPercent !== void 0 && credit.maxPercent > 0) {
|
|
1598
|
+
const maxCredit = annualTax * credit.maxPercent;
|
|
1599
|
+
creditAmount = Math.min(creditAmount, maxCredit);
|
|
1600
|
+
}
|
|
1601
|
+
creditAmount = Math.min(creditAmount, remainingTax);
|
|
1602
|
+
remainingTax -= creditAmount;
|
|
1603
|
+
}
|
|
1604
|
+
return Math.max(0, remainingTax);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/core/state-machine.ts
|
|
1608
|
+
var StateMachine = class {
|
|
1609
|
+
constructor(config) {
|
|
1610
|
+
this.config = config;
|
|
1611
|
+
this.validTransitions = /* @__PURE__ */ new Map();
|
|
1612
|
+
for (const state of config.states) {
|
|
1613
|
+
this.validTransitions.set(state, /* @__PURE__ */ new Set());
|
|
1614
|
+
}
|
|
1615
|
+
for (const transition of config.transitions) {
|
|
1616
|
+
const fromStates = Array.isArray(transition.from) ? transition.from : [transition.from];
|
|
1617
|
+
for (const from of fromStates) {
|
|
1618
|
+
this.validTransitions.get(from)?.add(transition.to);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
this.terminalStates = new Set(config.terminal || []);
|
|
1622
|
+
}
|
|
1623
|
+
validTransitions;
|
|
1624
|
+
terminalStates;
|
|
1625
|
+
/**
|
|
1626
|
+
* Get the initial state
|
|
1627
|
+
*/
|
|
1628
|
+
get initial() {
|
|
1629
|
+
return this.config.initial;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Get all valid states
|
|
1633
|
+
*/
|
|
1634
|
+
get states() {
|
|
1635
|
+
return this.config.states;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Check if a state is valid
|
|
1639
|
+
*/
|
|
1640
|
+
isValidState(state) {
|
|
1641
|
+
return this.config.states.includes(state);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Check if a state is terminal (no outgoing transitions)
|
|
1645
|
+
*/
|
|
1646
|
+
isTerminal(state) {
|
|
1647
|
+
return this.terminalStates.has(state);
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Check if transition from one state to another is valid
|
|
1651
|
+
*/
|
|
1652
|
+
canTransition(from, to) {
|
|
1653
|
+
return this.validTransitions.get(from)?.has(to) ?? false;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Get all valid next states from current state
|
|
1657
|
+
*/
|
|
1658
|
+
getNextStates(from) {
|
|
1659
|
+
return Array.from(this.validTransitions.get(from) || []);
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Validate a transition and return result
|
|
1663
|
+
*/
|
|
1664
|
+
validateTransition(from, to) {
|
|
1665
|
+
if (!this.isValidState(from)) {
|
|
1666
|
+
return {
|
|
1667
|
+
success: false,
|
|
1668
|
+
from,
|
|
1669
|
+
to,
|
|
1670
|
+
error: `Invalid current state: '${from}'`
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
if (!this.isValidState(to)) {
|
|
1674
|
+
return {
|
|
1675
|
+
success: false,
|
|
1676
|
+
from,
|
|
1677
|
+
to,
|
|
1678
|
+
error: `Invalid target state: '${to}'`
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
if (this.isTerminal(from)) {
|
|
1682
|
+
return {
|
|
1683
|
+
success: false,
|
|
1684
|
+
from,
|
|
1685
|
+
to,
|
|
1686
|
+
error: `Cannot transition from terminal state '${from}'`
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
if (!this.canTransition(from, to)) {
|
|
1690
|
+
const validNext = this.getNextStates(from);
|
|
1691
|
+
return {
|
|
1692
|
+
success: false,
|
|
1693
|
+
from,
|
|
1694
|
+
to,
|
|
1695
|
+
error: `Invalid transition: '${from}' \u2192 '${to}'. Valid transitions from '${from}': [${validNext.join(", ")}]`
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
return { success: true, from, to };
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Assert a transition is valid, throw if not
|
|
1702
|
+
*/
|
|
1703
|
+
assertTransition(from, to) {
|
|
1704
|
+
const result = this.validateTransition(from, to);
|
|
1705
|
+
if (!result.success) {
|
|
1706
|
+
throw new Error(result.error);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
function createStateMachine(config) {
|
|
1711
|
+
return new StateMachine(config);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/core/payroll-states.ts
|
|
1715
|
+
var PayrollStatusMachine = createStateMachine({
|
|
1716
|
+
states: ["pending", "processing", "paid", "failed", "voided", "reversed"],
|
|
1717
|
+
initial: "pending",
|
|
1718
|
+
transitions: [
|
|
1719
|
+
// Normal flow
|
|
1720
|
+
{ from: "pending", to: "processing" },
|
|
1721
|
+
{ from: "processing", to: "paid" },
|
|
1722
|
+
// Direct payment (skip processing for single salary)
|
|
1723
|
+
{ from: "pending", to: "paid" },
|
|
1724
|
+
// Failure handling
|
|
1725
|
+
{ from: "processing", to: "failed" },
|
|
1726
|
+
{ from: "failed", to: "pending" },
|
|
1727
|
+
// Retry
|
|
1728
|
+
// Void (unpaid only - pending, processing, or failed)
|
|
1729
|
+
{ from: ["pending", "processing", "failed"], to: "voided" },
|
|
1730
|
+
// Reversal (paid only)
|
|
1731
|
+
{ from: "paid", to: "reversed" },
|
|
1732
|
+
// Restore voided (back to pending for re-processing)
|
|
1733
|
+
{ from: "voided", to: "pending" }
|
|
1734
|
+
],
|
|
1735
|
+
terminal: ["reversed"]
|
|
1736
|
+
// Only reversed is truly terminal
|
|
1737
|
+
});
|
|
1738
|
+
var TaxStatusMachine = createStateMachine({
|
|
1739
|
+
states: ["pending", "submitted", "paid", "cancelled"],
|
|
1740
|
+
initial: "pending",
|
|
1741
|
+
transitions: [
|
|
1742
|
+
{ from: "pending", to: "submitted" },
|
|
1743
|
+
{ from: "submitted", to: "paid" },
|
|
1744
|
+
// Direct payment (some jurisdictions)
|
|
1745
|
+
{ from: "pending", to: "paid" },
|
|
1746
|
+
// Cancellation (from any non-terminal state)
|
|
1747
|
+
{ from: ["pending", "submitted"], to: "cancelled" }
|
|
1748
|
+
],
|
|
1749
|
+
terminal: ["paid", "cancelled"]
|
|
1750
|
+
});
|
|
1751
|
+
var LeaveRequestStatusMachine = createStateMachine({
|
|
1752
|
+
states: ["pending", "approved", "rejected", "cancelled"],
|
|
1753
|
+
initial: "pending",
|
|
1754
|
+
transitions: [
|
|
1755
|
+
{ from: "pending", to: "approved" },
|
|
1756
|
+
{ from: "pending", to: "rejected" },
|
|
1757
|
+
{ from: "pending", to: "cancelled" },
|
|
1758
|
+
// Cancel approved leave (before it starts)
|
|
1759
|
+
{ from: "approved", to: "cancelled" }
|
|
1760
|
+
],
|
|
1761
|
+
terminal: ["rejected", "cancelled"]
|
|
1762
|
+
});
|
|
1763
|
+
var EmployeeStatusMachine = createStateMachine({
|
|
1764
|
+
states: ["active", "on_leave", "suspended", "terminated"],
|
|
1765
|
+
initial: "active",
|
|
1766
|
+
transitions: [
|
|
1767
|
+
// Leave management
|
|
1768
|
+
{ from: "active", to: "on_leave" },
|
|
1769
|
+
{ from: "on_leave", to: "active" },
|
|
1770
|
+
// Suspension
|
|
1771
|
+
{ from: ["active", "on_leave"], to: "suspended" },
|
|
1772
|
+
{ from: "suspended", to: "active" },
|
|
1773
|
+
// Termination (from any state)
|
|
1774
|
+
{ from: ["active", "on_leave", "suspended"], to: "terminated" },
|
|
1775
|
+
// Re-hire (back to active)
|
|
1776
|
+
{ from: "terminated", to: "active" }
|
|
1777
|
+
],
|
|
1778
|
+
terminal: []
|
|
1779
|
+
// No terminal states (re-hire possible)
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// src/core/timeline-audit.ts
|
|
1783
|
+
var PAYROLL_EVENTS = {
|
|
1784
|
+
/**
|
|
1785
|
+
* Employee lifecycle events
|
|
1786
|
+
*/
|
|
1787
|
+
EMPLOYEE: {
|
|
1788
|
+
/** Employee was hired */
|
|
1789
|
+
HIRED: "employee.hired",
|
|
1790
|
+
/** Employee was terminated */
|
|
1791
|
+
TERMINATED: "employee.terminated",
|
|
1792
|
+
/** Employee was re-hired after termination */
|
|
1793
|
+
REHIRED: "employee.rehired",
|
|
1794
|
+
/** Employee status changed (active, on_leave, suspended) */
|
|
1795
|
+
STATUS_CHANGED: "employee.status_changed",
|
|
1796
|
+
/** Employee department/position changed */
|
|
1797
|
+
ROLE_CHANGED: "employee.role_changed",
|
|
1798
|
+
/** Employee probation ended */
|
|
1799
|
+
PROBATION_ENDED: "employee.probation_ended",
|
|
1800
|
+
/** Recommended event limits for employee timeline */
|
|
1801
|
+
limits: {
|
|
1802
|
+
"employee.status_changed": 50,
|
|
1803
|
+
"employee.role_changed": 20
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
/**
|
|
1807
|
+
* Compensation events
|
|
1808
|
+
*/
|
|
1809
|
+
COMPENSATION: {
|
|
1810
|
+
/** Base salary was updated */
|
|
1811
|
+
SALARY_UPDATED: "compensation.salary_updated",
|
|
1812
|
+
/** Allowance was added */
|
|
1813
|
+
ALLOWANCE_ADDED: "compensation.allowance_added",
|
|
1814
|
+
/** Allowance was removed */
|
|
1815
|
+
ALLOWANCE_REMOVED: "compensation.allowance_removed",
|
|
1816
|
+
/** Deduction was added */
|
|
1817
|
+
DEDUCTION_ADDED: "compensation.deduction_added",
|
|
1818
|
+
/** Deduction was removed */
|
|
1819
|
+
DEDUCTION_REMOVED: "compensation.deduction_removed",
|
|
1820
|
+
/** Bank details were updated */
|
|
1821
|
+
BANK_UPDATED: "compensation.bank_updated",
|
|
1822
|
+
/** Recommended event limits */
|
|
1823
|
+
limits: {
|
|
1824
|
+
"compensation.salary_updated": 24,
|
|
1825
|
+
// 2 years of monthly updates
|
|
1826
|
+
"compensation.allowance_added": 20,
|
|
1827
|
+
"compensation.allowance_removed": 20,
|
|
1828
|
+
"compensation.deduction_added": 20,
|
|
1829
|
+
"compensation.deduction_removed": 20,
|
|
1830
|
+
"compensation.bank_updated": 10
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
/**
|
|
1834
|
+
* Payroll processing events
|
|
1835
|
+
*/
|
|
1836
|
+
PAYROLL: {
|
|
1837
|
+
/** Salary was processed */
|
|
1838
|
+
PROCESSED: "payroll.processed",
|
|
1839
|
+
/** Payroll was voided (before payment) */
|
|
1840
|
+
VOIDED: "payroll.voided",
|
|
1841
|
+
/** Payroll was reversed (after payment) */
|
|
1842
|
+
REVERSED: "payroll.reversed",
|
|
1843
|
+
/** Payroll was restored from voided state */
|
|
1844
|
+
RESTORED: "payroll.restored",
|
|
1845
|
+
/** Payroll export was generated */
|
|
1846
|
+
EXPORTED: "payroll.exported",
|
|
1847
|
+
/** Recommended event limits */
|
|
1848
|
+
limits: {
|
|
1849
|
+
"payroll.processed": 36,
|
|
1850
|
+
// 3 years of monthly payroll
|
|
1851
|
+
"payroll.voided": 10,
|
|
1852
|
+
"payroll.reversed": 10,
|
|
1853
|
+
"payroll.restored": 5,
|
|
1854
|
+
"payroll.exported": 20
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
/**
|
|
1858
|
+
* Tax withholding events
|
|
1859
|
+
*/
|
|
1860
|
+
TAX: {
|
|
1861
|
+
/** Tax was withheld */
|
|
1862
|
+
WITHHELD: "tax.withheld",
|
|
1863
|
+
/** Tax was submitted to authorities */
|
|
1864
|
+
SUBMITTED: "tax.submitted",
|
|
1865
|
+
/** Tax payment was made */
|
|
1866
|
+
PAID: "tax.paid",
|
|
1867
|
+
/** Tax withholding was cancelled */
|
|
1868
|
+
CANCELLED: "tax.cancelled",
|
|
1869
|
+
/** Recommended event limits */
|
|
1870
|
+
limits: {
|
|
1871
|
+
"tax.withheld": 36,
|
|
1872
|
+
"tax.submitted": 12,
|
|
1873
|
+
"tax.paid": 12,
|
|
1874
|
+
"tax.cancelled": 10
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
/**
|
|
1878
|
+
* Leave management events
|
|
1879
|
+
*/
|
|
1880
|
+
LEAVE: {
|
|
1881
|
+
/** Leave was requested */
|
|
1882
|
+
REQUESTED: "leave.requested",
|
|
1883
|
+
/** Leave was approved */
|
|
1884
|
+
APPROVED: "leave.approved",
|
|
1885
|
+
/** Leave was rejected */
|
|
1886
|
+
REJECTED: "leave.rejected",
|
|
1887
|
+
/** Leave was cancelled */
|
|
1888
|
+
CANCELLED: "leave.cancelled",
|
|
1889
|
+
/** Leave balance was accrued */
|
|
1890
|
+
ACCRUED: "leave.accrued",
|
|
1891
|
+
/** Annual leave was reset */
|
|
1892
|
+
RESET: "leave.reset",
|
|
1893
|
+
/** Recommended event limits */
|
|
1894
|
+
limits: {
|
|
1895
|
+
"leave.requested": 50,
|
|
1896
|
+
"leave.approved": 50,
|
|
1897
|
+
"leave.rejected": 20,
|
|
1898
|
+
"leave.cancelled": 20,
|
|
1899
|
+
"leave.accrued": 12,
|
|
1900
|
+
"leave.reset": 5
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
var EMPLOYEE_TIMELINE_CONFIG = {
|
|
1905
|
+
ownerField: "organizationId",
|
|
1906
|
+
fieldName: "timeline",
|
|
1907
|
+
hideByDefault: true,
|
|
1908
|
+
// Don't include timeline in normal queries
|
|
1909
|
+
eventLimits: {
|
|
1910
|
+
...PAYROLL_EVENTS.EMPLOYEE.limits,
|
|
1911
|
+
...PAYROLL_EVENTS.COMPENSATION.limits
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
var PAYROLL_RECORD_TIMELINE_CONFIG = {
|
|
1915
|
+
ownerField: "organizationId",
|
|
1916
|
+
fieldName: "timeline",
|
|
1917
|
+
hideByDefault: true,
|
|
1918
|
+
eventLimits: PAYROLL_EVENTS.PAYROLL.limits
|
|
1919
|
+
};
|
|
1920
|
+
var LEAVE_REQUEST_TIMELINE_CONFIG = {
|
|
1921
|
+
ownerField: "organizationId",
|
|
1922
|
+
fieldName: "timeline",
|
|
1923
|
+
hideByDefault: true,
|
|
1924
|
+
eventLimits: PAYROLL_EVENTS.LEAVE.limits
|
|
1925
|
+
};
|
|
1926
|
+
function buildTimelineMetadata(context) {
|
|
1927
|
+
if (!context) return {};
|
|
1928
|
+
const result = {};
|
|
1929
|
+
if (context.userId) {
|
|
1930
|
+
result.performedByUserId = String(context.userId);
|
|
1931
|
+
}
|
|
1932
|
+
if (context.userName) {
|
|
1933
|
+
result.performedByName = context.userName;
|
|
1934
|
+
}
|
|
1935
|
+
if (context.userRole) {
|
|
1936
|
+
result.performedByRole = context.userRole;
|
|
1937
|
+
}
|
|
1938
|
+
if (context.organizationId) {
|
|
1939
|
+
result.organizationId = String(context.organizationId);
|
|
1940
|
+
}
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
function buildRequestContext(request) {
|
|
1944
|
+
if (!request) return void 0;
|
|
1945
|
+
const getHeader = (name) => {
|
|
1946
|
+
if (request.get) return request.get(name);
|
|
1947
|
+
if (request.headers) {
|
|
1948
|
+
const value = request.headers[name.toLowerCase()];
|
|
1949
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1950
|
+
}
|
|
1951
|
+
return void 0;
|
|
1952
|
+
};
|
|
1953
|
+
return {
|
|
1954
|
+
ip: request.ip || getHeader("x-forwarded-for"),
|
|
1955
|
+
userAgent: getHeader("user-agent"),
|
|
1956
|
+
origin: getHeader("origin")
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
export { Container, DEFAULT_WORK_SCHEDULE, EMPLOYEE_TIMELINE_CONFIG, EmployeeStatusMachine, EventBus, IdempotencyManager, LEAVE_REQUEST_TIMELINE_CONFIG, LeaveRequestStatusMachine, PAYROLL_EVENTS, PAYROLL_RECORD_TIMELINE_CONFIG, PayrollStatusMachine, PluginManager, Result, ResultClass, StateMachine, TaxStatusMachine, WebhookManager, all, buildRequestContext, buildTimelineMetadata, calculateAttendanceDeduction, calculateProration, calculateSalaryBreakdown, countWorkingDays, createEventBus, createNotificationPlugin, createStateMachine, definePlugin, err, flatMap, fromNullable, fromPromise, generatePayrollIdempotencyKey, getConfig, getContainer, getEventBus, getModels, getPayPeriod, initializeContainer, isContainerInitialized, isErr, isOk, isSingleTenant, loggingPlugin, map, mapErr, match, metricsPlugin, multiTenantPlugin, notificationPlugin, ok, onEmployeeHired, onMilestoneAchieved, onPayrollCompleted, onSalaryProcessed, resetEventBus, tryCatch, tryCatchSync, unwrap, unwrapOr, unwrapOrElse };
|
|
1961
|
+
//# sourceMappingURL=index.js.map
|
|
1962
|
+
//# sourceMappingURL=index.js.map
|