@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.
@@ -45,6 +45,11 @@ function applyTaxBrackets(amount, brackets) {
45
45
  }
46
46
 
47
47
  // src/utils/date.ts
48
+ function toUTCDateString(date) {
49
+ const d = new Date(date);
50
+ d.setHours(0, 0, 0, 0);
51
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
52
+ }
48
53
  function isEffectiveForPeriod(item, periodStart, periodEnd) {
49
54
  const effectiveFrom = item.effectiveFrom ? new Date(item.effectiveFrom) : /* @__PURE__ */ new Date(0);
50
55
  const effectiveTo = item.effectiveTo ? new Date(item.effectiveTo) : /* @__PURE__ */ new Date("2099-12-31");
@@ -57,7 +62,7 @@ var DEFAULT_WORK_SCHEDULE = {
57
62
  function countWorkingDays(startDate, endDate, options = {}) {
58
63
  const workDays = options.workingDays || DEFAULT_WORK_SCHEDULE.workingDays;
59
64
  const holidaySet = new Set(
60
- (options.holidays || []).map((d) => new Date(d).toDateString())
65
+ (options.holidays || []).map((d) => toUTCDateString(d))
61
66
  );
62
67
  let totalDays = 0;
63
68
  let workingDays = 0;
@@ -69,7 +74,7 @@ function countWorkingDays(startDate, endDate, options = {}) {
69
74
  end.setHours(0, 0, 0, 0);
70
75
  while (current <= end) {
71
76
  totalDays++;
72
- const isHoliday = holidaySet.has(current.toDateString());
77
+ const isHoliday = holidaySet.has(toUTCDateString(current));
73
78
  const isWorkDay = workDays.includes(current.getDay());
74
79
  if (isHoliday) {
75
80
  holidays++;
@@ -83,6 +88,52 @@ function countWorkingDays(startDate, endDate, options = {}) {
83
88
  return { totalDays, workingDays, weekends, holidays };
84
89
  }
85
90
 
91
+ // src/config.ts
92
+ var ORG_ROLES = {
93
+ OWNER: {
94
+ key: "owner",
95
+ label: "Owner",
96
+ description: "Full organization access (set by Organization model)"
97
+ },
98
+ MANAGER: {
99
+ key: "manager",
100
+ label: "Manager",
101
+ description: "Management and administrative features"
102
+ },
103
+ TRAINER: {
104
+ key: "trainer",
105
+ label: "Trainer",
106
+ description: "Training and coaching features"
107
+ },
108
+ STAFF: {
109
+ key: "staff",
110
+ label: "Staff",
111
+ description: "General staff access to basic features"
112
+ },
113
+ INTERN: {
114
+ key: "intern",
115
+ label: "Intern",
116
+ description: "Limited access for interns"
117
+ },
118
+ CONSULTANT: {
119
+ key: "consultant",
120
+ label: "Consultant",
121
+ description: "Project-based consultant access"
122
+ }
123
+ };
124
+ Object.values(ORG_ROLES).map((role) => role.key);
125
+ function getPayPeriodsPerYear(frequency) {
126
+ const periodsMap = {
127
+ monthly: 12,
128
+ bi_weekly: 26,
129
+ weekly: 52,
130
+ daily: 365,
131
+ hourly: 2080
132
+ // Assuming 40 hours/week * 52 weeks
133
+ };
134
+ return periodsMap[frequency];
135
+ }
136
+
86
137
  // src/calculators/prorating.calculator.ts
87
138
  function calculateProRating(input) {
88
139
  const { hireDate, terminationDate, periodStart, periodEnd, workingDays, holidays = [] } = input;
@@ -116,7 +167,7 @@ function calculateProRating(input) {
116
167
  };
117
168
  }
118
169
  function applyProRating(baseAmount, ratio) {
119
- return Math.round(baseAmount * ratio);
170
+ return roundMoney(baseAmount * ratio, 2);
120
171
  }
121
172
  function shouldProRate(hireDate, terminationDate, periodStart, periodEnd) {
122
173
  const hire = new Date(hireDate);
@@ -133,7 +184,7 @@ function calculateAttendanceDeduction(input) {
133
184
  const actual = Math.max(0, actualWorkingDays);
134
185
  const rate = Math.max(0, dailyRate);
135
186
  const absentDays = Math.max(0, expected - actual);
136
- const deductionAmount = Math.round(absentDays * rate);
187
+ const deductionAmount = roundMoney(absentDays * rate, 2);
137
188
  return {
138
189
  absentDays,
139
190
  deductionAmount,
@@ -143,20 +194,20 @@ function calculateAttendanceDeduction(input) {
143
194
  }
144
195
  function calculateDailyRate(monthlySalary, workingDays) {
145
196
  if (workingDays <= 0) return 0;
146
- return Math.round(monthlySalary / workingDays);
197
+ return roundMoney(monthlySalary / workingDays, 2);
147
198
  }
148
199
  function calculateHourlyRate(monthlySalary, workingDays, hoursPerDay = 8) {
149
200
  const dailyRate = calculateDailyRate(monthlySalary, workingDays);
150
201
  if (hoursPerDay <= 0) return 0;
151
- return Math.round(dailyRate / hoursPerDay);
202
+ return roundMoney(dailyRate / hoursPerDay, 2);
152
203
  }
153
204
  function calculatePartialDayDeduction(dailyRate, fractionAbsent) {
154
205
  const fraction = Math.min(1, Math.max(0, fractionAbsent));
155
- return Math.round(dailyRate * fraction);
206
+ return roundMoney(dailyRate * fraction, 2);
156
207
  }
157
208
  function calculateTotalAttendanceDeduction(input) {
158
209
  const { dailyRate, fullDayAbsences = 0, partialDayAbsences = [] } = input;
159
- const fullDayDeduction = Math.round(dailyRate * Math.max(0, fullDayAbsences));
210
+ const fullDayDeduction = roundMoney(dailyRate * Math.max(0, fullDayAbsences), 2);
160
211
  const partialDayDeduction = partialDayAbsences.reduce(
161
212
  (sum, fraction) => sum + calculatePartialDayDeduction(dailyRate, fraction),
162
213
  0
@@ -170,7 +221,7 @@ function calculateTotalAttendanceDeduction(input) {
170
221
 
171
222
  // src/calculators/salary.calculator.ts
172
223
  function calculateSalaryBreakdown(input) {
173
- const { employee, period, attendance, options = {}, config, taxBrackets } = input;
224
+ const { employee, period, attendance, options = {}, config, taxBrackets, taxOptions, jurisdictionTaxConfig } = input;
174
225
  const comp = employee.compensation;
175
226
  const originalBaseAmount = comp.baseAmount;
176
227
  const proRating = calculateProRatingForSalary(
@@ -187,8 +238,8 @@ function calculateSalaryBreakdown(input) {
187
238
  }
188
239
  const effectiveAllowances = (comp.allowances || []).filter((a) => isEffectiveForPeriod(a, period.startDate, period.endDate));
189
240
  const effectiveDeductions = (comp.deductions || []).filter((d) => isEffectiveForPeriod(d, period.startDate, period.endDate)).filter((d) => d.auto || d.recurring);
190
- const allowances = processAllowances(effectiveAllowances, originalBaseAmount, proRating, config);
191
- const deductions = processDeductions(effectiveDeductions, originalBaseAmount, proRating, config);
241
+ const allowances = processAllowances(effectiveAllowances, originalBaseAmount, proRating, config, options.skipProration);
242
+ const deductions = processDeductions(effectiveDeductions, originalBaseAmount, proRating, config, options.skipProration);
192
243
  if (!options.skipAttendance && config.attendanceIntegration && attendance) {
193
244
  const attendanceDeductionResult = calculateAttendanceDeductionFromData(
194
245
  attendance,
@@ -205,12 +256,24 @@ function calculateSalaryBreakdown(input) {
205
256
  }
206
257
  const grossSalary = calculateGross(baseAmount, allowances);
207
258
  const taxableAllowances = allowances.filter((a) => a.taxable);
208
- const taxableAmount = baseAmount + sumAllowances(taxableAllowances);
259
+ let taxableAmount = baseAmount + sumAllowances(taxableAllowances);
260
+ const preTaxDeductionAmount = calculatePreTaxDeductions(
261
+ effectiveDeductions,
262
+ deductions,
263
+ taxOptions,
264
+ jurisdictionTaxConfig
265
+ );
266
+ taxableAmount = Math.max(0, taxableAmount - preTaxDeductionAmount);
267
+ const frequency = employee.compensation?.frequency || "monthly";
209
268
  let taxAmount = 0;
210
269
  if (!options.skipTax && taxBrackets.length > 0 && config.autoDeductions) {
211
- const annualTaxable = taxableAmount * 12;
212
- const annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
213
- taxAmount = roundMoney(annualTax / 12);
270
+ taxAmount = calculateEnhancedTax(
271
+ taxableAmount,
272
+ taxBrackets,
273
+ taxOptions,
274
+ jurisdictionTaxConfig,
275
+ frequency
276
+ );
214
277
  }
215
278
  if (taxAmount > 0) {
216
279
  deductions.push({
@@ -246,11 +309,11 @@ function calculateProRatingForSalary(hireDate, terminationDate, periodStart, per
246
309
  holidays
247
310
  });
248
311
  }
249
- function processAllowances(allowances, originalBaseAmount, proRating, config) {
312
+ function processAllowances(allowances, originalBaseAmount, proRating, config, skipProration) {
250
313
  return allowances.map((a) => {
251
314
  let amount = a.isPercentage && a.value !== void 0 ? percentageOf(originalBaseAmount, a.value) : a.amount;
252
315
  const originalAmount = amount;
253
- if (proRating.isProRated && config.allowProRating) {
316
+ if (proRating.isProRated && config.allowProRating && !skipProration) {
254
317
  amount = prorateAmount(amount, proRating.ratio);
255
318
  }
256
319
  return {
@@ -263,11 +326,11 @@ function processAllowances(allowances, originalBaseAmount, proRating, config) {
263
326
  };
264
327
  });
265
328
  }
266
- function processDeductions(deductions, originalBaseAmount, proRating, config) {
329
+ function processDeductions(deductions, originalBaseAmount, proRating, config, skipProration) {
267
330
  return deductions.map((d) => {
268
331
  let amount = d.isPercentage && d.value !== void 0 ? percentageOf(originalBaseAmount, d.value) : d.amount;
269
332
  const originalAmount = amount;
270
- if (proRating.isProRated && config.allowProRating) {
333
+ if (proRating.isProRated && config.allowProRating && !skipProration) {
271
334
  amount = prorateAmount(amount, proRating.ratio);
272
335
  }
273
336
  return {
@@ -298,6 +361,78 @@ function calculateAttendanceDeductionFromData(attendance, baseAmount, effectiveW
298
361
  absentDays: result.absentDays
299
362
  };
300
363
  }
364
+ function calculatePreTaxDeductions(effectiveDeductions, processedDeductions, taxOptions, jurisdictionTaxConfig) {
365
+ let totalPreTax = 0;
366
+ for (let i = 0; i < effectiveDeductions.length; i++) {
367
+ const original = effectiveDeductions[i];
368
+ const processed = processedDeductions[i];
369
+ if (original.reducesTaxableIncome) {
370
+ totalPreTax += processed?.amount || 0;
371
+ }
372
+ }
373
+ if (jurisdictionTaxConfig?.preTaxDeductionTypes?.length) {
374
+ const preTaxTypes = new Set(jurisdictionTaxConfig.preTaxDeductionTypes);
375
+ for (let i = 0; i < effectiveDeductions.length; i++) {
376
+ const original = effectiveDeductions[i];
377
+ const processed = processedDeductions[i];
378
+ if (original.reducesTaxableIncome) continue;
379
+ if (preTaxTypes.has(original.type)) {
380
+ totalPreTax += processed?.amount || 0;
381
+ }
382
+ }
383
+ }
384
+ if (taxOptions?.preTaxDeductions?.length) {
385
+ for (const deduction of taxOptions.preTaxDeductions) {
386
+ totalPreTax += deduction.amount;
387
+ }
388
+ }
389
+ return roundMoney(totalPreTax);
390
+ }
391
+ function calculateEnhancedTax(periodTaxable, taxBrackets, taxOptions, jurisdictionTaxConfig, frequency = "monthly") {
392
+ const periodsPerYear = getPayPeriodsPerYear(frequency);
393
+ let annualTaxable = periodTaxable * periodsPerYear;
394
+ const threshold = getApplicableThreshold(taxOptions, jurisdictionTaxConfig);
395
+ if (threshold > 0) {
396
+ annualTaxable = Math.max(0, annualTaxable - threshold);
397
+ }
398
+ let annualTax = applyTaxBrackets(annualTaxable, taxBrackets);
399
+ if (taxOptions?.taxCredits?.length && annualTax > 0) {
400
+ annualTax = applyTaxCredits(annualTax, taxOptions.taxCredits);
401
+ }
402
+ return roundMoney(annualTax / periodsPerYear);
403
+ }
404
+ function getApplicableThreshold(taxOptions, jurisdictionTaxConfig) {
405
+ if (taxOptions?.standardDeductionOverride !== void 0) {
406
+ return taxOptions.standardDeductionOverride;
407
+ }
408
+ if (taxOptions?.taxpayerCategory) {
409
+ const category = taxOptions.taxpayerCategory;
410
+ if (taxOptions.thresholdOverrides?.[category] !== void 0) {
411
+ return taxOptions.thresholdOverrides[category];
412
+ }
413
+ if (jurisdictionTaxConfig?.thresholdsByCategory?.[category] !== void 0) {
414
+ return jurisdictionTaxConfig.thresholdsByCategory[category];
415
+ }
416
+ }
417
+ if (taxOptions?.applyStandardDeduction && jurisdictionTaxConfig?.standardDeduction) {
418
+ return jurisdictionTaxConfig.standardDeduction;
419
+ }
420
+ return 0;
421
+ }
422
+ function applyTaxCredits(annualTax, taxCredits) {
423
+ let remainingTax = annualTax;
424
+ for (const credit of taxCredits) {
425
+ if (remainingTax <= 0) break;
426
+ let creditAmount = credit.amount;
427
+ if (credit.maxPercent !== void 0 && credit.maxPercent > 0) {
428
+ const maxCredit = annualTax * credit.maxPercent;
429
+ creditAmount = Math.min(creditAmount, maxCredit);
430
+ }
431
+ creditAmount = Math.min(creditAmount, remainingTax);
432
+ remainingTax -= creditAmount;
433
+ }
434
+ return Math.max(0, remainingTax);
435
+ }
301
436
 
302
437
  export { applyProRating, calculateAttendanceDeduction, calculateDailyRate, calculateHourlyRate, calculatePartialDayDeduction, calculateProRating, calculateSalaryBreakdown, calculateTotalAttendanceDeduction, shouldProRate };
303
438
  //# sourceMappingURL=index.js.map