@classytic/payroll 1.0.2 → 2.3.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.
Potentially problematic release.
This version of @classytic/payroll might be problematic. Click here for more details.
- package/README.md +2599 -574
- package/dist/calculators/index.d.ts +433 -0
- package/dist/calculators/index.js +283 -0
- package/dist/calculators/index.js.map +1 -0
- package/dist/core/index.d.ts +314 -0
- package/dist/core/index.js +1166 -0
- package/dist/core/index.js.map +1 -0
- package/dist/employee-identity-DXhgOgXE.d.ts +473 -0
- package/dist/employee.factory-BlZqhiCk.d.ts +189 -0
- package/dist/idempotency-Cw2CWicb.d.ts +52 -0
- package/dist/index.d.ts +902 -0
- package/dist/index.js +9108 -0
- package/dist/index.js.map +1 -0
- package/dist/jurisdiction/index.d.ts +660 -0
- package/dist/jurisdiction/index.js +533 -0
- package/dist/jurisdiction/index.js.map +1 -0
- package/dist/payroll.d.ts +429 -0
- package/dist/payroll.js +5192 -0
- package/dist/payroll.js.map +1 -0
- package/dist/schemas/index.d.ts +3262 -0
- package/dist/schemas/index.js +780 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/services/index.d.ts +582 -0
- package/dist/services/index.js +2172 -0
- package/dist/services/index.js.map +1 -0
- package/dist/shift-compliance/index.d.ts +1171 -0
- package/dist/shift-compliance/index.js +1479 -0
- package/dist/shift-compliance/index.js.map +1 -0
- package/dist/types-BN3K_Uhr.d.ts +1842 -0
- package/dist/utils/index.d.ts +893 -0
- package/dist/utils/index.js +1515 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +72 -37
- package/dist/types/config.d.ts +0 -162
- package/dist/types/core/compensation.manager.d.ts +0 -54
- package/dist/types/core/employment.manager.d.ts +0 -49
- package/dist/types/core/payroll.manager.d.ts +0 -60
- package/dist/types/enums.d.ts +0 -117
- package/dist/types/factories/compensation.factory.d.ts +0 -196
- package/dist/types/factories/employee.factory.d.ts +0 -149
- package/dist/types/factories/payroll.factory.d.ts +0 -319
- package/dist/types/hrm.orchestrator.d.ts +0 -47
- package/dist/types/index.d.ts +0 -20
- package/dist/types/init.d.ts +0 -30
- package/dist/types/models/payroll-record.model.d.ts +0 -3
- package/dist/types/plugins/employee.plugin.d.ts +0 -2
- package/dist/types/schemas/employment.schema.d.ts +0 -959
- package/dist/types/services/compensation.service.d.ts +0 -94
- package/dist/types/services/employee.service.d.ts +0 -28
- package/dist/types/services/payroll.service.d.ts +0 -30
- package/dist/types/utils/calculation.utils.d.ts +0 -26
- package/dist/types/utils/date.utils.d.ts +0 -35
- package/dist/types/utils/logger.d.ts +0 -12
- package/dist/types/utils/query-builders.d.ts +0 -83
- package/dist/types/utils/validation.utils.d.ts +0 -33
- package/payroll.d.ts +0 -241
- package/src/config.js +0 -177
- package/src/core/compensation.manager.js +0 -242
- package/src/core/employment.manager.js +0 -224
- package/src/core/payroll.manager.js +0 -499
- package/src/enums.js +0 -141
- package/src/factories/compensation.factory.js +0 -198
- package/src/factories/employee.factory.js +0 -173
- package/src/factories/payroll.factory.js +0 -413
- package/src/hrm.orchestrator.js +0 -139
- package/src/index.js +0 -172
- package/src/init.js +0 -62
- package/src/models/payroll-record.model.js +0 -126
- package/src/plugins/employee.plugin.js +0 -164
- package/src/schemas/employment.schema.js +0 -126
- package/src/services/compensation.service.js +0 -231
- package/src/services/employee.service.js +0 -162
- package/src/services/payroll.service.js +0 -213
- package/src/utils/calculation.utils.js +0 -91
- package/src/utils/date.utils.js +0 -120
- package/src/utils/logger.js +0 -36
- package/src/utils/query-builders.js +0 -185
- package/src/utils/validation.utils.js +0 -122
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
|
|
3
|
+
// src/core/result.ts
|
|
4
|
+
function ok(value) {
|
|
5
|
+
return { ok: true, value };
|
|
6
|
+
}
|
|
7
|
+
function err(error) {
|
|
8
|
+
return { ok: false, error };
|
|
9
|
+
}
|
|
10
|
+
function isOk(result) {
|
|
11
|
+
return result.ok === true;
|
|
12
|
+
}
|
|
13
|
+
function isErr(result) {
|
|
14
|
+
return result.ok === false;
|
|
15
|
+
}
|
|
16
|
+
function unwrap(result) {
|
|
17
|
+
if (isOk(result)) {
|
|
18
|
+
return result.value;
|
|
19
|
+
}
|
|
20
|
+
throw result.error;
|
|
21
|
+
}
|
|
22
|
+
function unwrapOr(result, defaultValue) {
|
|
23
|
+
if (isOk(result)) {
|
|
24
|
+
return result.value;
|
|
25
|
+
}
|
|
26
|
+
return defaultValue;
|
|
27
|
+
}
|
|
28
|
+
function unwrapOrElse(result, fn) {
|
|
29
|
+
if (isOk(result)) {
|
|
30
|
+
return result.value;
|
|
31
|
+
}
|
|
32
|
+
return fn(result.error);
|
|
33
|
+
}
|
|
34
|
+
function map(result, fn) {
|
|
35
|
+
if (isOk(result)) {
|
|
36
|
+
return ok(fn(result.value));
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function mapErr(result, fn) {
|
|
41
|
+
if (isErr(result)) {
|
|
42
|
+
return err(fn(result.error));
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function flatMap(result, fn) {
|
|
47
|
+
if (isOk(result)) {
|
|
48
|
+
return fn(result.value);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
async function tryCatch(fn, errorTransform) {
|
|
53
|
+
try {
|
|
54
|
+
const value = await fn();
|
|
55
|
+
return ok(value);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (errorTransform) {
|
|
58
|
+
return err(errorTransform(error));
|
|
59
|
+
}
|
|
60
|
+
return err(error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function tryCatchSync(fn, errorTransform) {
|
|
64
|
+
try {
|
|
65
|
+
const value = fn();
|
|
66
|
+
return ok(value);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (errorTransform) {
|
|
69
|
+
return err(errorTransform(error));
|
|
70
|
+
}
|
|
71
|
+
return err(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function all(results) {
|
|
75
|
+
const values = [];
|
|
76
|
+
for (const result of results) {
|
|
77
|
+
if (isErr(result)) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
values.push(result.value);
|
|
81
|
+
}
|
|
82
|
+
return ok(values);
|
|
83
|
+
}
|
|
84
|
+
function match(result, handlers) {
|
|
85
|
+
if (isOk(result)) {
|
|
86
|
+
return handlers.ok(result.value);
|
|
87
|
+
}
|
|
88
|
+
return handlers.err(result.error);
|
|
89
|
+
}
|
|
90
|
+
async function fromPromise(promise, errorTransform) {
|
|
91
|
+
return tryCatch(() => promise, errorTransform);
|
|
92
|
+
}
|
|
93
|
+
function fromNullable(value, error) {
|
|
94
|
+
if (value === null || value === void 0) {
|
|
95
|
+
return err(error);
|
|
96
|
+
}
|
|
97
|
+
return ok(value);
|
|
98
|
+
}
|
|
99
|
+
var ResultClass = class _ResultClass {
|
|
100
|
+
constructor(result) {
|
|
101
|
+
this.result = result;
|
|
102
|
+
}
|
|
103
|
+
static ok(value) {
|
|
104
|
+
return new _ResultClass(ok(value));
|
|
105
|
+
}
|
|
106
|
+
static err(error) {
|
|
107
|
+
return new _ResultClass(err(error));
|
|
108
|
+
}
|
|
109
|
+
static async fromAsync(fn, errorTransform) {
|
|
110
|
+
const result = await tryCatch(fn, errorTransform);
|
|
111
|
+
return new _ResultClass(result);
|
|
112
|
+
}
|
|
113
|
+
isOk() {
|
|
114
|
+
return isOk(this.result);
|
|
115
|
+
}
|
|
116
|
+
isErr() {
|
|
117
|
+
return isErr(this.result);
|
|
118
|
+
}
|
|
119
|
+
unwrap() {
|
|
120
|
+
return unwrap(this.result);
|
|
121
|
+
}
|
|
122
|
+
unwrapOr(defaultValue) {
|
|
123
|
+
return unwrapOr(this.result, defaultValue);
|
|
124
|
+
}
|
|
125
|
+
map(fn) {
|
|
126
|
+
return new _ResultClass(map(this.result, fn));
|
|
127
|
+
}
|
|
128
|
+
mapErr(fn) {
|
|
129
|
+
return new _ResultClass(mapErr(this.result, fn));
|
|
130
|
+
}
|
|
131
|
+
flatMap(fn) {
|
|
132
|
+
return new _ResultClass(flatMap(this.result, fn));
|
|
133
|
+
}
|
|
134
|
+
match(handlers) {
|
|
135
|
+
return match(this.result, handlers);
|
|
136
|
+
}
|
|
137
|
+
toResult() {
|
|
138
|
+
return this.result;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var Result = {
|
|
142
|
+
ok,
|
|
143
|
+
err,
|
|
144
|
+
isOk,
|
|
145
|
+
isErr,
|
|
146
|
+
unwrap,
|
|
147
|
+
unwrapOr,
|
|
148
|
+
unwrapOrElse,
|
|
149
|
+
map,
|
|
150
|
+
mapErr,
|
|
151
|
+
flatMap,
|
|
152
|
+
tryCatch,
|
|
153
|
+
tryCatchSync,
|
|
154
|
+
all,
|
|
155
|
+
match,
|
|
156
|
+
fromPromise,
|
|
157
|
+
fromNullable
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/core/events.ts
|
|
161
|
+
var EventBus = class {
|
|
162
|
+
handlers = /* @__PURE__ */ new Map();
|
|
163
|
+
/**
|
|
164
|
+
* Register an event handler
|
|
165
|
+
*/
|
|
166
|
+
on(event, handler) {
|
|
167
|
+
if (!this.handlers.has(event)) {
|
|
168
|
+
this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
169
|
+
}
|
|
170
|
+
this.handlers.get(event).add(handler);
|
|
171
|
+
return () => this.off(event, handler);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Register a one-time event handler
|
|
175
|
+
*/
|
|
176
|
+
once(event, handler) {
|
|
177
|
+
const wrappedHandler = async (payload) => {
|
|
178
|
+
this.off(event, wrappedHandler);
|
|
179
|
+
await handler(payload);
|
|
180
|
+
};
|
|
181
|
+
return this.on(event, wrappedHandler);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Remove an event handler
|
|
185
|
+
*/
|
|
186
|
+
off(event, handler) {
|
|
187
|
+
const eventHandlers = this.handlers.get(event);
|
|
188
|
+
if (eventHandlers) {
|
|
189
|
+
eventHandlers.delete(handler);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Emit an event
|
|
194
|
+
*/
|
|
195
|
+
async emit(event, payload) {
|
|
196
|
+
const eventHandlers = this.handlers.get(event);
|
|
197
|
+
if (!eventHandlers || eventHandlers.size === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const handlers = Array.from(eventHandlers);
|
|
201
|
+
await Promise.all(
|
|
202
|
+
handlers.map(async (handler) => {
|
|
203
|
+
try {
|
|
204
|
+
await handler(payload);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(`Event handler error for ${event}:`, error);
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Emit event synchronously (fire-and-forget)
|
|
213
|
+
*/
|
|
214
|
+
emitSync(event, payload) {
|
|
215
|
+
void this.emit(event, payload);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Remove all handlers for an event
|
|
219
|
+
*/
|
|
220
|
+
removeAllListeners(event) {
|
|
221
|
+
if (event) {
|
|
222
|
+
this.handlers.delete(event);
|
|
223
|
+
} else {
|
|
224
|
+
this.handlers.clear();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get listener count for an event
|
|
229
|
+
*/
|
|
230
|
+
listenerCount(event) {
|
|
231
|
+
return this.handlers.get(event)?.size ?? 0;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get all registered events
|
|
235
|
+
*/
|
|
236
|
+
eventNames() {
|
|
237
|
+
return Array.from(this.handlers.keys());
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var defaultEventBus = null;
|
|
241
|
+
function getEventBus() {
|
|
242
|
+
if (!defaultEventBus) {
|
|
243
|
+
defaultEventBus = new EventBus();
|
|
244
|
+
}
|
|
245
|
+
return defaultEventBus;
|
|
246
|
+
}
|
|
247
|
+
function createEventBus() {
|
|
248
|
+
return new EventBus();
|
|
249
|
+
}
|
|
250
|
+
function resetEventBus() {
|
|
251
|
+
if (defaultEventBus) {
|
|
252
|
+
defaultEventBus.removeAllListeners();
|
|
253
|
+
}
|
|
254
|
+
defaultEventBus = null;
|
|
255
|
+
}
|
|
256
|
+
function onEmployeeHired(handler) {
|
|
257
|
+
return getEventBus().on("employee:hired", handler);
|
|
258
|
+
}
|
|
259
|
+
function onSalaryProcessed(handler) {
|
|
260
|
+
return getEventBus().on("salary:processed", handler);
|
|
261
|
+
}
|
|
262
|
+
function onPayrollCompleted(handler) {
|
|
263
|
+
return getEventBus().on("payroll:completed", handler);
|
|
264
|
+
}
|
|
265
|
+
function onMilestoneAchieved(handler) {
|
|
266
|
+
return getEventBus().on("milestone:achieved", handler);
|
|
267
|
+
}
|
|
268
|
+
var IdempotencyManager = class {
|
|
269
|
+
cache;
|
|
270
|
+
constructor(options = {}) {
|
|
271
|
+
this.cache = new LRUCache({
|
|
272
|
+
max: options.max || 1e4,
|
|
273
|
+
// Store 10k keys
|
|
274
|
+
ttl: options.ttl || 1e3 * 60 * 60 * 24
|
|
275
|
+
// 24 hours default
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if key exists and return cached result
|
|
280
|
+
*/
|
|
281
|
+
get(key) {
|
|
282
|
+
const cached = this.cache.get(key);
|
|
283
|
+
if (!cached) return null;
|
|
284
|
+
return {
|
|
285
|
+
value: cached.value,
|
|
286
|
+
cached: true,
|
|
287
|
+
createdAt: cached.createdAt
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Store result for idempotency key
|
|
292
|
+
*/
|
|
293
|
+
set(key, value) {
|
|
294
|
+
this.cache.set(key, {
|
|
295
|
+
value,
|
|
296
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Execute function with idempotency protection
|
|
301
|
+
*/
|
|
302
|
+
async execute(key, fn) {
|
|
303
|
+
const cached = this.get(key);
|
|
304
|
+
if (cached) {
|
|
305
|
+
return cached;
|
|
306
|
+
}
|
|
307
|
+
const value = await fn();
|
|
308
|
+
this.set(key, value);
|
|
309
|
+
return {
|
|
310
|
+
value,
|
|
311
|
+
cached: false,
|
|
312
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Clear a specific key
|
|
317
|
+
*/
|
|
318
|
+
delete(key) {
|
|
319
|
+
this.cache.delete(key);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Clear all keys
|
|
323
|
+
*/
|
|
324
|
+
clear() {
|
|
325
|
+
this.cache.clear();
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get cache stats
|
|
329
|
+
*/
|
|
330
|
+
stats() {
|
|
331
|
+
return {
|
|
332
|
+
size: this.cache.size,
|
|
333
|
+
max: this.cache.max
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function generatePayrollIdempotencyKey(organizationId, employeeId, month, year) {
|
|
338
|
+
return `payroll:${organizationId}:${employeeId}:${year}-${month}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/core/webhooks.ts
|
|
342
|
+
var WebhookManager = class {
|
|
343
|
+
webhooks = [];
|
|
344
|
+
deliveryLog = [];
|
|
345
|
+
/**
|
|
346
|
+
* Register a webhook
|
|
347
|
+
*/
|
|
348
|
+
register(config) {
|
|
349
|
+
this.webhooks.push({
|
|
350
|
+
retries: 3,
|
|
351
|
+
timeout: 3e4,
|
|
352
|
+
...config
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Remove a webhook
|
|
357
|
+
*/
|
|
358
|
+
unregister(url) {
|
|
359
|
+
this.webhooks = this.webhooks.filter((w) => w.url !== url);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Send webhook for event
|
|
363
|
+
*/
|
|
364
|
+
async send(event, payload) {
|
|
365
|
+
const matchingWebhooks = this.webhooks.filter((w) => w.events.includes(event));
|
|
366
|
+
const deliveries = matchingWebhooks.map(
|
|
367
|
+
(webhook) => this.deliver(webhook, event, payload)
|
|
368
|
+
);
|
|
369
|
+
await Promise.allSettled(deliveries);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Deliver webhook with retries
|
|
373
|
+
*/
|
|
374
|
+
async deliver(webhook, event, payload) {
|
|
375
|
+
const deliveryId = `${Date.now()}-${Math.random().toString(36)}`;
|
|
376
|
+
const delivery = {
|
|
377
|
+
id: deliveryId,
|
|
378
|
+
event,
|
|
379
|
+
url: webhook.url,
|
|
380
|
+
payload,
|
|
381
|
+
attempt: 0,
|
|
382
|
+
status: "pending"
|
|
383
|
+
};
|
|
384
|
+
this.deliveryLog.push(delivery);
|
|
385
|
+
const maxRetries = webhook.retries || 3;
|
|
386
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
387
|
+
delivery.attempt = attempt;
|
|
388
|
+
try {
|
|
389
|
+
const controller = new AbortController();
|
|
390
|
+
const timeout = setTimeout(() => controller.abort(), webhook.timeout || 3e4);
|
|
391
|
+
const headers = {
|
|
392
|
+
"Content-Type": "application/json",
|
|
393
|
+
"X-Payroll-Event": event,
|
|
394
|
+
"X-Payroll-Delivery": deliveryId,
|
|
395
|
+
...webhook.headers
|
|
396
|
+
};
|
|
397
|
+
if (webhook.secret) {
|
|
398
|
+
headers["X-Payroll-Signature"] = this.generateSignature(payload, webhook.secret);
|
|
399
|
+
}
|
|
400
|
+
const response = await fetch(webhook.url, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers,
|
|
403
|
+
body: JSON.stringify({
|
|
404
|
+
event,
|
|
405
|
+
payload,
|
|
406
|
+
deliveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
407
|
+
}),
|
|
408
|
+
signal: controller.signal
|
|
409
|
+
});
|
|
410
|
+
clearTimeout(timeout);
|
|
411
|
+
delivery.response = {
|
|
412
|
+
status: response.status,
|
|
413
|
+
body: await response.text()
|
|
414
|
+
};
|
|
415
|
+
delivery.sentAt = /* @__PURE__ */ new Date();
|
|
416
|
+
if (response.ok) {
|
|
417
|
+
delivery.status = "sent";
|
|
418
|
+
return delivery;
|
|
419
|
+
}
|
|
420
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
421
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
delivery.status = "failed";
|
|
425
|
+
delivery.error = `HTTP ${response.status}`;
|
|
426
|
+
return delivery;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
delivery.error = error.message;
|
|
429
|
+
if (attempt < maxRetries) {
|
|
430
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
delivery.status = "failed";
|
|
434
|
+
return delivery;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return delivery;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Generate HMAC signature for webhook
|
|
441
|
+
*/
|
|
442
|
+
generateSignature(payload, secret) {
|
|
443
|
+
const data = JSON.stringify(payload);
|
|
444
|
+
return Buffer.from(`${secret}:${data}`).toString("base64");
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Sleep for ms
|
|
448
|
+
*/
|
|
449
|
+
sleep(ms) {
|
|
450
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get delivery log
|
|
454
|
+
*/
|
|
455
|
+
getDeliveries(options) {
|
|
456
|
+
let results = this.deliveryLog;
|
|
457
|
+
if (options?.event) {
|
|
458
|
+
results = results.filter((d) => d.event === options.event);
|
|
459
|
+
}
|
|
460
|
+
if (options?.status) {
|
|
461
|
+
results = results.filter((d) => d.status === options.status);
|
|
462
|
+
}
|
|
463
|
+
if (options?.limit) {
|
|
464
|
+
results = results.slice(-options.limit);
|
|
465
|
+
}
|
|
466
|
+
return results;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Clear delivery log
|
|
470
|
+
*/
|
|
471
|
+
clearLog() {
|
|
472
|
+
this.deliveryLog = [];
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get all registered webhooks
|
|
476
|
+
*/
|
|
477
|
+
getWebhooks() {
|
|
478
|
+
return [...this.webhooks];
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// src/core/plugin.ts
|
|
483
|
+
var PluginManager = class {
|
|
484
|
+
constructor(context) {
|
|
485
|
+
this.context = context;
|
|
486
|
+
}
|
|
487
|
+
plugins = /* @__PURE__ */ new Map();
|
|
488
|
+
hooks = /* @__PURE__ */ new Map();
|
|
489
|
+
/**
|
|
490
|
+
* Register a plugin
|
|
491
|
+
*/
|
|
492
|
+
async register(plugin) {
|
|
493
|
+
if (this.plugins.has(plugin.name)) {
|
|
494
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
495
|
+
}
|
|
496
|
+
if (plugin.hooks) {
|
|
497
|
+
for (const [hookName, handler] of Object.entries(plugin.hooks)) {
|
|
498
|
+
if (handler) {
|
|
499
|
+
this.addHook(hookName, handler);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (plugin.init) {
|
|
504
|
+
await plugin.init(this.context);
|
|
505
|
+
}
|
|
506
|
+
this.plugins.set(plugin.name, plugin);
|
|
507
|
+
this.context.logger.debug(`Plugin "${plugin.name}" registered`);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Unregister a plugin
|
|
511
|
+
*/
|
|
512
|
+
async unregister(name) {
|
|
513
|
+
const plugin = this.plugins.get(name);
|
|
514
|
+
if (!plugin) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (plugin.destroy) {
|
|
518
|
+
await plugin.destroy();
|
|
519
|
+
}
|
|
520
|
+
this.plugins.delete(name);
|
|
521
|
+
this.context.logger.debug(`Plugin "${name}" unregistered`);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Add a hook handler
|
|
525
|
+
*/
|
|
526
|
+
addHook(hookName, handler) {
|
|
527
|
+
if (!this.hooks.has(hookName)) {
|
|
528
|
+
this.hooks.set(hookName, []);
|
|
529
|
+
}
|
|
530
|
+
this.hooks.get(hookName).push(handler);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Execute hooks for a given event
|
|
534
|
+
*/
|
|
535
|
+
async executeHooks(hookName, ...args) {
|
|
536
|
+
const handlers = this.hooks.get(hookName);
|
|
537
|
+
if (!handlers || handlers.length === 0) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
for (const handler of handlers) {
|
|
541
|
+
try {
|
|
542
|
+
await handler(...args);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.context.logger.error(`Hook "${hookName}" error:`, { error });
|
|
545
|
+
const errorHandlers = this.hooks.get("onError");
|
|
546
|
+
if (errorHandlers) {
|
|
547
|
+
for (const errorHandler of errorHandlers) {
|
|
548
|
+
try {
|
|
549
|
+
await errorHandler(error, hookName);
|
|
550
|
+
} catch {
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Get registered plugin names
|
|
559
|
+
*/
|
|
560
|
+
getPluginNames() {
|
|
561
|
+
return Array.from(this.plugins.keys());
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Check if plugin is registered
|
|
565
|
+
*/
|
|
566
|
+
hasPlugin(name) {
|
|
567
|
+
return this.plugins.has(name);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
function definePlugin(definition) {
|
|
571
|
+
return definition;
|
|
572
|
+
}
|
|
573
|
+
var loggingPlugin = definePlugin({
|
|
574
|
+
name: "logging",
|
|
575
|
+
version: "1.0.0",
|
|
576
|
+
init: (context) => {
|
|
577
|
+
context.addHook("employee:hired", (payload) => {
|
|
578
|
+
context.logger.info("Employee hired", {
|
|
579
|
+
employeeId: payload.employee.employeeId,
|
|
580
|
+
position: payload.employee.position
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
context.addHook("salary:processed", (payload) => {
|
|
584
|
+
context.logger.info("Salary processed", {
|
|
585
|
+
employeeId: payload.employee.employeeId,
|
|
586
|
+
amount: payload.payroll.netAmount,
|
|
587
|
+
period: payload.payroll.period
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
context.addHook("employee:terminated", (payload) => {
|
|
591
|
+
context.logger.info("Employee terminated", {
|
|
592
|
+
employeeId: payload.employee.employeeId,
|
|
593
|
+
reason: payload.reason
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
hooks: {
|
|
598
|
+
onError: (error, context) => {
|
|
599
|
+
console.error(`[Payroll Error] ${context}:`, error.message);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
var metricsPlugin = definePlugin({
|
|
604
|
+
name: "metrics",
|
|
605
|
+
version: "1.0.0",
|
|
606
|
+
init: (context) => {
|
|
607
|
+
const metrics = {
|
|
608
|
+
employeesHired: 0,
|
|
609
|
+
employeesTerminated: 0,
|
|
610
|
+
salariesProcessed: 0,
|
|
611
|
+
totalPaid: 0,
|
|
612
|
+
errors: 0
|
|
613
|
+
};
|
|
614
|
+
context.addHook("employee:hired", () => {
|
|
615
|
+
metrics.employeesHired++;
|
|
616
|
+
});
|
|
617
|
+
context.addHook("employee:terminated", () => {
|
|
618
|
+
metrics.employeesTerminated++;
|
|
619
|
+
});
|
|
620
|
+
context.addHook("salary:processed", (payload) => {
|
|
621
|
+
metrics.salariesProcessed++;
|
|
622
|
+
metrics.totalPaid += payload.payroll.netAmount;
|
|
623
|
+
});
|
|
624
|
+
context.payroll.metrics = metrics;
|
|
625
|
+
},
|
|
626
|
+
hooks: {
|
|
627
|
+
onError: (error, context) => {
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
function createNotificationPlugin(options) {
|
|
632
|
+
return definePlugin({
|
|
633
|
+
name: "notification",
|
|
634
|
+
version: "1.0.0",
|
|
635
|
+
init: (context) => {
|
|
636
|
+
if (options.onHired) {
|
|
637
|
+
context.addHook("employee:hired", async (payload) => {
|
|
638
|
+
await options.onHired({
|
|
639
|
+
id: payload.employee.id,
|
|
640
|
+
name: payload.employee.position
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (options.onTerminated) {
|
|
645
|
+
context.addHook("employee:terminated", async (payload) => {
|
|
646
|
+
await options.onTerminated({
|
|
647
|
+
id: payload.employee.id,
|
|
648
|
+
name: payload.employee.name
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (options.onSalaryProcessed) {
|
|
653
|
+
context.addHook("salary:processed", async (payload) => {
|
|
654
|
+
await options.onSalaryProcessed({
|
|
655
|
+
employee: {
|
|
656
|
+
id: payload.employee.id,
|
|
657
|
+
name: payload.employee.name
|
|
658
|
+
},
|
|
659
|
+
amount: payload.payroll.netAmount
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (options.onMilestone) {
|
|
664
|
+
context.addHook("milestone:achieved", async (payload) => {
|
|
665
|
+
await options.onMilestone({
|
|
666
|
+
employee: {
|
|
667
|
+
id: payload.employee.id,
|
|
668
|
+
name: payload.employee.name
|
|
669
|
+
},
|
|
670
|
+
milestone: payload.milestone.message
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
var notificationPlugin = createNotificationPlugin({});
|
|
678
|
+
|
|
679
|
+
// src/utils/logger.ts
|
|
680
|
+
var createConsoleLogger = () => ({
|
|
681
|
+
info: (message, meta) => {
|
|
682
|
+
if (meta) {
|
|
683
|
+
console.log(`[Payroll] INFO: ${message}`, meta);
|
|
684
|
+
} else {
|
|
685
|
+
console.log(`[Payroll] INFO: ${message}`);
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
error: (message, meta) => {
|
|
689
|
+
if (meta) {
|
|
690
|
+
console.error(`[Payroll] ERROR: ${message}`, meta);
|
|
691
|
+
} else {
|
|
692
|
+
console.error(`[Payroll] ERROR: ${message}`);
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
warn: (message, meta) => {
|
|
696
|
+
if (meta) {
|
|
697
|
+
console.warn(`[Payroll] WARN: ${message}`, meta);
|
|
698
|
+
} else {
|
|
699
|
+
console.warn(`[Payroll] WARN: ${message}`);
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
debug: (message, meta) => {
|
|
703
|
+
if (process.env.NODE_ENV !== "production") {
|
|
704
|
+
if (meta) {
|
|
705
|
+
console.log(`[Payroll] DEBUG: ${message}`, meta);
|
|
706
|
+
} else {
|
|
707
|
+
console.log(`[Payroll] DEBUG: ${message}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
var currentLogger = createConsoleLogger();
|
|
713
|
+
function getLogger() {
|
|
714
|
+
return currentLogger;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/config.ts
|
|
718
|
+
var HRM_CONFIG = {
|
|
719
|
+
dataRetention: {
|
|
720
|
+
payrollRecordsTTL: 63072e3,
|
|
721
|
+
// 2 years in seconds
|
|
722
|
+
exportWarningDays: 30,
|
|
723
|
+
archiveBeforeDeletion: true
|
|
724
|
+
},
|
|
725
|
+
payroll: {
|
|
726
|
+
defaultCurrency: "BDT",
|
|
727
|
+
allowProRating: true,
|
|
728
|
+
attendanceIntegration: true,
|
|
729
|
+
autoDeductions: true,
|
|
730
|
+
overtimeEnabled: false,
|
|
731
|
+
overtimeMultiplier: 1.5
|
|
732
|
+
},
|
|
733
|
+
salary: {
|
|
734
|
+
minimumWage: 0,
|
|
735
|
+
maximumAllowances: 10,
|
|
736
|
+
maximumDeductions: 10,
|
|
737
|
+
defaultFrequency: "monthly"
|
|
738
|
+
},
|
|
739
|
+
employment: {
|
|
740
|
+
defaultProbationMonths: 3,
|
|
741
|
+
maxProbationMonths: 6,
|
|
742
|
+
allowReHiring: true,
|
|
743
|
+
trackEmploymentHistory: true
|
|
744
|
+
},
|
|
745
|
+
validation: {
|
|
746
|
+
requireBankDetails: false,
|
|
747
|
+
requireUserId: false,
|
|
748
|
+
// Modern: Allow guest employees by default
|
|
749
|
+
identityMode: "employeeId",
|
|
750
|
+
// Modern: Use human-readable IDs as primary
|
|
751
|
+
identityFallbacks: ["email", "userId"]
|
|
752
|
+
// Smart fallback chain
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
var ORG_ROLES = {
|
|
756
|
+
OWNER: {
|
|
757
|
+
key: "owner",
|
|
758
|
+
label: "Owner",
|
|
759
|
+
description: "Full organization access (set by Organization model)"
|
|
760
|
+
},
|
|
761
|
+
MANAGER: {
|
|
762
|
+
key: "manager",
|
|
763
|
+
label: "Manager",
|
|
764
|
+
description: "Management and administrative features"
|
|
765
|
+
},
|
|
766
|
+
TRAINER: {
|
|
767
|
+
key: "trainer",
|
|
768
|
+
label: "Trainer",
|
|
769
|
+
description: "Training and coaching features"
|
|
770
|
+
},
|
|
771
|
+
STAFF: {
|
|
772
|
+
key: "staff",
|
|
773
|
+
label: "Staff",
|
|
774
|
+
description: "General staff access to basic features"
|
|
775
|
+
},
|
|
776
|
+
INTERN: {
|
|
777
|
+
key: "intern",
|
|
778
|
+
label: "Intern",
|
|
779
|
+
description: "Limited access for interns"
|
|
780
|
+
},
|
|
781
|
+
CONSULTANT: {
|
|
782
|
+
key: "consultant",
|
|
783
|
+
label: "Consultant",
|
|
784
|
+
description: "Project-based consultant access"
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
Object.values(ORG_ROLES).map((role) => role.key);
|
|
788
|
+
function mergeConfig(customConfig) {
|
|
789
|
+
if (!customConfig) return HRM_CONFIG;
|
|
790
|
+
return {
|
|
791
|
+
dataRetention: { ...HRM_CONFIG.dataRetention, ...customConfig.dataRetention },
|
|
792
|
+
payroll: { ...HRM_CONFIG.payroll, ...customConfig.payroll },
|
|
793
|
+
salary: { ...HRM_CONFIG.salary, ...customConfig.salary },
|
|
794
|
+
employment: { ...HRM_CONFIG.employment, ...customConfig.employment },
|
|
795
|
+
validation: {
|
|
796
|
+
...HRM_CONFIG.validation,
|
|
797
|
+
...customConfig.validation,
|
|
798
|
+
// Ensure fallbacks is always EmployeeIdentityMode[]
|
|
799
|
+
identityFallbacks: customConfig.validation?.identityFallbacks ?? HRM_CONFIG.validation.identityFallbacks
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/core/container.ts
|
|
805
|
+
var Container = class {
|
|
806
|
+
_models = null;
|
|
807
|
+
_config = HRM_CONFIG;
|
|
808
|
+
_singleTenant = null;
|
|
809
|
+
_logger;
|
|
810
|
+
_initialized = false;
|
|
811
|
+
constructor() {
|
|
812
|
+
this._logger = getLogger();
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Initialize container with configuration
|
|
816
|
+
*/
|
|
817
|
+
initialize(config) {
|
|
818
|
+
if (this._initialized) {
|
|
819
|
+
this._logger.warn("Container already initialized, re-initializing");
|
|
820
|
+
}
|
|
821
|
+
this._models = config.models;
|
|
822
|
+
this._config = mergeConfig(config.config);
|
|
823
|
+
this._singleTenant = config.singleTenant ?? null;
|
|
824
|
+
if (config.logger) {
|
|
825
|
+
this._logger = config.logger;
|
|
826
|
+
}
|
|
827
|
+
this._initialized = true;
|
|
828
|
+
this._logger.info("Container initialized", {
|
|
829
|
+
hasEmployeeModel: !!this._models.EmployeeModel,
|
|
830
|
+
hasPayrollRecordModel: !!this._models.PayrollRecordModel,
|
|
831
|
+
hasTransactionModel: !!this._models.TransactionModel,
|
|
832
|
+
hasAttendanceModel: !!this._models.AttendanceModel,
|
|
833
|
+
hasLeaveRequestModel: !!this._models.LeaveRequestModel,
|
|
834
|
+
hasTaxWithholdingModel: !!this._models.TaxWithholdingModel,
|
|
835
|
+
isSingleTenant: !!this._singleTenant
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Check if container is initialized
|
|
840
|
+
*/
|
|
841
|
+
isInitialized() {
|
|
842
|
+
return this._initialized;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Reset container (useful for testing)
|
|
846
|
+
*/
|
|
847
|
+
reset() {
|
|
848
|
+
this._models = null;
|
|
849
|
+
this._config = HRM_CONFIG;
|
|
850
|
+
this._singleTenant = null;
|
|
851
|
+
this._initialized = false;
|
|
852
|
+
this._logger.info("Container reset");
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Ensure container is initialized
|
|
856
|
+
*/
|
|
857
|
+
ensureInitialized() {
|
|
858
|
+
if (!this._initialized || !this._models) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
"Payroll not initialized. Call Payroll.initialize() first."
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Get models container (strongly typed)
|
|
866
|
+
*/
|
|
867
|
+
getModels() {
|
|
868
|
+
this.ensureInitialized();
|
|
869
|
+
return this._models;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Get Employee model (strongly typed)
|
|
873
|
+
*/
|
|
874
|
+
getEmployeeModel() {
|
|
875
|
+
this.ensureInitialized();
|
|
876
|
+
return this._models.EmployeeModel;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Get PayrollRecord model (strongly typed)
|
|
880
|
+
*/
|
|
881
|
+
getPayrollRecordModel() {
|
|
882
|
+
this.ensureInitialized();
|
|
883
|
+
return this._models.PayrollRecordModel;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Get Transaction model (strongly typed)
|
|
887
|
+
*/
|
|
888
|
+
getTransactionModel() {
|
|
889
|
+
this.ensureInitialized();
|
|
890
|
+
return this._models.TransactionModel;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Get Attendance model (optional, strongly typed)
|
|
894
|
+
*/
|
|
895
|
+
getAttendanceModel() {
|
|
896
|
+
this.ensureInitialized();
|
|
897
|
+
return this._models.AttendanceModel ?? null;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Get LeaveRequest model (optional, strongly typed)
|
|
901
|
+
*/
|
|
902
|
+
getLeaveRequestModel() {
|
|
903
|
+
this.ensureInitialized();
|
|
904
|
+
return this._models.LeaveRequestModel ?? null;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Get TaxWithholding model (optional, strongly typed)
|
|
908
|
+
*/
|
|
909
|
+
getTaxWithholdingModel() {
|
|
910
|
+
this.ensureInitialized();
|
|
911
|
+
return this._models.TaxWithholdingModel ?? null;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Get configuration
|
|
915
|
+
*/
|
|
916
|
+
getConfig() {
|
|
917
|
+
return this._config;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Get specific config section
|
|
921
|
+
*/
|
|
922
|
+
getConfigSection(section) {
|
|
923
|
+
return this._config[section];
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Check if single-tenant mode
|
|
927
|
+
*/
|
|
928
|
+
isSingleTenant() {
|
|
929
|
+
return !!this._singleTenant;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Get single-tenant config
|
|
933
|
+
*/
|
|
934
|
+
getSingleTenantConfig() {
|
|
935
|
+
return this._singleTenant;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Get organization ID (for single-tenant mode)
|
|
939
|
+
*/
|
|
940
|
+
getOrganizationId() {
|
|
941
|
+
if (!this._singleTenant || !this._singleTenant.organizationId) return null;
|
|
942
|
+
return typeof this._singleTenant.organizationId === "string" ? this._singleTenant.organizationId : this._singleTenant.organizationId.toString();
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Get logger
|
|
946
|
+
*/
|
|
947
|
+
getLogger() {
|
|
948
|
+
return this._logger;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Set logger
|
|
952
|
+
*/
|
|
953
|
+
setLogger(logger) {
|
|
954
|
+
this._logger = logger;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Has attendance integration
|
|
958
|
+
*/
|
|
959
|
+
hasAttendanceIntegration() {
|
|
960
|
+
return !!this._models?.AttendanceModel && this._config.payroll.attendanceIntegration;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Create operation context with defaults
|
|
964
|
+
*/
|
|
965
|
+
createOperationContext(overrides) {
|
|
966
|
+
const context = {};
|
|
967
|
+
if (this._singleTenant?.autoInject && !overrides?.organizationId) {
|
|
968
|
+
context.organizationId = this.getOrganizationId();
|
|
969
|
+
}
|
|
970
|
+
return { ...context, ...overrides };
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
var defaultContainer = null;
|
|
974
|
+
function getContainer() {
|
|
975
|
+
if (!defaultContainer) {
|
|
976
|
+
defaultContainer = new Container();
|
|
977
|
+
}
|
|
978
|
+
return defaultContainer;
|
|
979
|
+
}
|
|
980
|
+
function initializeContainer(config) {
|
|
981
|
+
getContainer().initialize(config);
|
|
982
|
+
}
|
|
983
|
+
function isContainerInitialized() {
|
|
984
|
+
return defaultContainer?.isInitialized() ?? false;
|
|
985
|
+
}
|
|
986
|
+
function getModels() {
|
|
987
|
+
return getContainer().getModels();
|
|
988
|
+
}
|
|
989
|
+
function getConfig() {
|
|
990
|
+
return getContainer().getConfig();
|
|
991
|
+
}
|
|
992
|
+
function isSingleTenant() {
|
|
993
|
+
return getContainer().isSingleTenant();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/core/config.ts
|
|
997
|
+
var DEFAULT_TAX_BRACKETS = [
|
|
998
|
+
{ min: 0, max: 1e4, rate: 0.1 },
|
|
999
|
+
{ min: 1e4, max: 4e4, rate: 0.12 },
|
|
1000
|
+
{ min: 4e4, max: 85e3, rate: 0.22 },
|
|
1001
|
+
{ min: 85e3, max: 165e3, rate: 0.24 },
|
|
1002
|
+
{ min: 165e3, max: 215e3, rate: 0.32 },
|
|
1003
|
+
{ min: 215e3, max: 54e4, rate: 0.35 },
|
|
1004
|
+
{ min: 54e4, max: Infinity, rate: 0.37 }
|
|
1005
|
+
];
|
|
1006
|
+
var DEFAULT_WORK_SCHEDULE = {
|
|
1007
|
+
workingDays: [1, 2, 3, 4, 5],
|
|
1008
|
+
// Monday to Friday
|
|
1009
|
+
hoursPerDay: 8
|
|
1010
|
+
};
|
|
1011
|
+
function countWorkingDays(startDate, endDate, options = {}) {
|
|
1012
|
+
const workDays = options.workingDays || DEFAULT_WORK_SCHEDULE.workingDays;
|
|
1013
|
+
const holidaySet = new Set(
|
|
1014
|
+
(options.holidays || []).map((d) => new Date(d).toDateString())
|
|
1015
|
+
);
|
|
1016
|
+
let totalDays = 0;
|
|
1017
|
+
let workingDays = 0;
|
|
1018
|
+
let holidays = 0;
|
|
1019
|
+
let weekends = 0;
|
|
1020
|
+
const current = new Date(startDate);
|
|
1021
|
+
current.setHours(0, 0, 0, 0);
|
|
1022
|
+
const end = new Date(endDate);
|
|
1023
|
+
end.setHours(0, 0, 0, 0);
|
|
1024
|
+
while (current <= end) {
|
|
1025
|
+
totalDays++;
|
|
1026
|
+
const isHoliday = holidaySet.has(current.toDateString());
|
|
1027
|
+
const isWorkDay = workDays.includes(current.getDay());
|
|
1028
|
+
if (isHoliday) {
|
|
1029
|
+
holidays++;
|
|
1030
|
+
} else if (isWorkDay) {
|
|
1031
|
+
workingDays++;
|
|
1032
|
+
} else {
|
|
1033
|
+
weekends++;
|
|
1034
|
+
}
|
|
1035
|
+
current.setDate(current.getDate() + 1);
|
|
1036
|
+
}
|
|
1037
|
+
return { totalDays, workingDays, weekends, holidays };
|
|
1038
|
+
}
|
|
1039
|
+
function calculateProration(hireDate, terminationDate, periodStart, periodEnd) {
|
|
1040
|
+
const hire = new Date(hireDate);
|
|
1041
|
+
hire.setHours(0, 0, 0, 0);
|
|
1042
|
+
const term = terminationDate ? new Date(terminationDate) : null;
|
|
1043
|
+
if (term) term.setHours(0, 0, 0, 0);
|
|
1044
|
+
const start = new Date(periodStart);
|
|
1045
|
+
start.setHours(0, 0, 0, 0);
|
|
1046
|
+
const end = new Date(periodEnd);
|
|
1047
|
+
end.setHours(0, 0, 0, 0);
|
|
1048
|
+
if (hire > end || term && term < start) {
|
|
1049
|
+
return { ratio: 0, reason: "full", isProrated: true };
|
|
1050
|
+
}
|
|
1051
|
+
const effectiveStart = hire > start ? hire : start;
|
|
1052
|
+
const effectiveEnd = term && term < end ? term : end;
|
|
1053
|
+
const totalDays = Math.ceil((end.getTime() - start.getTime()) / 864e5) + 1;
|
|
1054
|
+
const actualDays = Math.ceil((effectiveEnd.getTime() - effectiveStart.getTime()) / 864e5) + 1;
|
|
1055
|
+
const ratio = Math.min(1, Math.max(0, actualDays / totalDays));
|
|
1056
|
+
const isNewHire = hire > start;
|
|
1057
|
+
const isTermination = term !== null && term < end;
|
|
1058
|
+
let reason = "full";
|
|
1059
|
+
if (isNewHire && isTermination) {
|
|
1060
|
+
reason = "both";
|
|
1061
|
+
} else if (isNewHire) {
|
|
1062
|
+
reason = "new_hire";
|
|
1063
|
+
} else if (isTermination) {
|
|
1064
|
+
reason = "termination";
|
|
1065
|
+
}
|
|
1066
|
+
return { ratio, reason, isProrated: ratio < 1 };
|
|
1067
|
+
}
|
|
1068
|
+
function calculateSimpleTax(monthlyIncome, brackets = DEFAULT_TAX_BRACKETS) {
|
|
1069
|
+
const annualIncome = monthlyIncome * 12;
|
|
1070
|
+
let annualTax = 0;
|
|
1071
|
+
for (const bracket of brackets) {
|
|
1072
|
+
if (annualIncome <= bracket.min) continue;
|
|
1073
|
+
const taxableInBracket = Math.min(annualIncome, bracket.max) - bracket.min;
|
|
1074
|
+
annualTax += taxableInBracket * bracket.rate;
|
|
1075
|
+
}
|
|
1076
|
+
const monthlyTax = Math.round(annualTax / 12);
|
|
1077
|
+
const effectiveRate = monthlyIncome > 0 ? monthlyTax / monthlyIncome : 0;
|
|
1078
|
+
return { amount: monthlyTax, effectiveRate };
|
|
1079
|
+
}
|
|
1080
|
+
function calculateAttendanceDeduction(expectedDays, actualDays, dailyRate, maxDeductionPercent = 100) {
|
|
1081
|
+
const absentDays = Math.max(0, expectedDays - actualDays);
|
|
1082
|
+
const deduction = Math.round(absentDays * dailyRate);
|
|
1083
|
+
const maxDeduction = Math.round(dailyRate * expectedDays * maxDeductionPercent / 100);
|
|
1084
|
+
return Math.min(deduction, maxDeduction);
|
|
1085
|
+
}
|
|
1086
|
+
function calculateSalaryBreakdown(params) {
|
|
1087
|
+
const {
|
|
1088
|
+
baseSalary,
|
|
1089
|
+
hireDate,
|
|
1090
|
+
terminationDate,
|
|
1091
|
+
periodStart,
|
|
1092
|
+
periodEnd,
|
|
1093
|
+
allowances = [],
|
|
1094
|
+
deductions = [],
|
|
1095
|
+
options = {},
|
|
1096
|
+
attendance
|
|
1097
|
+
} = params;
|
|
1098
|
+
const workSchedule = { ...DEFAULT_WORK_SCHEDULE, ...options.workSchedule };
|
|
1099
|
+
const workingDays = countWorkingDays(periodStart, periodEnd, {
|
|
1100
|
+
workingDays: workSchedule.workingDays,
|
|
1101
|
+
holidays: options.holidays
|
|
1102
|
+
});
|
|
1103
|
+
const proration = options.skipProration ? { ratio: 1, reason: "full", isProrated: false } : calculateProration(hireDate, terminationDate, periodStart, periodEnd);
|
|
1104
|
+
const proratedBase = Math.round(baseSalary * proration.ratio);
|
|
1105
|
+
const processedAllowances = allowances.map((a) => ({
|
|
1106
|
+
type: a.type,
|
|
1107
|
+
amount: Math.round(a.amount * proration.ratio),
|
|
1108
|
+
taxable: a.taxable ?? true
|
|
1109
|
+
}));
|
|
1110
|
+
const totalAllowances = processedAllowances.reduce((sum, a) => sum + a.amount, 0);
|
|
1111
|
+
const processedDeductions = deductions.map((d) => ({
|
|
1112
|
+
type: d.type,
|
|
1113
|
+
amount: Math.round(d.amount * proration.ratio)
|
|
1114
|
+
}));
|
|
1115
|
+
let attendanceDeduction = 0;
|
|
1116
|
+
if (attendance && !options.skipAttendance && workingDays.workingDays > 0) {
|
|
1117
|
+
const dailyRate = proratedBase / workingDays.workingDays;
|
|
1118
|
+
attendanceDeduction = calculateAttendanceDeduction(
|
|
1119
|
+
attendance.expectedDays,
|
|
1120
|
+
attendance.actualDays,
|
|
1121
|
+
dailyRate
|
|
1122
|
+
);
|
|
1123
|
+
if (attendanceDeduction > 0) {
|
|
1124
|
+
processedDeductions.push({ type: "attendance", amount: attendanceDeduction });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const grossSalary = proratedBase + totalAllowances;
|
|
1128
|
+
let taxAmount = 0;
|
|
1129
|
+
if (!options.skipTax) {
|
|
1130
|
+
const taxableAllowances = processedAllowances.filter((a) => a.taxable).reduce((sum, a) => sum + a.amount, 0);
|
|
1131
|
+
const taxableIncome = proratedBase + taxableAllowances;
|
|
1132
|
+
const taxResult = calculateSimpleTax(taxableIncome);
|
|
1133
|
+
taxAmount = taxResult.amount;
|
|
1134
|
+
if (taxAmount > 0) {
|
|
1135
|
+
processedDeductions.push({ type: "tax", amount: taxAmount });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const totalDeductions = processedDeductions.filter((d) => d.type !== "tax").reduce((sum, d) => sum + d.amount, 0);
|
|
1139
|
+
const netSalary = grossSalary - totalDeductions - taxAmount;
|
|
1140
|
+
return {
|
|
1141
|
+
baseSalary,
|
|
1142
|
+
proratedBase,
|
|
1143
|
+
totalAllowances,
|
|
1144
|
+
totalDeductions,
|
|
1145
|
+
attendanceDeduction,
|
|
1146
|
+
grossSalary,
|
|
1147
|
+
taxAmount,
|
|
1148
|
+
netSalary,
|
|
1149
|
+
proration,
|
|
1150
|
+
workingDays,
|
|
1151
|
+
breakdown: {
|
|
1152
|
+
allowances: processedAllowances,
|
|
1153
|
+
deductions: processedDeductions
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function getPayPeriod(month, year, payDay = 28) {
|
|
1158
|
+
const startDate = new Date(year, month - 1, 1);
|
|
1159
|
+
const endDate = new Date(year, month, 0);
|
|
1160
|
+
const payDate = new Date(year, month - 1, Math.min(payDay, endDate.getDate()));
|
|
1161
|
+
return { startDate, endDate, payDate };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export { Container, DEFAULT_TAX_BRACKETS, DEFAULT_WORK_SCHEDULE, EventBus, IdempotencyManager, PluginManager, Result, ResultClass, WebhookManager, all, calculateAttendanceDeduction, calculateProration, calculateSalaryBreakdown, countWorkingDays, createEventBus, createNotificationPlugin, definePlugin, err, flatMap, fromNullable, fromPromise, generatePayrollIdempotencyKey, getConfig, getContainer, getEventBus, getModels, getPayPeriod, initializeContainer, isContainerInitialized, isErr, isOk, isSingleTenant, loggingPlugin, map, mapErr, match, metricsPlugin, notificationPlugin, ok, onEmployeeHired, onMilestoneAchieved, onPayrollCompleted, onSalaryProcessed, resetEventBus, tryCatch, tryCatchSync, unwrap, unwrapOr, unwrapOrElse };
|
|
1165
|
+
//# sourceMappingURL=index.js.map
|
|
1166
|
+
//# sourceMappingURL=index.js.map
|