@mudraforge/statutory 2.12.1 → 2.13.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/CHANGELOG.md +17 -0
- package/README.md +13 -0
- package/docs/ARCHITECTURE.md +70 -47
- package/docs/ARCHITECTURE_DECISIONS.md +22 -2
- package/docs/CORPUS_QUALITY.md +3 -3
- package/docs/DOMAIN_BREADTH_AND_DEPTH_COMPARISON.md +4 -4
- package/docs/ENTERPRISE_GOVERNANCE.md +2 -1
- package/docs/OKF_MAINTENANCE_SOP.md +5 -5
- package/docs/PILOT_EVALUATION_GUIDE.md +5 -4
- package/docs/PROVENANCE.md +6 -6
- package/docs/VERIFICATION.md +6 -6
- package/okf/corporate_ma_restructuring/business_combinations_indas_103/ast.json +20 -0
- package/okf/corporate_ma_restructuring/impairment_of_assets_indas_36/ast.json +20 -0
- package/okf/corporate_ma_restructuring/loss_carryforward_sec79/ast.json +33 -0
- package/okf/corporate_ma_restructuring/share_based_payments_indas_102/ast.json +20 -0
- package/okf/corporate_ma_restructuring/slump_sale_50b/ast.json +21 -0
- package/okf/corporate_ma_restructuring/tax_clearance_sec281/ast.json +21 -0
- package/okf/direct_tax/corporate_tax_115baa_115jb/ast.json +23 -0
- package/okf/direct_tax/income_taxes_deferred_tax_indas_12/ast.json +20 -0
- package/okf/direct_tax/special_rates_non_filer_206ab/ast.json +26 -0
- package/okf/direct_tax/statutory_disallowance_43b/ast.json +35 -0
- package/okf/direct_tax/tcs_goods_206c_1h/ast.json +69 -0
- package/okf/direct_tax/tds_commission_194h/ast.json +60 -0
- package/okf/direct_tax/tds_contractor_194c/ast.json +71 -0
- package/okf/direct_tax/tds_ecommerce_194o/ast.json +48 -0
- package/okf/direct_tax/tds_goods_194q/ast.json +71 -0
- package/okf/direct_tax/tds_goods_393/ast.json +59 -0
- package/okf/direct_tax/tds_individual_contract_194m/ast.json +59 -0
- package/okf/direct_tax/tds_interest_194a/ast.json +59 -0
- package/okf/direct_tax/tds_perquisites_194r/ast.json +47 -0
- package/okf/direct_tax/tds_property_194ia/ast.json +70 -0
- package/okf/direct_tax/tds_rent_194i/ast.json +60 -0
- package/okf/direct_tax/tds_rent_individual_194ib/ast.json +59 -0
- package/okf/direct_tax/tds_services_194j/ast.json +60 -0
- package/okf/indirect_tax_gst/blocked_itc_17_5/ast.json +26 -0
- package/okf/indirect_tax_gst/common_itc_reversal_rule42_43/ast.json +26 -0
- package/okf/indirect_tax_gst/composite_mixed_supplies_sec8/ast.json +21 -0
- package/okf/indirect_tax_gst/composition_levy_sec10/ast.json +23 -0
- package/okf/indirect_tax_gst/credit_debit_notes_sec34/ast.json +21 -0
- package/okf/indirect_tax_gst/e_invoicing_rule48_4/ast.json +20 -0
- package/okf/indirect_tax_gst/export_services_lut_sec16/ast.json +20 -0
- package/okf/indirect_tax_gst/gst_tds_sec51/ast.json +23 -0
- package/okf/indirect_tax_gst/itc_eligibility_16_2/ast.json +20 -0
- package/okf/indirect_tax_gst/itc_reversal_rule37_37a/ast.json +45 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/ast.json +26 -0
- package/okf/international_tax/cross_border_remittance_195/ast.json +39 -0
- package/okf/international_tax/financial_instruments_ecl_indas_109/ast.json +24 -0
- package/okf/international_tax/foreign_exchange_indas_21_109/ast.json +20 -0
- package/okf/international_tax/foreign_tax_credit_irc_901_904/ast.json +20 -0
- package/okf/international_tax/transfer_pricing_alp_92c/ast.json +25 -0
- package/okf/manifest.json +49 -3
- package/okf/msme_commercial/delayed_payments_15_16/ast.json +38 -0
- package/okf/msme_commercial/enterprise_classification_sec7/ast.json +25 -0
- package/okf/msme_commercial/lease_accounting_indas_116/ast.json +20 -0
- package/okf/msme_commercial/mca_msme_1_filing/ast.json +20 -0
- package/okf/msme_commercial/provisions_contingencies_indas_37/ast.json +20 -0
- package/okf/msme_commercial/revenue_recognition_indas_115/ast.json +20 -0
- package/okf/msme_commercial/statutory_disclosures_sec22/ast.json +21 -0
- package/package.json +16 -1
- package/src/constants.js +1 -1
- package/src/index.js +16 -2
- package/src/payroll.js +506 -0
- package/src/usvm/evaluation_vm.js +504 -0
- package/src/usvm/index.js +65 -0
- package/src/usvm/math_rational.js +165 -0
- package/src/usvm/rules/sec_115baa_115jb_mat.json +23 -0
- package/src/usvm/rules/sec_192_salary_tds.json +23 -0
- package/src/usvm/rules/sec_194a_interest.json +59 -0
- package/src/usvm/rules/sec_194c_contractors.json +71 -0
- package/src/usvm/rules/sec_194h_commission.json +60 -0
- package/src/usvm/rules/sec_194i_rent.json +60 -0
- package/src/usvm/rules/sec_194ia_property.json +70 -0
- package/src/usvm/rules/sec_194ib_rent_ind.json +59 -0
- package/src/usvm/rules/sec_194j_services.json +60 -0
- package/src/usvm/rules/sec_194m_contract_ind.json +59 -0
- package/src/usvm/rules/sec_194o_ecommerce.json +48 -0
- package/src/usvm/rules/sec_194q_goods.json +71 -0
- package/src/usvm/rules/sec_194r_perquisites.json +47 -0
- package/src/usvm/rules/sec_195_cross_border.json +39 -0
- package/src/usvm/rules/sec_206ab_non_filer.json +26 -0
- package/src/usvm/rules/sec_206c1h_tcs.json +69 -0
- package/src/usvm/rules/sec_281_tax_clearance.json +21 -0
- package/src/usvm/rules/sec_393_goods.json +59 -0
- package/src/usvm/rules/sec_43bh_disallowance.json +35 -0
- package/src/usvm/rules/sec_50b_slump_sale.json +21 -0
- package/src/usvm/rules/sec_79_loss_continuity.json +33 -0
- package/src/usvm/rules/sec_92c_transfer_pricing.json +25 -0
- package/src/usvm/rules/sec_ftc_dtaa_relief.json +20 -0
- package/src/usvm/rules/sec_gst_blocked_itc_17_5.json +26 -0
- package/src/usvm/rules/sec_gst_composite_mixed_8.json +21 -0
- package/src/usvm/rules/sec_gst_composition_levy_10.json +23 -0
- package/src/usvm/rules/sec_gst_credit_debit_notes_34.json +21 -0
- package/src/usvm/rules/sec_gst_einvoicing_rule48_4.json +20 -0
- package/src/usvm/rules/sec_gst_ewaybill_rule138.json +21 -0
- package/src/usvm/rules/sec_gst_export_lut_16.json +20 -0
- package/src/usvm/rules/sec_gst_itc_eligibility_16_2.json +20 -0
- package/src/usvm/rules/sec_gst_itc_reversal_rule37_37a.json +45 -0
- package/src/usvm/rules/sec_gst_rcm_liability_9_3.json +26 -0
- package/src/usvm/rules/sec_gst_rule42_43_apportionment.json +26 -0
- package/src/usvm/rules/sec_gst_tds_sec51.json +23 -0
- package/src/usvm/rules/sec_indas102_esop.json +20 -0
- package/src/usvm/rules/sec_indas103_mna.json +20 -0
- package/src/usvm/rules/sec_indas109_ecl.json +24 -0
- package/src/usvm/rules/sec_indas115_revenue.json +20 -0
- package/src/usvm/rules/sec_indas116_leases.json +20 -0
- package/src/usvm/rules/sec_indas12_deferred_tax.json +20 -0
- package/src/usvm/rules/sec_indas21_fx_reval.json +20 -0
- package/src/usvm/rules/sec_indas36_impairment.json +20 -0
- package/src/usvm/rules/sec_indas37_provisions.json +20 -0
- package/src/usvm/rules/sec_msme_classification_sec7.json +25 -0
- package/src/usvm/rules/sec_msme_delayed_payments_15_16.json +38 -0
- package/src/usvm/rules/sec_msme_disclosures_sec22.json +21 -0
- package/src/usvm/rules/sec_msme_mca_form_1.json +20 -0
- package/src/usvm/schemas/statutory_ast_schema.js +203 -0
- package/src/usvm/usvm_domain_evaluators.js +1021 -0
- package/types/index.d.ts +39 -0
package/src/index.js
CHANGED
|
@@ -14,6 +14,10 @@ import os from 'node:os';
|
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
15
|
import dns from 'node:dns';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import {
|
|
18
|
+
evaluateStatutoryAst,
|
|
19
|
+
resolveTemporalCrystallization
|
|
20
|
+
} from './usvm/index.js';
|
|
17
21
|
import {
|
|
18
22
|
roundToPaisa,
|
|
19
23
|
roundSection288B,
|
|
@@ -117,7 +121,7 @@ export { STATUTORY_CLIENT_VERSION, DEFAULT_BASE_URL };
|
|
|
117
121
|
/**
|
|
118
122
|
* Current verified Merkle root digest for the active OKF v0.2 corpus.
|
|
119
123
|
*/
|
|
120
|
-
export const CURRENT_RELEASE_MERKLE_ROOT = 'sha256-
|
|
124
|
+
export const CURRENT_RELEASE_MERKLE_ROOT = 'sha256-7ffc75e8611a9c966eea756dd544fdae9144e2318d3478633232bb876d05e364';
|
|
121
125
|
export const MERKLE_ROOT_OKF_V2 = CURRENT_RELEASE_MERKLE_ROOT;
|
|
122
126
|
|
|
123
127
|
/**
|
|
@@ -125,6 +129,7 @@ export const MERKLE_ROOT_OKF_V2 = CURRENT_RELEASE_MERKLE_ROOT;
|
|
|
125
129
|
* Prevents unauthorized bundle substitution attacks even if an attacker controls the remote endpoint.
|
|
126
130
|
*/
|
|
127
131
|
export const PINNED_RELEASE_ROOTS = new Set([
|
|
132
|
+
'sha256-7ffc75e8611a9c966eea756dd544fdae9144e2318d3478633232bb876d05e364', // feat/usvm-declarative-architecture (46 Declarative ASTs across 5 canonical domains)
|
|
128
133
|
'sha256-318004673a779499e82fd7316c7a6728f3bcdaf449bb4a557b6bd685b899dee3', // fix/public-okf-table1-sl-nos-and-temporal-thresholds (Section 393 Table 1 Sl Nos & 2025 Act thresholds)
|
|
129
134
|
'sha256-b17c37b2724aece2a28beaab5720a777ae27e661ac51f1436be6eff3ca5ef64b', // feat/okf-accounting-domain-expansion (46 Sections across 5 Domains, 52 Circulars, 47 Forms)
|
|
130
135
|
'sha256-a94baf08cbdea064a7b1bd25c1e1311821bf75e9b0d15cc8e0648339d487c79b', // feat/zoho-gstr2b-ap-reconciliation (38 Sections across 5 Domains, 44 Circulars, 39 Forms)
|
|
@@ -916,6 +921,13 @@ export function lookupLocalStatute(section, okfDir = DEFAULT_LOCAL_OKF_DIR) {
|
|
|
916
921
|
forms: fs.existsSync(formsDir) ? fs.readdirSync(formsDir).filter(f => f.endsWith('.json') || f.endsWith('.md')) : []
|
|
917
922
|
};
|
|
918
923
|
|
|
924
|
+
const astPath = path.join(parentDir, 'ast.json');
|
|
925
|
+
if (fs.existsSync(astPath)) {
|
|
926
|
+
try {
|
|
927
|
+
data.ast = JSON.parse(fs.readFileSync(astPath, 'utf8'));
|
|
928
|
+
} catch (_) {}
|
|
929
|
+
}
|
|
930
|
+
|
|
919
931
|
return data;
|
|
920
932
|
}
|
|
921
933
|
return null;
|
|
@@ -3412,7 +3424,9 @@ export {
|
|
|
3412
3424
|
calculateEclUtilizationInterest,
|
|
3413
3425
|
calculatePortfolioEclUtilizationInterest,
|
|
3414
3426
|
buildEvidenceProvenance,
|
|
3415
|
-
runStatutoryWatchtower
|
|
3427
|
+
runStatutoryWatchtower,
|
|
3428
|
+
evaluateStatutoryAst,
|
|
3429
|
+
resolveTemporalCrystallization
|
|
3416
3430
|
};
|
|
3417
3431
|
|
|
3418
3432
|
|
package/src/payroll.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DETERMINISTIC STATUTORY PAYROLL & SALARY WITHHOLDING CALCULATORS
|
|
3
|
+
* ==============================================================================
|
|
4
|
+
* Implements pure mathematical functions for Indian payroll compliance:
|
|
5
|
+
* 1. Salary TDS under Income-tax Act, 1961 Section 192 & Income-tax Act, 2025 Section 392(1)
|
|
6
|
+
* - Default Section 115BAC(1A) New Tax Regime with ₹75,000 standard deduction
|
|
7
|
+
* - Old Tax Regime opt-out with ₹50,000 standard deduction and Form 12BB deductions
|
|
8
|
+
* - Section 87A rebate and 4% Health & Education Cess
|
|
9
|
+
* 2. Employees' Provident Fund (EPF & EPS) under EPF Act, 1952
|
|
10
|
+
* - ₹15,000 standard wage ceiling vs. Paragraph 26(6) joint higher wage declarations
|
|
11
|
+
* - Age 58 EPS cessation rule
|
|
12
|
+
* 3. Employees' State Insurance (ESI) under ESI Act, 1948
|
|
13
|
+
* - ₹21,000 gross wage threshold & ₹176/day exempt employee floor
|
|
14
|
+
* 4. Professional Tax (PT) across Indian state statutory slabs
|
|
15
|
+
*
|
|
16
|
+
* Statutory Invariant: Every calculator strictly returns isAuthorizedToDisburse: false.
|
|
17
|
+
* Zero external npm dependencies. Integer paise arithmetic to prevent float drift.
|
|
18
|
+
* ==============================================================================
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { roundToPaisa, roundSection288B } from './calculators.js';
|
|
22
|
+
|
|
23
|
+
export const TAX_REGIMES = Object.freeze({
|
|
24
|
+
NEW: 'NEW',
|
|
25
|
+
OLD: 'OLD'
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const NEW_REGIME_SLABS_AY2025_26 = Object.freeze([
|
|
29
|
+
{ min: 0, max: 300000, ratePercent: 0 },
|
|
30
|
+
{ min: 300000, max: 700000, ratePercent: 5 },
|
|
31
|
+
{ min: 700000, max: 1000000, ratePercent: 10 },
|
|
32
|
+
{ min: 1000000, max: 1200000, ratePercent: 15 },
|
|
33
|
+
{ min: 1200000, max: 1500000, ratePercent: 20 },
|
|
34
|
+
{ min: 1500000, max: Infinity, ratePercent: 30 }
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const OLD_REGIME_SLABS = Object.freeze([
|
|
38
|
+
{ min: 0, max: 250000, ratePercent: 0 },
|
|
39
|
+
{ min: 250000, max: 500000, ratePercent: 5 },
|
|
40
|
+
{ min: 500000, max: 1000000, ratePercent: 20 },
|
|
41
|
+
{ min: 1000000, max: Infinity, ratePercent: 30 }
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export const EPF_STATUTORY_CONSTANTS = Object.freeze({
|
|
45
|
+
STANDARD_WAGE_CEILING_INR: 15000,
|
|
46
|
+
EMPLOYEE_RATE_PERCENT: 12.0,
|
|
47
|
+
EMPLOYER_TOTAL_RATE_PERCENT: 12.0,
|
|
48
|
+
EPS_RATE_PERCENT: 8.33,
|
|
49
|
+
EPS_MAX_MONTHLY_CAP_INR: 1250,
|
|
50
|
+
EPS_SUPERANNUATION_AGE_YEARS: 58
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const ESI_STATUTORY_CONSTANTS = Object.freeze({
|
|
54
|
+
GROSS_WAGE_CEILING_INR: 21000,
|
|
55
|
+
EMPLOYEE_RATE_PERCENT: 0.75,
|
|
56
|
+
EMPLOYER_RATE_PERCENT: 3.25,
|
|
57
|
+
DAILY_EXEMPT_WAGE_FLOOR_INR: 176
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Evaluates annual and monthly Salary TDS under Section 192 (1961 Act)
|
|
62
|
+
* or Section 392(1) (2025 Act).
|
|
63
|
+
*/
|
|
64
|
+
export function evaluateSalaryTds({
|
|
65
|
+
annualGrossSalaryInr,
|
|
66
|
+
taxRegime = TAX_REGIMES.NEW,
|
|
67
|
+
form12BBClaims = {},
|
|
68
|
+
standardDeductionOverride = null,
|
|
69
|
+
monthsElapsedInFy = 0,
|
|
70
|
+
priorTdsDeductedInr = 0,
|
|
71
|
+
transactionDate = null,
|
|
72
|
+
isSeniorCitizen = false,
|
|
73
|
+
isSuperSeniorCitizen = false
|
|
74
|
+
} = {}) {
|
|
75
|
+
const gross = Math.max(0, Number(annualGrossSalaryInr) || 0);
|
|
76
|
+
const dateStr = transactionDate ? String(transactionDate).slice(0, 10) : new Date().toISOString().slice(0, 10);
|
|
77
|
+
const isAct2025 = Boolean(dateStr >= '2026-04-01');
|
|
78
|
+
|
|
79
|
+
const normalizedRegime = String(taxRegime || '').toUpperCase().trim() === TAX_REGIMES.OLD
|
|
80
|
+
? TAX_REGIMES.OLD
|
|
81
|
+
: TAX_REGIMES.NEW;
|
|
82
|
+
|
|
83
|
+
// Standard Deduction: ₹75,000 for New Regime (Finance (No. 2) Act, 2024), ₹50,000 for Old Regime
|
|
84
|
+
let standardDeductionInr = standardDeductionOverride !== null && standardDeductionOverride !== undefined
|
|
85
|
+
? Math.max(0, Number(standardDeductionOverride) || 0)
|
|
86
|
+
: (normalizedRegime === TAX_REGIMES.NEW ? 75000 : 50000);
|
|
87
|
+
|
|
88
|
+
let allowedDeductionsInr = standardDeductionInr;
|
|
89
|
+
const deductionsBreakdown = {
|
|
90
|
+
standardDeductionInr
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (normalizedRegime === TAX_REGIMES.OLD) {
|
|
94
|
+
// Section 80C capped at ₹1,50,000
|
|
95
|
+
const claim80C = Math.min(150000, Math.max(0, Number(form12BBClaims.section80CInr) || 0));
|
|
96
|
+
// Section 80D (Health Insurance) capped at ₹25,000 (or ₹50,000 for senior citizen)
|
|
97
|
+
const cap80D = isSeniorCitizen ? 50000 : 25000;
|
|
98
|
+
const claim80D = Math.min(cap80D, Math.max(0, Number(form12BBClaims.section80DInr) || 0));
|
|
99
|
+
// Section 24(b) Home loan interest capped at ₹2,00,000
|
|
100
|
+
const claimHomeLoan = Math.min(200000, Math.max(0, Number(form12BBClaims.homeLoanInterestSec24Inr) || 0));
|
|
101
|
+
// HRA Exemption under Section 10(13A)
|
|
102
|
+
const claimHra = Math.max(0, Number(form12BBClaims.hraExemptionInr) || 0);
|
|
103
|
+
// LTA Exemption under Section 10(5)
|
|
104
|
+
const claimLta = Math.max(0, Number(form12BBClaims.ltaExemptionInr) || 0);
|
|
105
|
+
// Other eligible deductions
|
|
106
|
+
const claimOther = Math.max(0, Number(form12BBClaims.otherDeductionsInr) || 0);
|
|
107
|
+
|
|
108
|
+
deductionsBreakdown.section80CInr = claim80C;
|
|
109
|
+
deductionsBreakdown.section80DInr = claim80D;
|
|
110
|
+
deductionsBreakdown.homeLoanInterestSec24Inr = claimHomeLoan;
|
|
111
|
+
deductionsBreakdown.hraExemptionInr = claimHra;
|
|
112
|
+
deductionsBreakdown.ltaExemptionInr = claimLta;
|
|
113
|
+
deductionsBreakdown.otherDeductionsInr = claimOther;
|
|
114
|
+
|
|
115
|
+
allowedDeductionsInr += (claim80C + claim80D + claimHomeLoan + claimHra + claimLta + claimOther);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const netTaxableIncomeInr = Math.max(0, gross - allowedDeductionsInr);
|
|
119
|
+
|
|
120
|
+
// Compute Tax on Slabs
|
|
121
|
+
let taxBeforeRebate = 0;
|
|
122
|
+
const slabs = normalizedRegime === TAX_REGIMES.NEW
|
|
123
|
+
? NEW_REGIME_SLABS_AY2025_26
|
|
124
|
+
: OLD_REGIME_SLABS;
|
|
125
|
+
|
|
126
|
+
let previousMax = 0;
|
|
127
|
+
if (normalizedRegime === TAX_REGIMES.OLD && isSuperSeniorCitizen) {
|
|
128
|
+
previousMax = 500000;
|
|
129
|
+
} else if (normalizedRegime === TAX_REGIMES.OLD && isSeniorCitizen) {
|
|
130
|
+
previousMax = 300000;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const slab of slabs) {
|
|
134
|
+
const slabFloor = Math.max(slab.min, previousMax);
|
|
135
|
+
if (netTaxableIncomeInr > slabFloor) {
|
|
136
|
+
const taxableInThisSlab = Math.min(netTaxableIncomeInr, slab.max) - slabFloor;
|
|
137
|
+
if (taxableInThisSlab > 0 && slab.ratePercent > 0) {
|
|
138
|
+
taxBeforeRebate += taxableInThisSlab * (slab.ratePercent / 100);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Section 87A Rebate
|
|
144
|
+
// New Regime: Taxable income up to ₹7,00,000 gets full rebate (₹25,000 max rebate)
|
|
145
|
+
// Old Regime: Taxable income up to ₹5,00,000 gets full rebate (₹12,500 max rebate)
|
|
146
|
+
let section87ARebateInr = 0;
|
|
147
|
+
if (normalizedRegime === TAX_REGIMES.NEW && netTaxableIncomeInr <= 700000) {
|
|
148
|
+
section87ARebateInr = Math.min(taxBeforeRebate, 25000);
|
|
149
|
+
} else if (normalizedRegime === TAX_REGIMES.OLD && netTaxableIncomeInr <= 500000) {
|
|
150
|
+
section87ARebateInr = Math.min(taxBeforeRebate, 12500);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const taxAfterRebate = Math.max(0, taxBeforeRebate - section87ARebateInr);
|
|
154
|
+
|
|
155
|
+
// 4% Health and Education Cess
|
|
156
|
+
const cessInr = taxAfterRebate > 0 ? taxAfterRebate * 0.04 : 0;
|
|
157
|
+
const totalAnnualTdsInr = roundSection288B(taxAfterRebate + cessInr);
|
|
158
|
+
|
|
159
|
+
// Monthly Withholding Amortization
|
|
160
|
+
const validElapsed = Math.min(11, Math.max(0, Number(monthsElapsedInFy) || 0));
|
|
161
|
+
const remainingMonths = Math.max(1, 12 - validElapsed);
|
|
162
|
+
const priorDeducted = Math.max(0, Number(priorTdsDeductedInr) || 0);
|
|
163
|
+
const unrecoveredTax = Math.max(0, totalAnnualTdsInr - priorDeducted);
|
|
164
|
+
const monthlyTdsDeductionInr = roundSection288B(unrecoveredTax / remainingMonths);
|
|
165
|
+
|
|
166
|
+
const governingSection = isAct2025
|
|
167
|
+
? 'Income-tax Act, 2025 Section 392(1)'
|
|
168
|
+
: (normalizedRegime === TAX_REGIMES.NEW
|
|
169
|
+
? 'Income-tax Act, 1961 Section 192 read with Section 115BAC'
|
|
170
|
+
: 'Income-tax Act, 1961 Section 192 (Old Tax Regime Opt-Out)');
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
status: 'SUCCESS',
|
|
174
|
+
governingSection,
|
|
175
|
+
isAct2025,
|
|
176
|
+
taxRegime: normalizedRegime,
|
|
177
|
+
annualGrossSalaryInr: roundToPaisa(gross),
|
|
178
|
+
totalDeductionsAllowedInr: roundToPaisa(allowedDeductionsInr),
|
|
179
|
+
deductionsBreakdown,
|
|
180
|
+
netTaxableIncomeInr: roundToPaisa(netTaxableIncomeInr),
|
|
181
|
+
taxBeforeRebateInr: roundToPaisa(taxBeforeRebate),
|
|
182
|
+
section87ARebateInr: roundToPaisa(section87ARebateInr),
|
|
183
|
+
taxAfterRebateInr: roundToPaisa(taxAfterRebate),
|
|
184
|
+
healthAndEducationCessInr: roundToPaisa(cessInr),
|
|
185
|
+
totalAnnualTdsInr,
|
|
186
|
+
priorTdsDeductedInr: roundToPaisa(priorDeducted),
|
|
187
|
+
remainingMonthsInFy: remainingMonths,
|
|
188
|
+
monthlyTdsDeductionInr,
|
|
189
|
+
averageTaxRatePercent: gross > 0 ? roundToPaisa((totalAnnualTdsInr / gross) * 100) : 0,
|
|
190
|
+
isAuthorizedToDisburse: false
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Evaluates Employees' Provident Fund (EPF) and Employees' Pension Scheme (EPS)
|
|
196
|
+
* contributions under the Employees' Provident Funds and Miscellaneous Provisions Act, 1952.
|
|
197
|
+
*/
|
|
198
|
+
export function evaluateEpfContribution({
|
|
199
|
+
monthlyBasicAndDaInr,
|
|
200
|
+
epfOnActualWages = false,
|
|
201
|
+
employeeAgeYears = 30,
|
|
202
|
+
isInternationalWorker = false,
|
|
203
|
+
optedOutOfEpf = false
|
|
204
|
+
} = {}) {
|
|
205
|
+
if (optedOutOfEpf) {
|
|
206
|
+
return {
|
|
207
|
+
status: 'OPTED_OUT',
|
|
208
|
+
statutoryWageCeilingInr: EPF_STATUTORY_CONSTANTS.STANDARD_WAGE_CEILING_INR,
|
|
209
|
+
effectiveEpfWageInr: 0,
|
|
210
|
+
employeeContributionInr: 0,
|
|
211
|
+
employerEpsContributionInr: 0,
|
|
212
|
+
employerEpfContributionInr: 0,
|
|
213
|
+
totalEmployerContributionInr: 0,
|
|
214
|
+
totalEpfRemittanceInr: 0,
|
|
215
|
+
isAuthorizedToDisburse: false
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const wage = Math.max(0, Number(monthlyBasicAndDaInr) || 0);
|
|
220
|
+
const age = Number(employeeAgeYears) || 30;
|
|
221
|
+
|
|
222
|
+
// Wage ceiling applies unless employee has joint declaration under Para 26(6) or is International Worker
|
|
223
|
+
const isCeilingApplicable = !epfOnActualWages && !isInternationalWorker;
|
|
224
|
+
const effectiveEpfWageInr = isCeilingApplicable
|
|
225
|
+
? Math.min(wage, EPF_STATUTORY_CONSTANTS.STANDARD_WAGE_CEILING_INR)
|
|
226
|
+
: wage;
|
|
227
|
+
|
|
228
|
+
// Employee Share: Exactly 12% of effective EPF wage
|
|
229
|
+
const employeeContributionInr = roundToPaisa(effectiveEpfWageInr * (EPF_STATUTORY_CONSTANTS.EMPLOYEE_RATE_PERCENT / 100));
|
|
230
|
+
|
|
231
|
+
// Employer Share: Exactly 12% total, split into EPS (8.33%) and EPF (3.67%)
|
|
232
|
+
let employerEpsContributionInr = 0;
|
|
233
|
+
let employerEpfContributionInr = 0;
|
|
234
|
+
|
|
235
|
+
if (age >= EPF_STATUTORY_CONSTANTS.EPS_SUPERANNUATION_AGE_YEARS) {
|
|
236
|
+
// EPS membership ceases at age 58; entire 12% employer share goes to EPF
|
|
237
|
+
employerEpsContributionInr = 0;
|
|
238
|
+
employerEpfContributionInr = employeeContributionInr;
|
|
239
|
+
} else {
|
|
240
|
+
// EPS is calculated at 8.33% of capped wage (max ₹1,250/month)
|
|
241
|
+
const epsWage = Math.min(effectiveEpfWageInr, EPF_STATUTORY_CONSTANTS.STANDARD_WAGE_CEILING_INR);
|
|
242
|
+
// Under EPFO Rule 38(1), EPS contribution rounds to nearest integer rupee (e.g. 15,000 * 8.33% = 1,249.50 -> 1,250)
|
|
243
|
+
employerEpsContributionInr = Math.min(
|
|
244
|
+
EPF_STATUTORY_CONSTANTS.EPS_MAX_MONTHLY_CAP_INR,
|
|
245
|
+
Math.round(epsWage * (EPF_STATUTORY_CONSTANTS.EPS_RATE_PERCENT / 100))
|
|
246
|
+
);
|
|
247
|
+
employerEpfContributionInr = roundToPaisa(employeeContributionInr - employerEpsContributionInr);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const totalEmployerContributionInr = roundToPaisa(employerEpsContributionInr + employerEpfContributionInr);
|
|
251
|
+
const totalEpfRemittanceInr = roundToPaisa(employeeContributionInr + totalEmployerContributionInr);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
status: 'SUCCESS',
|
|
255
|
+
governingStatute: "Employees' Provident Funds and Miscellaneous Provisions Act, 1952",
|
|
256
|
+
statutoryWageCeilingInr: isCeilingApplicable ? EPF_STATUTORY_CONSTANTS.STANDARD_WAGE_CEILING_INR : null,
|
|
257
|
+
epfOnActualWages: Boolean(epfOnActualWages || isInternationalWorker),
|
|
258
|
+
effectiveEpfWageInr: roundToPaisa(effectiveEpfWageInr),
|
|
259
|
+
employeeContributionInr,
|
|
260
|
+
employerEpsContributionInr,
|
|
261
|
+
employerEpfContributionInr,
|
|
262
|
+
totalEmployerContributionInr,
|
|
263
|
+
totalEpfRemittanceInr,
|
|
264
|
+
isSuperannuationAge: age >= EPF_STATUTORY_CONSTANTS.EPS_SUPERANNUATION_AGE_YEARS,
|
|
265
|
+
isAuthorizedToDisburse: false
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Evaluates Employees' State Insurance (ESI) contributions under the ESI Act, 1948.
|
|
271
|
+
*/
|
|
272
|
+
export function evaluateEsiContribution({
|
|
273
|
+
monthlyGrossSalaryInr,
|
|
274
|
+
workingDaysInMonth = 26,
|
|
275
|
+
optedOutOfEsi = false
|
|
276
|
+
} = {}) {
|
|
277
|
+
const gross = Math.max(0, Number(monthlyGrossSalaryInr) || 0);
|
|
278
|
+
|
|
279
|
+
if (optedOutOfEsi || gross > ESI_STATUTORY_CONSTANTS.GROSS_WAGE_CEILING_INR) {
|
|
280
|
+
return {
|
|
281
|
+
status: gross > ESI_STATUTORY_CONSTANTS.GROSS_WAGE_CEILING_INR ? 'EXCEEDS_WAGE_CEILING_EXEMPT' : 'OPTED_OUT',
|
|
282
|
+
isCovered: false,
|
|
283
|
+
monthlyGrossSalaryInr: roundToPaisa(gross),
|
|
284
|
+
wageCeilingInr: ESI_STATUTORY_CONSTANTS.GROSS_WAGE_CEILING_INR,
|
|
285
|
+
employeeContributionInr: 0,
|
|
286
|
+
employerContributionInr: 0,
|
|
287
|
+
totalEsiRemittanceInr: 0,
|
|
288
|
+
isAuthorizedToDisburse: false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Exempt Employee Check: Daily average wage <= ₹176 pays 0% employee share
|
|
293
|
+
const days = Math.max(1, Number(workingDaysInMonth) || 26);
|
|
294
|
+
const dailyAverageWage = gross / days;
|
|
295
|
+
const isExemptEmployee = dailyAverageWage <= ESI_STATUTORY_CONSTANTS.DAILY_EXEMPT_WAGE_FLOOR_INR;
|
|
296
|
+
|
|
297
|
+
const employeeRate = isExemptEmployee ? 0 : (ESI_STATUTORY_CONSTANTS.EMPLOYEE_RATE_PERCENT / 100);
|
|
298
|
+
const employerRate = ESI_STATUTORY_CONSTANTS.EMPLOYER_RATE_PERCENT / 100;
|
|
299
|
+
|
|
300
|
+
const employeeContributionInr = roundToPaisa(gross * employeeRate);
|
|
301
|
+
const employerContributionInr = roundToPaisa(gross * employerRate);
|
|
302
|
+
const totalEsiRemittanceInr = roundToPaisa(employeeContributionInr + employerContributionInr);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
status: 'SUCCESS',
|
|
306
|
+
governingStatute: "Employees' State Insurance Act, 1948",
|
|
307
|
+
isCovered: true,
|
|
308
|
+
isExemptEmployee,
|
|
309
|
+
monthlyGrossSalaryInr: roundToPaisa(gross),
|
|
310
|
+
employeeContributionInr,
|
|
311
|
+
employerContributionInr,
|
|
312
|
+
totalEsiRemittanceInr,
|
|
313
|
+
isAuthorizedToDisburse: false
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Evaluates State Professional Tax (PT) across major Indian jurisdictions.
|
|
319
|
+
*/
|
|
320
|
+
export function evaluateProfessionalTax({
|
|
321
|
+
monthlyGrossSalaryInr,
|
|
322
|
+
stateCode = '27', // Default Maharashtra ('27')
|
|
323
|
+
salaryMonth = 1, // 1-12 (Month 2 = February has special slab in MH)
|
|
324
|
+
gender = 'MALE'
|
|
325
|
+
} = {}) {
|
|
326
|
+
const gross = Math.max(0, Number(monthlyGrossSalaryInr) || 0);
|
|
327
|
+
const cleanState = String(stateCode || '27').trim().toUpperCase();
|
|
328
|
+
const cleanGender = String(gender || 'MALE').trim().toUpperCase();
|
|
329
|
+
const month = Number(salaryMonth) || 1;
|
|
330
|
+
|
|
331
|
+
let ptInr = 0;
|
|
332
|
+
let stateName = 'Other';
|
|
333
|
+
|
|
334
|
+
// Maharashtra (27 / MH)
|
|
335
|
+
if (cleanState === '27' || cleanState === 'MH' || cleanState === 'MAHARASHTRA') {
|
|
336
|
+
stateName = 'Maharashtra';
|
|
337
|
+
// Women earning <= ₹25,000 are exempt in Maharashtra w.e.f. 2023 amendment
|
|
338
|
+
if (cleanGender === 'FEMALE' && gross <= 25000) {
|
|
339
|
+
ptInr = 0;
|
|
340
|
+
} else if (gross <= 7500) {
|
|
341
|
+
ptInr = 0;
|
|
342
|
+
} else if (gross <= 10000) {
|
|
343
|
+
ptInr = 175;
|
|
344
|
+
} else {
|
|
345
|
+
// ₹200 for 11 months, ₹300 in February
|
|
346
|
+
ptInr = (month === 2) ? 300 : 200;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Karnataka (29 / KA)
|
|
350
|
+
else if (cleanState === '29' || cleanState === 'KA' || cleanState === 'KARNATAKA') {
|
|
351
|
+
stateName = 'Karnataka';
|
|
352
|
+
// Gross > ₹25,000 pays ₹200/month (w.e.f. 2023 amendment)
|
|
353
|
+
ptInr = gross > 25000 ? 200 : 0;
|
|
354
|
+
}
|
|
355
|
+
// West Bengal (19 / WB)
|
|
356
|
+
else if (cleanState === '19' || cleanState === 'WB' || cleanState === 'WEST BENGAL') {
|
|
357
|
+
stateName = 'West Bengal';
|
|
358
|
+
if (gross <= 10000) ptInr = 0;
|
|
359
|
+
else if (gross <= 15000) ptInr = 110;
|
|
360
|
+
else if (gross <= 20000) ptInr = 130;
|
|
361
|
+
else if (gross <= 40000) ptInr = 150;
|
|
362
|
+
else ptInr = 200;
|
|
363
|
+
}
|
|
364
|
+
// Gujarat (24 / GJ)
|
|
365
|
+
else if (cleanState === '24' || cleanState === 'GJ' || cleanState === 'GUJARAT') {
|
|
366
|
+
stateName = 'Gujarat';
|
|
367
|
+
if (gross <= 12000) ptInr = 0;
|
|
368
|
+
else ptInr = 200;
|
|
369
|
+
}
|
|
370
|
+
// Telangana (36 / TS) & Andhra Pradesh (37 / AP)
|
|
371
|
+
else if (cleanState === '36' || cleanState === 'TS' || cleanState === '37' || cleanState === 'AP') {
|
|
372
|
+
stateName = 'Telangana / Andhra Pradesh';
|
|
373
|
+
if (gross <= 15000) ptInr = 0;
|
|
374
|
+
else if (gross <= 20000) ptInr = 150;
|
|
375
|
+
else ptInr = 200;
|
|
376
|
+
}
|
|
377
|
+
// Delhi (07), Haryana (06), UP (09) do not levy employee PT
|
|
378
|
+
else {
|
|
379
|
+
stateName = 'Exempt / No PT Jurisdiction';
|
|
380
|
+
ptInr = 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
status: 'SUCCESS',
|
|
385
|
+
stateName,
|
|
386
|
+
stateCode: cleanState,
|
|
387
|
+
monthlyGrossSalaryInr: roundToPaisa(gross),
|
|
388
|
+
salaryMonth: month,
|
|
389
|
+
professionalTaxInr: ptInr,
|
|
390
|
+
isAuthorizedToDisburse: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Composite Line-Item Evaluator for an Individual Employee
|
|
396
|
+
*/
|
|
397
|
+
export function evaluateEmployeeSalaryPackage({
|
|
398
|
+
employeeId,
|
|
399
|
+
employeeName,
|
|
400
|
+
pan,
|
|
401
|
+
bankAccountNumber,
|
|
402
|
+
bankIfscCode,
|
|
403
|
+
monthlyGrossSalaryInr,
|
|
404
|
+
monthlyBasicAndDaInr = null,
|
|
405
|
+
annualGrossSalaryInr = null,
|
|
406
|
+
taxRegime = TAX_REGIMES.NEW,
|
|
407
|
+
form12BBClaims = {},
|
|
408
|
+
epfOnActualWages = false,
|
|
409
|
+
employeeAgeYears = 30,
|
|
410
|
+
stateCode = '27',
|
|
411
|
+
salaryMonth = 1,
|
|
412
|
+
gender = 'MALE',
|
|
413
|
+
otherDeductionsInr = 0,
|
|
414
|
+
transactionDate = null
|
|
415
|
+
} = {}) {
|
|
416
|
+
const grossMonthly = Math.max(0, Number(monthlyGrossSalaryInr) || 0);
|
|
417
|
+
const basicMonthly = monthlyBasicAndDaInr !== null && monthlyBasicAndDaInr !== undefined
|
|
418
|
+
? Math.max(0, Number(monthlyBasicAndDaInr) || 0)
|
|
419
|
+
: roundToPaisa(grossMonthly * 0.50); // Default standard: Basic is 50% of gross
|
|
420
|
+
const grossAnnual = annualGrossSalaryInr !== null && annualGrossSalaryInr !== undefined
|
|
421
|
+
? Math.max(0, Number(annualGrossSalaryInr) || 0)
|
|
422
|
+
: roundToPaisa(grossMonthly * 12);
|
|
423
|
+
|
|
424
|
+
// 1. Salary TDS
|
|
425
|
+
const tdsResult = evaluateSalaryTds({
|
|
426
|
+
annualGrossSalaryInr: grossAnnual,
|
|
427
|
+
taxRegime,
|
|
428
|
+
form12BBClaims,
|
|
429
|
+
transactionDate
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// 2. EPF
|
|
433
|
+
const epfResult = evaluateEpfContribution({
|
|
434
|
+
monthlyBasicAndDaInr: basicMonthly,
|
|
435
|
+
epfOnActualWages,
|
|
436
|
+
employeeAgeYears
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// 3. ESI
|
|
440
|
+
const esiResult = evaluateEsiContribution({
|
|
441
|
+
monthlyGrossSalaryInr: grossMonthly
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// 4. Professional Tax
|
|
445
|
+
const ptResult = evaluateProfessionalTax({
|
|
446
|
+
monthlyGrossSalaryInr: grossMonthly,
|
|
447
|
+
stateCode,
|
|
448
|
+
salaryMonth,
|
|
449
|
+
gender
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const otherDeductions = Math.max(0, Number(otherDeductionsInr) || 0);
|
|
453
|
+
|
|
454
|
+
const totalEmployeeDeductionsInr = roundToPaisa(
|
|
455
|
+
tdsResult.monthlyTdsDeductionInr +
|
|
456
|
+
epfResult.employeeContributionInr +
|
|
457
|
+
esiResult.employeeContributionInr +
|
|
458
|
+
ptResult.professionalTaxInr +
|
|
459
|
+
otherDeductions
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const netSalaryInr = roundToPaisa(Math.max(0, grossMonthly - totalEmployeeDeductionsInr));
|
|
463
|
+
|
|
464
|
+
const totalEmployerContributionsInr = roundToPaisa(
|
|
465
|
+
epfResult.totalEmployerContributionInr +
|
|
466
|
+
esiResult.employerContributionInr
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const totalCostToCompanyInr = roundToPaisa(grossMonthly + totalEmployerContributionsInr);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
status: 'SUCCESS',
|
|
473
|
+
employeeId: String(employeeId || '').trim(),
|
|
474
|
+
employeeName: String(employeeName || '').trim(),
|
|
475
|
+
pan: String(pan || '').toUpperCase().trim(),
|
|
476
|
+
bankAccountNumber: String(bankAccountNumber || '').trim(),
|
|
477
|
+
bankIfscCode: String(bankIfscCode || '').toUpperCase().trim(),
|
|
478
|
+
monthlyGrossSalaryInr: grossMonthly,
|
|
479
|
+
monthlyBasicAndDaInr: basicMonthly,
|
|
480
|
+
annualGrossSalaryInr: grossAnnual,
|
|
481
|
+
withholdings: {
|
|
482
|
+
salaryTdsMonthlyInr: tdsResult.monthlyTdsDeductionInr,
|
|
483
|
+
epfEmployeeInr: epfResult.employeeContributionInr,
|
|
484
|
+
esiEmployeeInr: esiResult.employeeContributionInr,
|
|
485
|
+
professionalTaxInr: ptResult.professionalTaxInr,
|
|
486
|
+
otherDeductionsInr: otherDeductions,
|
|
487
|
+
totalEmployeeDeductionsInr
|
|
488
|
+
},
|
|
489
|
+
employerContributions: {
|
|
490
|
+
epfEmployerTotalInr: epfResult.totalEmployerContributionInr,
|
|
491
|
+
epfEmployerShareInr: epfResult.employerEpfContributionInr,
|
|
492
|
+
epsEmployerShareInr: epfResult.employerEpsContributionInr,
|
|
493
|
+
esiEmployerInr: esiResult.employerContributionInr,
|
|
494
|
+
totalEmployerContributionsInr
|
|
495
|
+
},
|
|
496
|
+
netSalaryInr,
|
|
497
|
+
totalCostToCompanyInr,
|
|
498
|
+
statutoryDetails: {
|
|
499
|
+
tds: tdsResult,
|
|
500
|
+
epf: epfResult,
|
|
501
|
+
esi: esiResult,
|
|
502
|
+
pt: ptResult
|
|
503
|
+
},
|
|
504
|
+
isAuthorizedToDisburse: false
|
|
505
|
+
};
|
|
506
|
+
}
|