@classytic/payroll 2.0.0 → 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.

@@ -1,3 +1,5 @@
1
+ import { LRUCache } from 'lru-cache';
2
+
1
3
  // src/core/result.ts
2
4
  function ok(value) {
3
5
  return { ok: true, value };
@@ -263,6 +265,219 @@ function onPayrollCompleted(handler) {
263
265
  function onMilestoneAchieved(handler) {
264
266
  return getEventBus().on("milestone:achieved", handler);
265
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
+ };
266
481
 
267
482
  // src/core/plugin.ts
268
483
  var PluginManager = class {
@@ -529,9 +744,12 @@ var HRM_CONFIG = {
529
744
  },
530
745
  validation: {
531
746
  requireBankDetails: false,
532
- requireEmployeeId: true,
533
- uniqueEmployeeIdPerOrg: true,
534
- allowMultiTenantEmployees: true
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
535
753
  }
536
754
  };
537
755
  var ORG_ROLES = {
@@ -574,13 +792,17 @@ function mergeConfig(customConfig) {
574
792
  payroll: { ...HRM_CONFIG.payroll, ...customConfig.payroll },
575
793
  salary: { ...HRM_CONFIG.salary, ...customConfig.salary },
576
794
  employment: { ...HRM_CONFIG.employment, ...customConfig.employment },
577
- validation: { ...HRM_CONFIG.validation, ...customConfig.validation }
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
+ }
578
801
  };
579
802
  }
580
803
 
581
804
  // src/core/container.ts
582
- var Container = class _Container {
583
- static instance = null;
805
+ var Container = class {
584
806
  _models = null;
585
807
  _config = HRM_CONFIG;
586
808
  _singleTenant = null;
@@ -589,21 +811,6 @@ var Container = class _Container {
589
811
  constructor() {
590
812
  this._logger = getLogger();
591
813
  }
592
- /**
593
- * Get singleton instance
594
- */
595
- static getInstance() {
596
- if (!_Container.instance) {
597
- _Container.instance = new _Container();
598
- }
599
- return _Container.instance;
600
- }
601
- /**
602
- * Reset instance (for testing)
603
- */
604
- static resetInstance() {
605
- _Container.instance = null;
606
- }
607
814
  /**
608
815
  * Initialize container with configuration
609
816
  */
@@ -623,6 +830,8 @@ var Container = class _Container {
623
830
  hasPayrollRecordModel: !!this._models.PayrollRecordModel,
624
831
  hasTransactionModel: !!this._models.TransactionModel,
625
832
  hasAttendanceModel: !!this._models.AttendanceModel,
833
+ hasLeaveRequestModel: !!this._models.LeaveRequestModel,
834
+ hasTaxWithholdingModel: !!this._models.TaxWithholdingModel,
626
835
  isSingleTenant: !!this._singleTenant
627
836
  });
628
837
  }
@@ -632,6 +841,16 @@ var Container = class _Container {
632
841
  isInitialized() {
633
842
  return this._initialized;
634
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
+ }
635
854
  /**
636
855
  * Ensure container is initialized
637
856
  */
@@ -643,40 +862,54 @@ var Container = class _Container {
643
862
  }
644
863
  }
645
864
  /**
646
- * Get models container
865
+ * Get models container (strongly typed)
647
866
  */
648
867
  getModels() {
649
868
  this.ensureInitialized();
650
869
  return this._models;
651
870
  }
652
871
  /**
653
- * Get Employee model
872
+ * Get Employee model (strongly typed)
654
873
  */
655
874
  getEmployeeModel() {
656
875
  this.ensureInitialized();
657
876
  return this._models.EmployeeModel;
658
877
  }
659
878
  /**
660
- * Get PayrollRecord model
879
+ * Get PayrollRecord model (strongly typed)
661
880
  */
662
881
  getPayrollRecordModel() {
663
882
  this.ensureInitialized();
664
883
  return this._models.PayrollRecordModel;
665
884
  }
666
885
  /**
667
- * Get Transaction model
886
+ * Get Transaction model (strongly typed)
668
887
  */
669
888
  getTransactionModel() {
670
889
  this.ensureInitialized();
671
890
  return this._models.TransactionModel;
672
891
  }
673
892
  /**
674
- * Get Attendance model (optional)
893
+ * Get Attendance model (optional, strongly typed)
675
894
  */
676
895
  getAttendanceModel() {
677
896
  this.ensureInitialized();
678
897
  return this._models.AttendanceModel ?? null;
679
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
+ }
680
913
  /**
681
914
  * Get configuration
682
915
  */
@@ -737,82 +970,46 @@ var Container = class _Container {
737
970
  return { ...context, ...overrides };
738
971
  }
739
972
  };
973
+ var defaultContainer = null;
740
974
  function getContainer() {
741
- return Container.getInstance();
975
+ if (!defaultContainer) {
976
+ defaultContainer = new Container();
977
+ }
978
+ return defaultContainer;
742
979
  }
743
980
  function initializeContainer(config) {
744
- Container.getInstance().initialize(config);
981
+ getContainer().initialize(config);
745
982
  }
746
983
  function isContainerInitialized() {
747
- return Container.getInstance().isInitialized();
984
+ return defaultContainer?.isInitialized() ?? false;
748
985
  }
749
986
  function getModels() {
750
- return Container.getInstance().getModels();
987
+ return getContainer().getModels();
751
988
  }
752
989
  function getConfig() {
753
- return Container.getInstance().getConfig();
990
+ return getContainer().getConfig();
754
991
  }
755
992
  function isSingleTenant() {
756
- return Container.getInstance().isSingleTenant();
993
+ return getContainer().isSingleTenant();
757
994
  }
758
995
 
759
996
  // src/core/config.ts
760
- var COUNTRY_DEFAULTS = {
761
- US: {
762
- currency: "USD",
763
- workDays: [1, 2, 3, 4, 5],
764
- // Mon-Fri
765
- taxBrackets: [
766
- { min: 0, max: 11e3, rate: 0.1 },
767
- { min: 11e3, max: 44725, rate: 0.12 },
768
- { min: 44725, max: 95375, rate: 0.22 },
769
- { min: 95375, max: 182100, rate: 0.24 },
770
- { min: 182100, max: Infinity, rate: 0.32 }
771
- ]
772
- },
773
- BD: {
774
- currency: "BDT",
775
- workDays: [0, 1, 2, 3, 4],
776
- // Sun-Thu
777
- taxBrackets: [
778
- { min: 0, max: 35e4, rate: 0 },
779
- { min: 35e4, max: 45e4, rate: 0.05 },
780
- { min: 45e4, max: 75e4, rate: 0.1 },
781
- { min: 75e4, max: 115e4, rate: 0.15 },
782
- { min: 115e4, max: Infinity, rate: 0.2 }
783
- ]
784
- },
785
- UK: {
786
- currency: "GBP",
787
- workDays: [1, 2, 3, 4, 5],
788
- taxBrackets: [
789
- { min: 0, max: 12570, rate: 0 },
790
- { min: 12570, max: 50270, rate: 0.2 },
791
- { min: 50270, max: 125140, rate: 0.4 },
792
- { min: 125140, max: Infinity, rate: 0.45 }
793
- ]
794
- },
795
- IN: {
796
- currency: "INR",
797
- workDays: [1, 2, 3, 4, 5, 6],
798
- // Mon-Sat
799
- taxBrackets: [
800
- { min: 0, max: 3e5, rate: 0 },
801
- { min: 3e5, max: 6e5, rate: 0.05 },
802
- { min: 6e5, max: 9e5, rate: 0.1 },
803
- { min: 9e5, max: 12e5, rate: 0.15 },
804
- { min: 12e5, max: Infinity, rate: 0.2 }
805
- ]
806
- }
807
- };
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
+ ];
808
1006
  var DEFAULT_WORK_SCHEDULE = {
809
- workDays: [1, 2, 3, 4, 5],
1007
+ workingDays: [1, 2, 3, 4, 5],
810
1008
  // Monday to Friday
811
1009
  hoursPerDay: 8
812
1010
  };
813
- var DEFAULT_TAX_BRACKETS = COUNTRY_DEFAULTS.US.taxBrackets;
814
1011
  function countWorkingDays(startDate, endDate, options = {}) {
815
- const workDays = options.workDays || DEFAULT_WORK_SCHEDULE.workDays;
1012
+ const workDays = options.workingDays || DEFAULT_WORK_SCHEDULE.workingDays;
816
1013
  const holidaySet = new Set(
817
1014
  (options.holidays || []).map((d) => new Date(d).toDateString())
818
1015
  );
@@ -868,8 +1065,7 @@ function calculateProration(hireDate, terminationDate, periodStart, periodEnd) {
868
1065
  }
869
1066
  return { ratio, reason, isProrated: ratio < 1 };
870
1067
  }
871
- function calculateTax(monthlyIncome, currency, customBrackets) {
872
- const brackets = customBrackets || COUNTRY_DEFAULTS[currency]?.taxBrackets || DEFAULT_TAX_BRACKETS;
1068
+ function calculateSimpleTax(monthlyIncome, brackets = DEFAULT_TAX_BRACKETS) {
873
1069
  const annualIncome = monthlyIncome * 12;
874
1070
  let annualTax = 0;
875
1071
  for (const bracket of brackets) {
@@ -890,7 +1086,6 @@ function calculateAttendanceDeduction(expectedDays, actualDays, dailyRate, maxDe
890
1086
  function calculateSalaryBreakdown(params) {
891
1087
  const {
892
1088
  baseSalary,
893
- currency,
894
1089
  hireDate,
895
1090
  terminationDate,
896
1091
  periodStart,
@@ -902,7 +1097,7 @@ function calculateSalaryBreakdown(params) {
902
1097
  } = params;
903
1098
  const workSchedule = { ...DEFAULT_WORK_SCHEDULE, ...options.workSchedule };
904
1099
  const workingDays = countWorkingDays(periodStart, periodEnd, {
905
- workDays: workSchedule.workDays,
1100
+ workingDays: workSchedule.workingDays,
906
1101
  holidays: options.holidays
907
1102
  });
908
1103
  const proration = options.skipProration ? { ratio: 1, reason: "full", isProrated: false } : calculateProration(hireDate, terminationDate, periodStart, periodEnd);
@@ -934,14 +1129,14 @@ function calculateSalaryBreakdown(params) {
934
1129
  if (!options.skipTax) {
935
1130
  const taxableAllowances = processedAllowances.filter((a) => a.taxable).reduce((sum, a) => sum + a.amount, 0);
936
1131
  const taxableIncome = proratedBase + taxableAllowances;
937
- const taxResult = calculateTax(taxableIncome, currency);
1132
+ const taxResult = calculateSimpleTax(taxableIncome);
938
1133
  taxAmount = taxResult.amount;
939
1134
  if (taxAmount > 0) {
940
1135
  processedDeductions.push({ type: "tax", amount: taxAmount });
941
1136
  }
942
1137
  }
943
- const totalDeductions = processedDeductions.filter((d) => d.type !== "tax" && d.type !== "attendance").reduce((sum, d) => sum + d.amount, 0);
944
- const netSalary = grossSalary - totalDeductions - attendanceDeduction - taxAmount;
1138
+ const totalDeductions = processedDeductions.filter((d) => d.type !== "tax").reduce((sum, d) => sum + d.amount, 0);
1139
+ const netSalary = grossSalary - totalDeductions - taxAmount;
945
1140
  return {
946
1141
  baseSalary,
947
1142
  proratedBase,
@@ -966,6 +1161,6 @@ function getPayPeriod(month, year, payDay = 28) {
966
1161
  return { startDate, endDate, payDate };
967
1162
  }
968
1163
 
969
- export { COUNTRY_DEFAULTS, Container, DEFAULT_TAX_BRACKETS, DEFAULT_WORK_SCHEDULE, EventBus, PluginManager, Result, ResultClass, all, calculateAttendanceDeduction, calculateProration, calculateSalaryBreakdown, calculateTax, countWorkingDays, createEventBus, createNotificationPlugin, definePlugin, err, flatMap, fromNullable, fromPromise, 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 };
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 };
970
1165
  //# sourceMappingURL=index.js.map
971
1166
  //# sourceMappingURL=index.js.map