@mudraforge/statutory 2.11.0 → 2.12.1
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 +155 -1
- package/CONTRIBUTING.md +2 -2
- package/README.md +1 -1
- package/SECURITY.md +2 -2
- package/SKILL.md +1 -1
- package/bin/cli.js +288 -27
- package/docs/ACCOUNTING_ASSURANCE_AUDIT_DOSSIER.md +276 -0
- package/docs/ARCHITECTURE.md +6 -6
- package/docs/CASE_01_FORM_TEXT.txt +48 -0
- package/docs/CATALOG.md +405 -39
- package/docs/CA_PILOT_BENCHMARK_CASES.md +282 -0
- package/docs/CORPUS_QUALITY.md +31 -22
- package/docs/DOMAIN_BREADTH_AND_DEPTH_COMPARISON.md +1 -1
- package/okf/corporate_ma_restructuring/business_combinations_indas_103/circulars/mca_notification_indas_103_business_combinations.md +41 -0
- package/okf/corporate_ma_restructuring/business_combinations_indas_103/forms/purchase_price_allocation_schedule.json +110 -0
- package/okf/corporate_ma_restructuring/business_combinations_indas_103/section.json +65 -0
- package/okf/corporate_ma_restructuring/impairment_of_assets_indas_36/circulars/mca_notification_indas_36_impairment.md +45 -0
- package/okf/corporate_ma_restructuring/impairment_of_assets_indas_36/forms/cgu_impairment_testing_register.json +53 -0
- package/okf/corporate_ma_restructuring/impairment_of_assets_indas_36/section.json +61 -0
- package/okf/corporate_ma_restructuring/share_based_payments_indas_102/circulars/icai_guidance_share_based_payments.md +37 -0
- package/okf/corporate_ma_restructuring/share_based_payments_indas_102/forms/esop_graded_vesting_amortization_schedule.json +49 -0
- package/okf/corporate_ma_restructuring/share_based_payments_indas_102/section.json +60 -0
- package/okf/direct_tax/income_taxes_deferred_tax_indas_12/circulars/mca_notification_indas_12_income_taxes.md +40 -0
- package/okf/direct_tax/income_taxes_deferred_tax_indas_12/forms/deferred_tax_asset_liability_reconciliation_schedule.json +46 -0
- package/okf/direct_tax/income_taxes_deferred_tax_indas_12/section.json +61 -0
- package/okf/direct_tax/tds_commission_194h/section.json +5 -4
- package/okf/direct_tax/tds_contractor_194c/section.json +3 -3
- package/okf/direct_tax/tds_ecommerce_194o/section.json +3 -3
- package/okf/direct_tax/tds_goods_194q/circulars/cbdt_circular_13_2021_guidelines.md +2 -2
- package/okf/direct_tax/tds_goods_194q/section.json +9 -3
- package/okf/direct_tax/tds_goods_393/section.json +6 -4
- package/okf/direct_tax/tds_individual_contract_194m/section.json +3 -3
- package/okf/direct_tax/tds_interest_194a/section.json +7 -4
- package/okf/direct_tax/tds_perquisites_194r/section.json +1 -1
- package/okf/direct_tax/tds_property_194ia/section.json +3 -3
- package/okf/direct_tax/tds_rent_194i/section.json +5 -4
- package/okf/direct_tax/tds_rent_individual_194ib/section.json +2 -2
- package/okf/direct_tax/tds_services_194j/circulars/cbdt_circular_03_2020_fts_rate_cut.md +1 -1
- package/okf/direct_tax/tds_services_194j/section.json +10 -3
- package/okf/indirect_tax_gst/common_itc_reversal_rule42_43/circulars/cbic_circular_170_2022_itc_reporting.md +30 -0
- package/okf/indirect_tax_gst/common_itc_reversal_rule42_43/circulars/cbic_notification_18_2022_cutoff_amendment.md +26 -0
- package/okf/indirect_tax_gst/common_itc_reversal_rule42_43/forms/gstr_3b_table_4b1_reversal_schedule.json +101 -0
- package/okf/indirect_tax_gst/common_itc_reversal_rule42_43/forms/rule_42_43_annual_reconciliation_worksheet.json +87 -0
- package/okf/indirect_tax_gst/common_itc_reversal_rule42_43/section.json +65 -0
- package/okf/indirect_tax_gst/itc_reversal_rule37_37a/circulars/cbic_circular_192_2023_ecl_interest_calculation.md +36 -0
- package/okf/indirect_tax_gst/itc_reversal_rule37_37a/section.json +8 -1
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/circulars/cbic_notification_07_2025_composition_rent_exclusion.md +41 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/circulars/cbic_notification_09_2024_renting_rcm.md +37 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/circulars/gst_council_55th_meeting_renting_rcm_regularization.md +31 -0
- package/okf/indirect_tax_gst/rcm_liability_sec9_3/section.json +24 -2
- package/okf/international_tax/financial_instruments_ecl_indas_109/circulars/mca_notification_indas_109_financial_instruments.md +43 -0
- package/okf/international_tax/financial_instruments_ecl_indas_109/forms/indas_109_ecl_provision_matrix_schedule.json +43 -0
- package/okf/international_tax/financial_instruments_ecl_indas_109/section.json +62 -0
- package/okf/manifest.json +71 -20
- package/okf/msme_commercial/lease_accounting_indas_116/circulars/mca_notification_indas_116_leases.md +48 -0
- package/okf/msme_commercial/lease_accounting_indas_116/forms/lease_rou_asset_and_liability_schedule.json +55 -0
- package/okf/msme_commercial/lease_accounting_indas_116/section.json +67 -0
- package/okf/msme_commercial/provisions_contingencies_indas_37/circulars/mca_notification_indas_37_provisions.md +36 -0
- package/okf/msme_commercial/provisions_contingencies_indas_37/forms/provisions_and_contingent_liabilities_register.json +38 -0
- package/okf/msme_commercial/provisions_contingencies_indas_37/section.json +61 -0
- package/okf/msme_commercial/revenue_recognition_indas_115/circulars/mca_notification_indas_115_revenue_contracts.md +56 -0
- package/okf/msme_commercial/revenue_recognition_indas_115/forms/revenue_contract_5step_allocation_schedule.json +54 -0
- package/okf/msme_commercial/revenue_recognition_indas_115/section.json +69 -0
- package/package.json +7 -1
- package/src/calculators.js +2195 -260
- package/src/constants.js +1 -1
- package/src/index.js +407 -23
- package/src/tally.js +812 -0
- package/src/zoho_gstr2b.js +2964 -0
- package/types/index.d.ts +476 -0
|
@@ -0,0 +1,2964 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* @MUDRAFORGE/STATUTORY — ZOHO BOOKS & GSTR-2B AP RECONCILIATION ENGINE
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Implements an end-to-end Accounts Payable (AP) purchase register and
|
|
7
|
+
* GSTR-2B automated reconciliation workflow.
|
|
8
|
+
*
|
|
9
|
+
* Statutory Baseline & Accounting Architecture:
|
|
10
|
+
* 1. CGST Act Section 16(2)(aa) & Rule 36(4):
|
|
11
|
+
* - Input Tax Credit (ITC) can only be availed if invoice details have been
|
|
12
|
+
* furnished by the supplier in Form GSTR-1 and communicated to the
|
|
13
|
+
* recipient in Form GSTR-2B.
|
|
14
|
+
* - Distinguishes outright non-filing from recent timing differences (QRMP/IFF).
|
|
15
|
+
* 2. CGST Act Section 17(5) Blocked Credits:
|
|
16
|
+
* - Invoices matching GSTR-2B but falling under blocked statutory categories
|
|
17
|
+
* (e.g., motor vehicles, food & beverages, personal consumption) are
|
|
18
|
+
* segregated into MATCHED_INELIGIBLE_STATUTORY_BLOCK_17_5.
|
|
19
|
+
* 3. Place of Supply (POS) & Distinct Person Jurisdiction (IGST Act Sec 7, 8, 10, 12):
|
|
20
|
+
* - Decouples 4 orthogonal decisions on every transaction:
|
|
21
|
+
* a) Tax Determination (Intrastate vs Interstate correctness under IGST Act),
|
|
22
|
+
* b) Recipient Jurisdiction (Filing GSTIN vs Distinct Person branch GSTIN u/s 25(4)),
|
|
23
|
+
* c) ITC Eligibility (Eligible, Blocked u/s 17(5), Stranded local levy u/s 12(3), or Defective),
|
|
24
|
+
* d) Operational Routing (Claim 4A5, Route to Branch, Book as Expense, Demand Credit Note).
|
|
25
|
+
* 4. CGST Act Section 16(2) Second Proviso read with Rule 37 & Rule 88B(3):
|
|
26
|
+
* - Proportional reversal on partial payments: accounts for value vs tax payment appropriation
|
|
27
|
+
* ('PROPORTIONATE', 'VALUE_FIRST', 'TAX_FIRST').
|
|
28
|
+
* - Reverse Charge (RCM) and Schedule I deemed supplies are statutorily exempt.
|
|
29
|
+
* - Section 50(1) interest is audited against actual Electronic Credit Ledger (ECL) balances
|
|
30
|
+
* per Rule 88B(3). Zero interest applies if ECL balance remained above unreversed ITC.
|
|
31
|
+
* - Subsequent re-availment upon payment (Rule 37(4)) is exempt from Section 16(4) time-bar.
|
|
32
|
+
* 5. Epistemic Uncertainty & Human-in-the-Loop Governance:
|
|
33
|
+
* - When evidence is incomplete (missing POS, invalid GSTIN checksum, duplicate invoice numbers,
|
|
34
|
+
* unlinked credit notes, or missing ECL history), the engine issues an explicit
|
|
35
|
+
* REQUIRES_HUMAN_VERIFICATION disposition rather than manufacturing false certainty.
|
|
36
|
+
* 6. Zero external dependencies: Pure native ES modules.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import crypto from 'node:crypto';
|
|
40
|
+
import {
|
|
41
|
+
cleanNumericAmount,
|
|
42
|
+
toIntegerPaise,
|
|
43
|
+
fromIntegerPaise,
|
|
44
|
+
roundToPaisa,
|
|
45
|
+
isValidGstinChecksum
|
|
46
|
+
} from './calculators.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculates the exact Day 180 overdue date for an invoice under Section 16(2) second proviso.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} billDateIso - ISO date string (YYYY-MM-DD)
|
|
52
|
+
* @returns {string|null}
|
|
53
|
+
*/
|
|
54
|
+
export function calculateOverdueStartDate(billDateIso) {
|
|
55
|
+
if (!billDateIso) return null;
|
|
56
|
+
const d = new Date(billDateIso);
|
|
57
|
+
if (isNaN(d.getTime())) return null;
|
|
58
|
+
d.setDate(d.getDate() + 180);
|
|
59
|
+
return d.toISOString().slice(0, 10);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculates exact chronological interest under Section 50(1) read with Rule 88B(3)
|
|
64
|
+
* across an array of daily or step Electronic Credit Ledger (ECL) balance entries.
|
|
65
|
+
*
|
|
66
|
+
* Implements Explanation (1) to Rule 88B:
|
|
67
|
+
* "Input tax credit wrongly availed shall be construed to have been utilized, when the balance in
|
|
68
|
+
* the electronic credit ledger falls below the amount of input tax credit wrongly availed,
|
|
69
|
+
* and the extent of utilization, shall be the amount by which the balance falls below the amount."
|
|
70
|
+
*
|
|
71
|
+
* Handles:
|
|
72
|
+
* 1. Balance never falling below unreversed ITC -> 0% interest throughout.
|
|
73
|
+
* 2. Temporary dips below unreversed ITC -> interest strictly for the dip days on the dipped portion.
|
|
74
|
+
* 3. Multiple dips and subsequent recoveries -> day-weighted sum across all dip intervals.
|
|
75
|
+
* 4. Partial utilization -> interest calculated strictly on max(0, unreversedItc - balance).
|
|
76
|
+
* 5. Reversal occurring after utilization -> interest ceases on reversal date.
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} params
|
|
79
|
+
* @param {number} params.unreversedItcInr - Total unreversed / wrongly availed ITC amount
|
|
80
|
+
* @param {string} params.overdueStartDate - ISO date string (YYYY-MM-DD) when 180 days expired
|
|
81
|
+
* @param {string} params.asOfDate - ISO date string (YYYY-MM-DD) of assessment / audit
|
|
82
|
+
* @param {string} [params.reversalDate] - Optional ISO date string when taxpayer reversed ITC
|
|
83
|
+
* @param {Array<{ date: string, balanceInr: number }>} params.eclLedgerHistory - Chronological ledger snapshots
|
|
84
|
+
* @param {number} [params.annualInterestRate=0.18] - Statutory annual interest rate (default 18% p.a.)
|
|
85
|
+
* @returns {Object}
|
|
86
|
+
*/
|
|
87
|
+
export function calculateEclUtilizationInterest({
|
|
88
|
+
unreversedItcInr,
|
|
89
|
+
overdueStartDate,
|
|
90
|
+
asOfDate,
|
|
91
|
+
reversalDate = null,
|
|
92
|
+
eclLedgerHistory = [],
|
|
93
|
+
annualInterestRate = 0.18
|
|
94
|
+
}) {
|
|
95
|
+
const unreversedItcPaise = toIntegerPaise(unreversedItcInr, 'unreversedItcInr');
|
|
96
|
+
if (unreversedItcPaise <= 0) {
|
|
97
|
+
return {
|
|
98
|
+
totalInterestInr: 0,
|
|
99
|
+
totalInterestPaise: 0,
|
|
100
|
+
totalOverdueDays: 0,
|
|
101
|
+
utilizedDaysCount: 0,
|
|
102
|
+
zeroUtilizationDaysCount: 0,
|
|
103
|
+
maxWrongfulUtilizationInr: 0,
|
|
104
|
+
chronologicalIntervals: [],
|
|
105
|
+
explanation: 'Unreversed ITC is zero. No interest payable.',
|
|
106
|
+
isAuthorizedToDisburse: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const startNorm = normalizeIsoDate(overdueStartDate);
|
|
111
|
+
const asOfNorm = normalizeIsoDate(asOfDate);
|
|
112
|
+
const reversalNorm = reversalDate ? normalizeIsoDate(reversalDate) : null;
|
|
113
|
+
|
|
114
|
+
if (!startNorm || !asOfNorm) {
|
|
115
|
+
throw new Error('[@mudraforge/statutory] overdueStartDate and asOfDate must be valid ISO date strings (YYYY-MM-DD)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let effectiveEndDate = asOfNorm;
|
|
119
|
+
if (reversalNorm && reversalNorm < effectiveEndDate) {
|
|
120
|
+
effectiveEndDate = reversalNorm;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const startMs = new Date(startNorm).getTime();
|
|
124
|
+
const endMs = new Date(effectiveEndDate).getTime();
|
|
125
|
+
|
|
126
|
+
if (endMs <= startMs) {
|
|
127
|
+
return {
|
|
128
|
+
totalInterestInr: 0,
|
|
129
|
+
totalInterestPaise: 0,
|
|
130
|
+
totalOverdueDays: 0,
|
|
131
|
+
utilizedDaysCount: 0,
|
|
132
|
+
zeroUtilizationDaysCount: 0,
|
|
133
|
+
maxWrongfulUtilizationInr: 0,
|
|
134
|
+
chronologicalIntervals: [],
|
|
135
|
+
explanation: `Reversal or audit date (${effectiveEndDate}) is on or before overdue start date (${startNorm}). Zero overdue days; zero interest payable.`,
|
|
136
|
+
isAuthorizedToDisburse: false
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const totalOverdueDays = Math.round((endMs - startMs) / (1000 * 60 * 60 * 24));
|
|
141
|
+
|
|
142
|
+
const sortedHistory = (Array.isArray(eclLedgerHistory) ? eclLedgerHistory : [])
|
|
143
|
+
.filter(e => e && e.date)
|
|
144
|
+
.map(e => ({
|
|
145
|
+
date: normalizeIsoDate(e.date),
|
|
146
|
+
balancePaise: toIntegerPaise(e.balanceInr, 'eclBalanceInr'),
|
|
147
|
+
igstPaise: e.igstBalanceInr !== undefined ? toIntegerPaise(e.igstBalanceInr) : null,
|
|
148
|
+
cgstPaise: e.cgstBalanceInr !== undefined ? toIntegerPaise(e.cgstBalanceInr) : null,
|
|
149
|
+
sgstPaise: e.sgstBalanceInr !== undefined ? toIntegerPaise(e.sgstBalanceInr) : null,
|
|
150
|
+
cessPaise: e.cessBalanceInr !== undefined ? toIntegerPaise(e.cessBalanceInr) : 0
|
|
151
|
+
}))
|
|
152
|
+
.filter(e => e.date !== null)
|
|
153
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
154
|
+
|
|
155
|
+
if (sortedHistory.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
totalInterestInr: 0,
|
|
158
|
+
totalInterestPaise: 0,
|
|
159
|
+
totalOverdueDays,
|
|
160
|
+
utilizedDaysCount: 0,
|
|
161
|
+
zeroUtilizationDaysCount: 0,
|
|
162
|
+
maxWrongfulUtilizationInr: 0,
|
|
163
|
+
chronologicalIntervals: [],
|
|
164
|
+
explanation: 'Electronic Credit Ledger balance history is empty. Rule 88B(3) utilization cannot be computed without ledger closing balances.',
|
|
165
|
+
isAuthorizedToDisburse: false
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Coalesce same-day entries to net daily closing balance
|
|
170
|
+
const dateMap = new Map();
|
|
171
|
+
for (const entry of sortedHistory) {
|
|
172
|
+
dateMap.set(entry.date, entry);
|
|
173
|
+
}
|
|
174
|
+
const coalescedHistory = Array.from(dateMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
175
|
+
|
|
176
|
+
function getBalancePaiseOnDate(dayIso) {
|
|
177
|
+
let activeBalance = coalescedHistory[0].balancePaise;
|
|
178
|
+
for (const entry of coalescedHistory) {
|
|
179
|
+
if (entry.date <= dayIso) {
|
|
180
|
+
activeBalance = entry.balancePaise;
|
|
181
|
+
} else {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return activeBalance;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let totalInterestPaisePrecise = 0;
|
|
189
|
+
let utilizedDaysCount = 0;
|
|
190
|
+
let zeroUtilizationDaysCount = 0;
|
|
191
|
+
let maxWrongfulUtilizationPaise = 0;
|
|
192
|
+
|
|
193
|
+
const dailyEvaluations = [];
|
|
194
|
+
|
|
195
|
+
let currentMs = startMs;
|
|
196
|
+
while (currentMs < endMs) {
|
|
197
|
+
const currentIso = new Date(currentMs).toISOString().slice(0, 10);
|
|
198
|
+
const dayBalancePaise = getBalancePaiseOnDate(currentIso);
|
|
199
|
+
|
|
200
|
+
const wrongfulUtilizedPaise = Math.max(0, unreversedItcPaise - dayBalancePaise);
|
|
201
|
+
|
|
202
|
+
if (wrongfulUtilizedPaise > 0) {
|
|
203
|
+
utilizedDaysCount++;
|
|
204
|
+
if (wrongfulUtilizedPaise > maxWrongfulUtilizationPaise) {
|
|
205
|
+
maxWrongfulUtilizationPaise = wrongfulUtilizedPaise;
|
|
206
|
+
}
|
|
207
|
+
const dayInterestPaise = wrongfulUtilizedPaise * (annualInterestRate / 365);
|
|
208
|
+
totalInterestPaisePrecise += dayInterestPaise;
|
|
209
|
+
dailyEvaluations.push({
|
|
210
|
+
date: currentIso,
|
|
211
|
+
balanceInr: fromIntegerPaise(dayBalancePaise),
|
|
212
|
+
utilizedInr: fromIntegerPaise(wrongfulUtilizedPaise),
|
|
213
|
+
dayInterestInr: roundToPaisa(dayInterestPaise / 100)
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
zeroUtilizationDaysCount++;
|
|
217
|
+
dailyEvaluations.push({
|
|
218
|
+
date: currentIso,
|
|
219
|
+
balanceInr: fromIntegerPaise(dayBalancePaise),
|
|
220
|
+
utilizedInr: 0,
|
|
221
|
+
dayInterestInr: 0
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
currentMs += 1000 * 60 * 60 * 24;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const chronologicalIntervals = [];
|
|
229
|
+
if (dailyEvaluations.length > 0) {
|
|
230
|
+
let currentInterval = {
|
|
231
|
+
startDate: dailyEvaluations[0].date,
|
|
232
|
+
endDate: dailyEvaluations[0].date,
|
|
233
|
+
days: 1,
|
|
234
|
+
eclBalanceInr: dailyEvaluations[0].balanceInr,
|
|
235
|
+
utilizedAmountInr: dailyEvaluations[0].utilizedInr,
|
|
236
|
+
intervalInterestPaise: dailyEvaluations[0].dayInterestInr * 100
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
for (let i = 1; i < dailyEvaluations.length; i++) {
|
|
240
|
+
const day = dailyEvaluations[i];
|
|
241
|
+
if (day.utilizedInr === currentInterval.utilizedAmountInr && day.balanceInr === currentInterval.eclBalanceInr) {
|
|
242
|
+
currentInterval.endDate = day.date;
|
|
243
|
+
currentInterval.days++;
|
|
244
|
+
currentInterval.intervalInterestPaise += day.dayInterestInr * 100;
|
|
245
|
+
} else {
|
|
246
|
+
currentInterval.intervalInterestInr = fromIntegerPaise(Math.round(currentInterval.intervalInterestPaise));
|
|
247
|
+
delete currentInterval.intervalInterestPaise;
|
|
248
|
+
chronologicalIntervals.push(currentInterval);
|
|
249
|
+
|
|
250
|
+
currentInterval = {
|
|
251
|
+
startDate: day.date,
|
|
252
|
+
endDate: day.date,
|
|
253
|
+
days: 1,
|
|
254
|
+
eclBalanceInr: day.balanceInr,
|
|
255
|
+
utilizedAmountInr: day.utilizedInr,
|
|
256
|
+
intervalInterestPaise: day.dayInterestInr * 100
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
currentInterval.intervalInterestInr = fromIntegerPaise(Math.round(currentInterval.intervalInterestPaise));
|
|
261
|
+
delete currentInterval.intervalInterestPaise;
|
|
262
|
+
chronologicalIntervals.push(currentInterval);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const roundedInterestPaise = Math.round(totalInterestPaisePrecise);
|
|
266
|
+
const totalInterestInr = fromIntegerPaise(roundedInterestPaise);
|
|
267
|
+
const maxWrongfulUtilizationInr = fromIntegerPaise(maxWrongfulUtilizationPaise);
|
|
268
|
+
|
|
269
|
+
let explanation = '';
|
|
270
|
+
if (utilizedDaysCount === 0) {
|
|
271
|
+
explanation = `Rule 88B(3) Proviso: Electronic Credit Ledger balance remained equal to or above unreversed ITC (₹${fromIntegerPaise(unreversedItcPaise).toLocaleString('en-IN')}) for all ${totalOverdueDays} overdue days. Credit was unutilized; ₹0.00 interest payable.`;
|
|
272
|
+
} else {
|
|
273
|
+
explanation = `Rule 88B(3): Electronic Credit Ledger dipped below unreversed ITC (max dip ₹${maxWrongfulUtilizationInr.toLocaleString('en-IN')}) for ${utilizedDaysCount} of ${totalOverdueDays} overdue days. Section 50(1) 18% p.a. interest computed strictly on utilized amounts across dip intervals totaling ₹${totalInterestInr.toLocaleString('en-IN')}.`;
|
|
274
|
+
if (reversalNorm && reversalNorm < asOfNorm) {
|
|
275
|
+
explanation += ` Interest accrual ceased on reversal date (${reversalNorm}).`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
totalInterestInr,
|
|
281
|
+
totalInterestPaise: roundedInterestPaise,
|
|
282
|
+
totalOverdueDays,
|
|
283
|
+
utilizedDaysCount,
|
|
284
|
+
zeroUtilizationDaysCount,
|
|
285
|
+
maxWrongfulUtilizationInr,
|
|
286
|
+
chronologicalIntervals,
|
|
287
|
+
explanation,
|
|
288
|
+
isAuthorizedToDisburse: false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Reconstruct cumulative portfolio ECL utilization for multiple overlapping overdue invoices under Rule 88B(3).
|
|
294
|
+
* Accurately pools overlapping unreversed ITC and computes multi-invoice shortfall against daily ledger balances.
|
|
295
|
+
*
|
|
296
|
+
* @param {Object} params
|
|
297
|
+
* @param {Array<Object>} params.overdueBills
|
|
298
|
+
* @param {Array<{date: string, balanceInr: number}>} params.eclLedgerHistory
|
|
299
|
+
* @param {string} params.asOfDate
|
|
300
|
+
* @param {number} [params.annualInterestRate=0.18]
|
|
301
|
+
* @returns {Object} CumulativePortfolioEclAuditResult
|
|
302
|
+
*/
|
|
303
|
+
export function calculatePortfolioEclUtilizationInterest(params = {}) {
|
|
304
|
+
const overdueBills = Array.isArray(params.overdueBills) ? params.overdueBills : [];
|
|
305
|
+
const eclLedgerHistory = Array.isArray(params.eclLedgerHistory) ? params.eclLedgerHistory : [];
|
|
306
|
+
const asOfDate = normalizeIsoDate(params.asOfDate || new Date().toISOString().slice(0, 10));
|
|
307
|
+
const annualInterestRate = typeof params.annualInterestRate === 'number' ? params.annualInterestRate : 0.18;
|
|
308
|
+
|
|
309
|
+
if (overdueBills.length === 0 || eclLedgerHistory.length === 0) {
|
|
310
|
+
return {
|
|
311
|
+
totalInterestInr: 0,
|
|
312
|
+
totalInterestPaise: 0,
|
|
313
|
+
totalOverdueDays: 0,
|
|
314
|
+
utilizedDaysCount: 0,
|
|
315
|
+
zeroUtilizationDaysCount: 0,
|
|
316
|
+
maxPortfolioUtilizationInr: 0,
|
|
317
|
+
chronologicalIntervals: [],
|
|
318
|
+
explanation: 'Zero overdue bills or empty ECL ledger history for portfolio analysis.',
|
|
319
|
+
isAuthorizedToDisburse: false
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Coalesce same-day entries to net daily closing balance
|
|
324
|
+
const dateMap = new Map();
|
|
325
|
+
for (const e of eclLedgerHistory) {
|
|
326
|
+
if (!e || !e.date) continue;
|
|
327
|
+
const d = normalizeIsoDate(e.date);
|
|
328
|
+
if (!d) continue;
|
|
329
|
+
|
|
330
|
+
const hasHeadwise = (e.igstBalanceInr !== undefined || e.cgstBalanceInr !== undefined || e.sgstBalanceInr !== undefined);
|
|
331
|
+
const igstPaise = toIntegerPaise(e.igstBalanceInr ?? 0);
|
|
332
|
+
const cgstPaise = toIntegerPaise(e.cgstBalanceInr ?? 0);
|
|
333
|
+
const sgstPaise = toIntegerPaise(e.sgstBalanceInr ?? 0);
|
|
334
|
+
const cessPaise = toIntegerPaise(e.cessBalanceInr ?? 0);
|
|
335
|
+
// Exclude Compensation Cess from available general ITC per Circular 192/04/2023-GST
|
|
336
|
+
const balancePaise = hasHeadwise
|
|
337
|
+
? (igstPaise + cgstPaise + sgstPaise)
|
|
338
|
+
: toIntegerPaise(e.balanceInr ?? 0, 'eclBalanceInr');
|
|
339
|
+
|
|
340
|
+
dateMap.set(d, {
|
|
341
|
+
date: d,
|
|
342
|
+
balancePaise,
|
|
343
|
+
hasHeadwise,
|
|
344
|
+
igstPaise,
|
|
345
|
+
cgstPaise,
|
|
346
|
+
sgstPaise,
|
|
347
|
+
cessPaise
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const sortedHistory = Array.from(dateMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
352
|
+
|
|
353
|
+
if (sortedHistory.length === 0) {
|
|
354
|
+
return {
|
|
355
|
+
totalInterestInr: 0,
|
|
356
|
+
totalInterestPaise: 0,
|
|
357
|
+
totalOverdueDays: 0,
|
|
358
|
+
utilizedDaysCount: 0,
|
|
359
|
+
zeroUtilizationDaysCount: 0,
|
|
360
|
+
maxPortfolioUtilizationInr: 0,
|
|
361
|
+
chronologicalIntervals: [],
|
|
362
|
+
explanation: 'Zero valid dates found in ECL ledger history.',
|
|
363
|
+
isAuthorizedToDisburse: false
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let earliestStartMs = Infinity;
|
|
368
|
+
let latestEndMs = -Infinity;
|
|
369
|
+
|
|
370
|
+
const normalizedBills = [];
|
|
371
|
+
for (const b of overdueBills) {
|
|
372
|
+
const start = b.overdueStartDate ? normalizeIsoDate(b.overdueStartDate) : calculateOverdueStartDate(b.billDate);
|
|
373
|
+
if (!start) continue;
|
|
374
|
+
const end = b.reversalDate ? normalizeIsoDate(b.reversalDate) : asOfDate;
|
|
375
|
+
const startMs = new Date(start).getTime();
|
|
376
|
+
const endMs = new Date(end).getTime();
|
|
377
|
+
if (startMs >= endMs) continue;
|
|
378
|
+
|
|
379
|
+
if (startMs < earliestStartMs) earliestStartMs = startMs;
|
|
380
|
+
if (endMs > latestEndMs) latestEndMs = endMs;
|
|
381
|
+
|
|
382
|
+
const revTax = b.unreversedItcInr !== undefined
|
|
383
|
+
? b.unreversedItcInr
|
|
384
|
+
: (b.rule37ReversalTaxInr !== undefined ? b.rule37ReversalTaxInr : (b.reversalTaxInr !== undefined ? b.reversalTaxInr : b.totalTaxInr));
|
|
385
|
+
const unreversedPaise = toIntegerPaise(revTax || 0);
|
|
386
|
+
|
|
387
|
+
const igstPaise = toIntegerPaise(b.igstInr ?? b.igst_amount ?? (b.hasIgst ? revTax : 0));
|
|
388
|
+
const cgstPaise = toIntegerPaise(b.cgstInr ?? b.cgst_amount ?? (!b.hasIgst && b.cgstInr !== undefined ? b.cgstInr : 0));
|
|
389
|
+
const sgstPaise = toIntegerPaise(b.sgstInr ?? b.sgst_amount ?? (!b.hasIgst && b.sgstInr !== undefined ? b.sgstInr : 0));
|
|
390
|
+
|
|
391
|
+
normalizedBills.push({
|
|
392
|
+
billNumber: b.billNumber,
|
|
393
|
+
startIso: start,
|
|
394
|
+
endIso: end,
|
|
395
|
+
unreversedPaise,
|
|
396
|
+
igstPaise,
|
|
397
|
+
cgstPaise,
|
|
398
|
+
sgstPaise
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (normalizedBills.length === 0 || earliestStartMs >= latestEndMs) {
|
|
403
|
+
return {
|
|
404
|
+
totalInterestInr: 0,
|
|
405
|
+
totalInterestPaise: 0,
|
|
406
|
+
totalOverdueDays: 0,
|
|
407
|
+
utilizedDaysCount: 0,
|
|
408
|
+
zeroUtilizationDaysCount: 0,
|
|
409
|
+
maxPortfolioUtilizationInr: 0,
|
|
410
|
+
chronologicalIntervals: [],
|
|
411
|
+
explanation: 'Overdue duration is zero across portfolio bills.',
|
|
412
|
+
isAuthorizedToDisburse: false
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getBalanceEntry(dayIso) {
|
|
417
|
+
let active = sortedHistory[0];
|
|
418
|
+
for (const entry of sortedHistory) {
|
|
419
|
+
if (entry.date <= dayIso) {
|
|
420
|
+
active = entry;
|
|
421
|
+
} else {
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return active;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let currentMs = earliestStartMs;
|
|
429
|
+
let totalInterestPaisePrecise = 0;
|
|
430
|
+
let utilizedDaysCount = 0;
|
|
431
|
+
let zeroUtilizationDaysCount = 0;
|
|
432
|
+
let maxPortfolioUtilizationPaise = 0;
|
|
433
|
+
const dailyEvaluations = [];
|
|
434
|
+
|
|
435
|
+
while (currentMs < latestEndMs) {
|
|
436
|
+
const dayIso = new Date(currentMs).toISOString().slice(0, 10);
|
|
437
|
+
let activePoolPaise = 0;
|
|
438
|
+
let poolIgst = 0;
|
|
439
|
+
let poolCgst = 0;
|
|
440
|
+
let poolSgst = 0;
|
|
441
|
+
|
|
442
|
+
for (const b of normalizedBills) {
|
|
443
|
+
if (dayIso >= b.startIso && dayIso < b.endIso) {
|
|
444
|
+
activePoolPaise += b.unreversedPaise;
|
|
445
|
+
poolIgst += b.igstPaise;
|
|
446
|
+
poolCgst += b.cgstPaise;
|
|
447
|
+
poolSgst += b.sgstPaise;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const dayEntry = getBalanceEntry(dayIso);
|
|
452
|
+
let wrongfulUtilizedPaise = 0;
|
|
453
|
+
|
|
454
|
+
if (dayEntry.hasHeadwise && (poolIgst > 0 || poolCgst > 0 || poolSgst > 0)) {
|
|
455
|
+
// Circular No. 192/04/2023-GST head-wise utilization rules
|
|
456
|
+
// IGST can be paid from IGST + CGST + SGST (Combined pool)
|
|
457
|
+
const combinedPool = dayEntry.igstPaise + dayEntry.cgstPaise + dayEntry.sgstPaise;
|
|
458
|
+
const igstShortfall = Math.max(0, poolIgst - combinedPool);
|
|
459
|
+
|
|
460
|
+
// CGST can be paid from CGST + IGST (SGST not cross-utilizable)
|
|
461
|
+
const cgstAvail = dayEntry.cgstPaise + dayEntry.igstPaise;
|
|
462
|
+
const cgstShortfall = Math.max(0, poolCgst - cgstAvail);
|
|
463
|
+
|
|
464
|
+
// SGST can be paid from SGST + IGST (CGST not cross-utilizable)
|
|
465
|
+
const sgstAvail = dayEntry.sgstPaise + dayEntry.igstPaise;
|
|
466
|
+
const sgstShortfall = Math.max(0, poolSgst - sgstAvail);
|
|
467
|
+
|
|
468
|
+
wrongfulUtilizedPaise = Math.max(igstShortfall, cgstShortfall, sgstShortfall);
|
|
469
|
+
if (wrongfulUtilizedPaise === 0 && (poolIgst + poolCgst + poolSgst) > combinedPool) {
|
|
470
|
+
wrongfulUtilizedPaise = Math.max(0, (poolIgst + poolCgst + poolSgst) - combinedPool);
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
wrongfulUtilizedPaise = Math.max(0, activePoolPaise - dayEntry.balancePaise);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const dayBalPaise = dayEntry.balancePaise;
|
|
477
|
+
|
|
478
|
+
if (wrongfulUtilizedPaise > 0) {
|
|
479
|
+
utilizedDaysCount++;
|
|
480
|
+
if (wrongfulUtilizedPaise > maxPortfolioUtilizationPaise) {
|
|
481
|
+
maxPortfolioUtilizationPaise = wrongfulUtilizedPaise;
|
|
482
|
+
}
|
|
483
|
+
const dayInt = wrongfulUtilizedPaise * (annualInterestRate / 365);
|
|
484
|
+
totalInterestPaisePrecise += dayInt;
|
|
485
|
+
dailyEvaluations.push({
|
|
486
|
+
date: dayIso,
|
|
487
|
+
balanceInr: fromIntegerPaise(dayBalPaise),
|
|
488
|
+
activePoolInr: fromIntegerPaise(activePoolPaise),
|
|
489
|
+
utilizedInr: fromIntegerPaise(wrongfulUtilizedPaise),
|
|
490
|
+
dayInterestInr: roundToPaisa(dayInt / 100)
|
|
491
|
+
});
|
|
492
|
+
} else {
|
|
493
|
+
zeroUtilizationDaysCount++;
|
|
494
|
+
dailyEvaluations.push({
|
|
495
|
+
date: dayIso,
|
|
496
|
+
balanceInr: fromIntegerPaise(dayBalPaise),
|
|
497
|
+
activePoolInr: fromIntegerPaise(activePoolPaise),
|
|
498
|
+
utilizedInr: 0,
|
|
499
|
+
dayInterestInr: 0
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
currentMs += 1000 * 60 * 60 * 24;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const chronologicalIntervals = [];
|
|
507
|
+
if (dailyEvaluations.length > 0) {
|
|
508
|
+
let currentInterval = {
|
|
509
|
+
startDate: dailyEvaluations[0].date,
|
|
510
|
+
endDate: dailyEvaluations[0].date,
|
|
511
|
+
days: 1,
|
|
512
|
+
eclBalanceInr: dailyEvaluations[0].balanceInr,
|
|
513
|
+
activePoolInr: dailyEvaluations[0].activePoolInr,
|
|
514
|
+
utilizedAmountInr: dailyEvaluations[0].utilizedInr,
|
|
515
|
+
intervalInterestPaise: dailyEvaluations[0].dayInterestInr * 100
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
for (let i = 1; i < dailyEvaluations.length; i++) {
|
|
519
|
+
const day = dailyEvaluations[i];
|
|
520
|
+
if (day.utilizedInr === currentInterval.utilizedAmountInr && day.balanceInr === currentInterval.eclBalanceInr && day.activePoolInr === currentInterval.activePoolInr) {
|
|
521
|
+
currentInterval.endDate = day.date;
|
|
522
|
+
currentInterval.days++;
|
|
523
|
+
currentInterval.intervalInterestPaise += day.dayInterestInr * 100;
|
|
524
|
+
} else {
|
|
525
|
+
currentInterval.intervalInterestInr = fromIntegerPaise(Math.round(currentInterval.intervalInterestPaise));
|
|
526
|
+
delete currentInterval.intervalInterestPaise;
|
|
527
|
+
chronologicalIntervals.push(currentInterval);
|
|
528
|
+
|
|
529
|
+
currentInterval = {
|
|
530
|
+
startDate: day.date,
|
|
531
|
+
endDate: day.date,
|
|
532
|
+
days: 1,
|
|
533
|
+
eclBalanceInr: day.balanceInr,
|
|
534
|
+
activePoolInr: day.activePoolInr,
|
|
535
|
+
utilizedAmountInr: day.utilizedInr,
|
|
536
|
+
intervalInterestPaise: day.dayInterestInr * 100
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
currentInterval.intervalInterestInr = fromIntegerPaise(Math.round(currentInterval.intervalInterestPaise));
|
|
541
|
+
delete currentInterval.intervalInterestPaise;
|
|
542
|
+
chronologicalIntervals.push(currentInterval);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const roundedInterestPaise = Math.round(totalInterestPaisePrecise);
|
|
546
|
+
return {
|
|
547
|
+
totalInterestInr: fromIntegerPaise(roundedInterestPaise),
|
|
548
|
+
totalInterestPaise: roundedInterestPaise,
|
|
549
|
+
totalOverdueDays: dailyEvaluations.length,
|
|
550
|
+
utilizedDaysCount,
|
|
551
|
+
zeroUtilizationDaysCount,
|
|
552
|
+
maxPortfolioUtilizationInr: fromIntegerPaise(maxPortfolioUtilizationPaise),
|
|
553
|
+
chronologicalIntervals,
|
|
554
|
+
explanation: utilizedDaysCount === 0
|
|
555
|
+
? `Rule 88B(3) Portfolio Proviso: ECL balance exceeded cumulative unreversed ITC pool for all ${dailyEvaluations.length} days across ${normalizedBills.length} overlapping bills. Zero penal interest payable.`
|
|
556
|
+
: `Rule 88B(3) Portfolio Dip: ECL balance dipped below cumulative unreversed ITC pool on ${utilizedDaysCount} days, reaching max shortfall of INR ${fromIntegerPaise(maxPortfolioUtilizationPaise).toLocaleString('en-IN')}. Total portfolio interest: INR ${fromIntegerPaise(roundedInterestPaise).toLocaleString('en-IN')}.`,
|
|
557
|
+
isAuthorizedToDisburse: false
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Constructs an immutable, self-contained evidence provenance record for a statutory reconciliation item.
|
|
563
|
+
* Enables complete forensic reconstruction of the decision chain months later.
|
|
564
|
+
*
|
|
565
|
+
* @param {Object} params
|
|
566
|
+
* @returns {Object}
|
|
567
|
+
*/
|
|
568
|
+
export function buildEvidenceProvenance({
|
|
569
|
+
bill = null,
|
|
570
|
+
record2b = null,
|
|
571
|
+
matchType = 'UNMATCHED',
|
|
572
|
+
disposition,
|
|
573
|
+
statutorySection,
|
|
574
|
+
jurisdictionDecisions = null,
|
|
575
|
+
uncertaintyReason = null,
|
|
576
|
+
asOfDate = new Date().toISOString().slice(0, 10)
|
|
577
|
+
}) {
|
|
578
|
+
const billNum = bill ? bill.billNumber : null;
|
|
579
|
+
const inv2bNum = record2b ? (record2b.invoiceNumber || record2b.billNumber || record2b.inum) : null;
|
|
580
|
+
const gstin = bill ? bill.vendorGstin : (record2b ? record2b.supplierGstin : null);
|
|
581
|
+
const taxable = bill ? bill.taxableAmountInr : (record2b ? record2b.taxableAmountInr : 0);
|
|
582
|
+
const tax = bill ? bill.totalTaxInr : (record2b ? record2b.totalTaxInr : 0);
|
|
583
|
+
const total = bill ? bill.totalAmountInr : (record2b ? record2b.totalAmountInr : 0);
|
|
584
|
+
|
|
585
|
+
const sigString = `${billNum || 'NO_BILL'}|${inv2bNum || 'NO_2B'}|${gstin || 'NO_GSTIN'}|${total}|${asOfDate}`;
|
|
586
|
+
const provenanceHash = crypto.createHash('sha256').update(sigString).digest('hex').slice(0, 16);
|
|
587
|
+
const provenanceId = `MF-PROV-${provenanceHash}`;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
provenanceId,
|
|
591
|
+
provenanceTimestamp: new Date().toISOString(),
|
|
592
|
+
source: {
|
|
593
|
+
sourceSystem: bill && bill.source ? bill.source : 'ZOHO_BOOKS',
|
|
594
|
+
docType: bill && bill.docType ? bill.docType : 'BILL',
|
|
595
|
+
billId: bill && bill.billId ? bill.billId : billNum,
|
|
596
|
+
billNumber: billNum,
|
|
597
|
+
rowNumber: bill && bill.rowNumber ? bill.rowNumber : null
|
|
598
|
+
},
|
|
599
|
+
externalEvidence: {
|
|
600
|
+
sourceSystem: 'GST_PORTAL_GSTR2B',
|
|
601
|
+
presentInGstr2b: Boolean(record2b),
|
|
602
|
+
invoiceNumber: inv2bNum,
|
|
603
|
+
filingPeriod: record2b && record2b.filingPeriod ? record2b.filingPeriod : null,
|
|
604
|
+
supplierFilingStatus: record2b ? (record2b.itcAvailable === false ? 'PORTAL_INELIGIBLE_FLAGGED' : 'FILED_ELIGIBLE') : 'UNFILED_BY_SUPPLIER',
|
|
605
|
+
portalRevReason: record2b && record2b.itcRevReason ? record2b.itcRevReason : null
|
|
606
|
+
},
|
|
607
|
+
matchingMethod: matchType,
|
|
608
|
+
statutoryBasis: {
|
|
609
|
+
governingStatute: 'Central Goods and Services Tax Act, 2017 / Integrated Goods and Services Tax Act, 2017',
|
|
610
|
+
primarySection: statutorySection || 'CGST Act Section 16',
|
|
611
|
+
officialCitations: [
|
|
612
|
+
'CBIC Circular No. 170/02/2022-GST',
|
|
613
|
+
'CBIC Notification No. 19/2022-Central Tax',
|
|
614
|
+
'Notification No. 14/2022-Central Tax (Rule 88B)'
|
|
615
|
+
]
|
|
616
|
+
},
|
|
617
|
+
inputSnapshot: {
|
|
618
|
+
billNumber: billNum,
|
|
619
|
+
billDate: bill ? bill.billDate : null,
|
|
620
|
+
vendorGstin: gstin,
|
|
621
|
+
taxableAmountInr: taxable,
|
|
622
|
+
totalTaxInr: tax,
|
|
623
|
+
totalAmountInr: total,
|
|
624
|
+
balanceInr: bill ? bill.balanceInr : 0,
|
|
625
|
+
posStateCode: bill ? bill.posStateCode : (record2b ? record2b.posStateCode : null),
|
|
626
|
+
isRcm: bill ? Boolean(bill.isRcm) : (record2b ? Boolean(record2b.isRcm) : false),
|
|
627
|
+
category: bill && bill.category ? bill.category : null
|
|
628
|
+
},
|
|
629
|
+
jurisdictionDecisions: {
|
|
630
|
+
taxClassification: jurisdictionDecisions ? jurisdictionDecisions.taxClassification : 'INDETERMINATE',
|
|
631
|
+
recipientJurisdiction: jurisdictionDecisions ? jurisdictionDecisions.recipientJurisdiction : 'INDETERMINATE',
|
|
632
|
+
itcEligibility: jurisdictionDecisions ? jurisdictionDecisions.itcEligibility : 'INDETERMINATE',
|
|
633
|
+
operationalRouting: jurisdictionDecisions ? jurisdictionDecisions.operationalRouting : 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
634
|
+
},
|
|
635
|
+
uncertaintyAssessment: {
|
|
636
|
+
isCertain: !uncertaintyReason,
|
|
637
|
+
uncertaintyReason: uncertaintyReason || 'NONE',
|
|
638
|
+
actionableRemedy: disposition === 'REQUIRES_HUMAN_VERIFICATION' ? 'MANDATORY_HUMAN_INTERVENTION' : 'AUTOMATED_DISPOSITION'
|
|
639
|
+
},
|
|
640
|
+
governanceTrail: {
|
|
641
|
+
requiresHumanReview: disposition === 'REQUIRES_HUMAN_VERIFICATION',
|
|
642
|
+
auditVerificationDate: asOfDate,
|
|
643
|
+
engineIdentity: '@mudraforge/statutory@2.11.0',
|
|
644
|
+
merkleSubstrateRoot: 'sha256-df76c16ed86673426eeb87fafa1c23712830ce5314339ff17fbb2e0e1795e046',
|
|
645
|
+
isAuthorizedToDisburse: false
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Normalizes an invoice number for fuzzy canonical matching across ERP and GSTN.
|
|
652
|
+
* Strips whitespace, converts to uppercase, removes leading zeroes, and collapses separators.
|
|
653
|
+
*
|
|
654
|
+
* @param {string} raw
|
|
655
|
+
* @returns {{ exactKey: string, alphaNumericKey: string }}
|
|
656
|
+
*/
|
|
657
|
+
export function normalizeInvoiceNumber(raw) {
|
|
658
|
+
if (!raw || typeof raw !== 'string') {
|
|
659
|
+
return { exactKey: '', alphaNumericKey: '' };
|
|
660
|
+
}
|
|
661
|
+
const trimmed = raw.trim().toUpperCase();
|
|
662
|
+
const exactKey = trimmed.replace(/\s+/g, '');
|
|
663
|
+
const alphaNumericKey = trimmed.replace(/[^A-Z0-9]/g, '').replace(/^0+/, '');
|
|
664
|
+
return { exactKey, alphaNumericKey };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Normalizes and validates a GSTIN string.
|
|
669
|
+
*
|
|
670
|
+
* @param {string} raw
|
|
671
|
+
* @returns {{ gstin: string|null, pan: string|null, stateCode: string|null, isValid: boolean }}
|
|
672
|
+
*/
|
|
673
|
+
export function normalizeGstin(raw) {
|
|
674
|
+
if (!raw || typeof raw !== 'string') {
|
|
675
|
+
return { gstin: null, pan: null, stateCode: null, isValid: false };
|
|
676
|
+
}
|
|
677
|
+
const clean = raw.trim().toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
678
|
+
if (clean.length !== 15) {
|
|
679
|
+
return { gstin: clean || null, pan: null, stateCode: null, isValid: false };
|
|
680
|
+
}
|
|
681
|
+
const stateCode = clean.slice(0, 2);
|
|
682
|
+
const pan = clean.slice(2, 12);
|
|
683
|
+
const isValid = isValidGstinChecksum(clean);
|
|
684
|
+
return { gstin: clean, pan, stateCode, isValid };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Parses Zoho Books Bills CSV export into normalized bill records.
|
|
689
|
+
*
|
|
690
|
+
* @param {string} csvText
|
|
691
|
+
* @returns {Array<Object>}
|
|
692
|
+
*/
|
|
693
|
+
export function parseZohoBillsCsv(csvText) {
|
|
694
|
+
if (!csvText || typeof csvText !== 'string') {
|
|
695
|
+
throw new Error('[@mudraforge/statutory] Zoho Books CSV content must be a non-empty string.');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const cleanCsv = csvText.replace(/^\uFEFF/, '');
|
|
699
|
+
const lines = cleanCsv.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
700
|
+
if (lines.length < 2) return [];
|
|
701
|
+
|
|
702
|
+
const firstLine = lines[0];
|
|
703
|
+
const delimiter = !firstLine.includes(',') && firstLine.includes(';') ? ';' : (!firstLine.includes(',') && firstLine.includes('\t') ? '\t' : ',');
|
|
704
|
+
|
|
705
|
+
const rawHeaders = parseCsvRow(lines[0], delimiter);
|
|
706
|
+
const headers = rawHeaders.map(h => h.toLowerCase().replace(/[^a-z0-9_]/g, '_'));
|
|
707
|
+
|
|
708
|
+
const colIndex = {
|
|
709
|
+
date: findColIndex(headers, ['bill_date', 'date', 'invoice_date']),
|
|
710
|
+
billNo: findColIndex(headers, ['bill_number', 'bill_no', 'invoice_number', 'bill__', 'bill']),
|
|
711
|
+
dueDate: findColIndex(headers, ['due_date', 'payment_due_date']),
|
|
712
|
+
vendorName: findColIndex(headers, ['vendor_name', 'vendor', 'supplier_name', 'party_name']),
|
|
713
|
+
gstin: findColIndex(headers, ['gst_identification_number__gstin_', 'gstin', 'vendor_gstin', 'party_gstin']),
|
|
714
|
+
pos: findColIndex(headers, ['place_of_supply', 'destination_state', 'pos_state', 'pos']),
|
|
715
|
+
status: findColIndex(headers, ['status', 'bill_status']),
|
|
716
|
+
taxableAmount: findColIndex(headers, ['taxable_amount', 'subtotal', 'item_total', 'basic_amount']),
|
|
717
|
+
totalAmount: findColIndex(headers, ['total', 'bill_amount', 'amount', 'gross_amount']),
|
|
718
|
+
balance: findColIndex(headers, ['balance', 'balance_due', 'amount_due', 'outstanding']),
|
|
719
|
+
cgst: findColIndex(headers, ['cgst', 'cgst_amount']),
|
|
720
|
+
sgst: findColIndex(headers, ['sgst', 'sgst_amount']),
|
|
721
|
+
igst: findColIndex(headers, ['igst', 'igst_amount']),
|
|
722
|
+
category: findColIndex(headers, ['account_name', 'item_category', 'category', 'expense_account']),
|
|
723
|
+
isRcm: findColIndex(headers, ['is_reverse_charge_applied', 'reverse_charge', 'is_rcm', 'rcm'])
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
if (colIndex.billNo === -1 || (colIndex.totalAmount === -1 && colIndex.taxableAmount === -1)) {
|
|
727
|
+
throw new Error('[@mudraforge/statutory] Zoho Books CSV must include a Bill Number and Total/Taxable Amount column.');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const bills = [];
|
|
731
|
+
|
|
732
|
+
for (let i = 1; i < lines.length; i++) {
|
|
733
|
+
const row = parseCsvRow(lines[i], delimiter);
|
|
734
|
+
if (row.length === 0 || row.every(cell => !cell || !cell.trim())) continue;
|
|
735
|
+
|
|
736
|
+
const rawDate = colIndex.date !== -1 ? row[colIndex.date] : null;
|
|
737
|
+
const billDate = normalizeIsoDate(rawDate);
|
|
738
|
+
|
|
739
|
+
const rawDueDate = colIndex.dueDate !== -1 ? row[colIndex.dueDate] : null;
|
|
740
|
+
const dueDate = normalizeIsoDate(rawDueDate);
|
|
741
|
+
|
|
742
|
+
const billNumber = colIndex.billNo !== -1 ? (row[colIndex.billNo] || `BILL-${i}`).trim() : `BILL-${i}`;
|
|
743
|
+
const vendorName = colIndex.vendorName !== -1 ? (row[colIndex.vendorName] || 'Unknown Vendor').trim() : 'Unknown Vendor';
|
|
744
|
+
const rawGstin = colIndex.gstin !== -1 ? (row[colIndex.gstin] || '').trim().toUpperCase() : '';
|
|
745
|
+
const normalizedGstinObj = normalizeGstin(rawGstin);
|
|
746
|
+
|
|
747
|
+
const status = colIndex.status !== -1 ? (row[colIndex.status] || 'unpaid').trim().toLowerCase() : 'unpaid';
|
|
748
|
+
const category = colIndex.category !== -1 ? (row[colIndex.category] || '').trim() : '';
|
|
749
|
+
const rawPos = colIndex.pos !== -1 ? (row[colIndex.pos] || '').trim() : '';
|
|
750
|
+
|
|
751
|
+
const rawTaxable = colIndex.taxableAmount !== -1 ? row[colIndex.taxableAmount] : '0';
|
|
752
|
+
const rawTotal = colIndex.totalAmount !== -1 ? row[colIndex.totalAmount] : rawTaxable;
|
|
753
|
+
const rawBalance = colIndex.balance !== -1 ? row[colIndex.balance] : '0';
|
|
754
|
+
|
|
755
|
+
const rawCgst = colIndex.cgst !== -1 ? row[colIndex.cgst] : '0';
|
|
756
|
+
const rawSgst = colIndex.sgst !== -1 ? row[colIndex.sgst] : '0';
|
|
757
|
+
const rawIgst = colIndex.igst !== -1 ? row[colIndex.igst] : '0';
|
|
758
|
+
|
|
759
|
+
const isCommercialRentRcm = /commercial.*rent|office.*rent|property.*rent|warehouse.*rent|renting.*commercial/i.test(category) &&
|
|
760
|
+
(!normalizedGstinObj.gstin || normalizedGstinObj.gstin === 'UNREGISTERED' || normalizedGstinObj.gstin.length < 15) &&
|
|
761
|
+
(!billDate || billDate >= '2024-10-10');
|
|
762
|
+
const isRcm = Boolean((colIndex.isRcm !== -1 ? /true|yes|y|1/i.test(row[colIndex.isRcm] || '') : false) || isCommercialRentRcm);
|
|
763
|
+
|
|
764
|
+
const totalAmount = cleanNumericAmount(rawTotal);
|
|
765
|
+
const taxableAmount = colIndex.taxableAmount !== -1 ? cleanNumericAmount(rawTaxable) : totalAmount;
|
|
766
|
+
const balance = cleanNumericAmount(rawBalance);
|
|
767
|
+
|
|
768
|
+
const cgst = cleanNumericAmount(rawCgst);
|
|
769
|
+
const sgst = cleanNumericAmount(rawSgst);
|
|
770
|
+
const igst = cleanNumericAmount(rawIgst);
|
|
771
|
+
const totalTax = roundToPaisa(cgst + sgst + igst);
|
|
772
|
+
|
|
773
|
+
const { exactKey, alphaNumericKey } = normalizeInvoiceNumber(billNumber);
|
|
774
|
+
|
|
775
|
+
// Check if category indicates Section 17(5) blocked credit
|
|
776
|
+
const isBlocked17_5 = /motor|vehicle|food|beverage|catering|club|fitness|personal|gift/i.test(category);
|
|
777
|
+
|
|
778
|
+
bills.push({
|
|
779
|
+
source: 'ZOHO_BOOKS',
|
|
780
|
+
docType: 'BILL',
|
|
781
|
+
rowNumber: i + 1,
|
|
782
|
+
billNumber,
|
|
783
|
+
exactKey,
|
|
784
|
+
alphaNumericKey,
|
|
785
|
+
billDate,
|
|
786
|
+
dueDate,
|
|
787
|
+
vendorName,
|
|
788
|
+
vendorGstin: normalizedGstinObj.gstin,
|
|
789
|
+
vendorPan: normalizedGstinObj.pan,
|
|
790
|
+
isGstinValid: normalizedGstinObj.isValid,
|
|
791
|
+
posStateCode: rawPos ? rawPos.slice(0, 2) : (normalizedGstinObj.stateCode || null),
|
|
792
|
+
status,
|
|
793
|
+
category,
|
|
794
|
+
isBlocked17_5,
|
|
795
|
+
isRcm,
|
|
796
|
+
isCommercialRentRcm,
|
|
797
|
+
isSchedule1DeemedSupply: /distinct|related|deemed|schedule\s*1/i.test(category),
|
|
798
|
+
taxableAmountInr: roundToPaisa(taxableAmount),
|
|
799
|
+
cgstInr: roundToPaisa(cgst),
|
|
800
|
+
sgstInr: roundToPaisa(sgst),
|
|
801
|
+
igstInr: roundToPaisa(igst),
|
|
802
|
+
totalTaxInr: totalTax,
|
|
803
|
+
totalAmountInr: roundToPaisa(totalAmount),
|
|
804
|
+
balanceInr: roundToPaisa(balance),
|
|
805
|
+
isFullyPaid: balance <= 0 || status === 'paid'
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return bills;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Parses Zoho Books API JSON response into normalized bill and vendor credit records.
|
|
814
|
+
* Supports bills array, vendor_credits array, line items, and pagination context.
|
|
815
|
+
*
|
|
816
|
+
* @param {Object|string} jsonData - Raw Zoho Books API response object or JSON string
|
|
817
|
+
* @returns {Array<Object>}
|
|
818
|
+
*/
|
|
819
|
+
export function parseZohoBillsJson(jsonData) {
|
|
820
|
+
const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
|
821
|
+
|
|
822
|
+
// Handle Zoho API error responses
|
|
823
|
+
if (data && typeof data.code === 'number' && data.code !== 0) {
|
|
824
|
+
throw new Error(`[@mudraforge/statutory] Zoho Books API Error (code ${data.code}): ${data.message || 'Unknown API failure'}`);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const rawBills = Array.isArray(data) ? data : (Array.isArray(data.bills) ? data.bills : []);
|
|
828
|
+
const rawCredits = Array.isArray(data.vendor_credits) ? data.vendor_credits : [];
|
|
829
|
+
|
|
830
|
+
const records = [];
|
|
831
|
+
|
|
832
|
+
// 1. Process Bills
|
|
833
|
+
for (let idx = 0; idx < rawBills.length; idx++) {
|
|
834
|
+
const b = rawBills[idx];
|
|
835
|
+
const billNumber = (b.bill_number || b.invoice_number || `ZOHO-BILL-${idx + 1}`).trim();
|
|
836
|
+
const { exactKey, alphaNumericKey } = normalizeInvoiceNumber(billNumber);
|
|
837
|
+
const rawGstin = (b.gst_no || b.gstin || b.vendor_gstin || '').trim().toUpperCase();
|
|
838
|
+
const normalizedGstinObj = normalizeGstin(rawGstin);
|
|
839
|
+
|
|
840
|
+
const taxableAmount = cleanNumericAmount(b.sub_total || b.taxable_amount || b.total || 0);
|
|
841
|
+
const totalAmount = cleanNumericAmount(b.total || taxableAmount);
|
|
842
|
+
const balance = cleanNumericAmount(b.balance !== undefined ? b.balance : (b.status === 'paid' ? 0 : totalAmount));
|
|
843
|
+
|
|
844
|
+
const cgst = cleanNumericAmount(b.cgst_amount || 0);
|
|
845
|
+
const sgst = cleanNumericAmount(b.sgst_amount || 0);
|
|
846
|
+
const igst = cleanNumericAmount(b.igst_amount || 0);
|
|
847
|
+
const totalTax = roundToPaisa(cgst + sgst + igst);
|
|
848
|
+
|
|
849
|
+
let category = String(b.category_name || b.account_name || '').trim();
|
|
850
|
+
let isBlocked17_5 = Boolean(b.is_blocked_17_5) || /motor|vehicle|food|beverage|catering|club|fitness|personal|gift/i.test(category);
|
|
851
|
+
|
|
852
|
+
// Inspect line items if present
|
|
853
|
+
if (Array.isArray(b.line_items) && b.line_items.length > 0) {
|
|
854
|
+
for (const item of b.line_items) {
|
|
855
|
+
const itemCat = String(item.account_name || item.name || item.description || '');
|
|
856
|
+
if (/motor|vehicle|food|beverage|catering|club|fitness|personal|gift/i.test(itemCat)) {
|
|
857
|
+
isBlocked17_5 = true;
|
|
858
|
+
if (!category) category = itemCat;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const billDateNorm = normalizeIsoDate(b.date || b.bill_date);
|
|
864
|
+
const isCommercialRentRcm = /commercial.*rent|office.*rent|property.*rent|warehouse.*rent|renting.*commercial/i.test(category) &&
|
|
865
|
+
(!normalizedGstinObj.gstin || normalizedGstinObj.gstin === 'UNREGISTERED' || normalizedGstinObj.gstin.length < 15) &&
|
|
866
|
+
(!billDateNorm || billDateNorm >= '2024-10-10');
|
|
867
|
+
const isRcm = Boolean(b.is_reverse_charge_applied || b.reverse_charge_vat_registered || b.is_rcm || isCommercialRentRcm);
|
|
868
|
+
const rawPos = String(b.place_of_supply || b.destination_state || '').trim();
|
|
869
|
+
|
|
870
|
+
records.push({
|
|
871
|
+
source: 'ZOHO_BOOKS_API',
|
|
872
|
+
docType: 'BILL',
|
|
873
|
+
rowNumber: idx + 1,
|
|
874
|
+
billId: b.bill_id || null,
|
|
875
|
+
billNumber,
|
|
876
|
+
exactKey,
|
|
877
|
+
alphaNumericKey,
|
|
878
|
+
billDate: normalizeIsoDate(b.date || b.bill_date),
|
|
879
|
+
dueDate: normalizeIsoDate(b.due_date),
|
|
880
|
+
vendorName: (b.vendor_name || 'Unknown Vendor').trim(),
|
|
881
|
+
vendorGstin: normalizedGstinObj.gstin,
|
|
882
|
+
vendorPan: normalizedGstinObj.pan,
|
|
883
|
+
isGstinValid: normalizedGstinObj.isValid,
|
|
884
|
+
posStateCode: rawPos ? rawPos.slice(0, 2) : (normalizedGstinObj.stateCode || null),
|
|
885
|
+
status: String(b.status || 'unpaid').toLowerCase(),
|
|
886
|
+
category,
|
|
887
|
+
isBlocked17_5,
|
|
888
|
+
isRcm,
|
|
889
|
+
isCommercialRentRcm,
|
|
890
|
+
isSchedule1DeemedSupply: /distinct|related|deemed|schedule\s*1/i.test(category),
|
|
891
|
+
taxableAmountInr: roundToPaisa(taxableAmount),
|
|
892
|
+
cgstInr: roundToPaisa(cgst),
|
|
893
|
+
sgstInr: roundToPaisa(sgst),
|
|
894
|
+
igstInr: roundToPaisa(igst),
|
|
895
|
+
totalTaxInr: totalTax,
|
|
896
|
+
totalAmountInr: roundToPaisa(totalAmount),
|
|
897
|
+
balanceInr: roundToPaisa(balance),
|
|
898
|
+
isFullyPaid: balance <= 0 || b.status === 'paid'
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// 2. Process Vendor Credits (Credit Notes)
|
|
903
|
+
for (let cIdx = 0; cIdx < rawCredits.length; cIdx++) {
|
|
904
|
+
const c = rawCredits[cIdx];
|
|
905
|
+
const creditNumber = (c.vendor_credit_number || c.credit_note_number || `ZOHO-CREDIT-${cIdx + 1}`).trim();
|
|
906
|
+
const { exactKey, alphaNumericKey } = normalizeInvoiceNumber(creditNumber);
|
|
907
|
+
const rawGstin = (c.gst_no || c.gstin || c.vendor_gstin || '').trim().toUpperCase();
|
|
908
|
+
const normalizedGstinObj = normalizeGstin(rawGstin);
|
|
909
|
+
|
|
910
|
+
const rawTaxable = cleanNumericAmount(c.sub_total || c.taxable_amount || c.total || 0);
|
|
911
|
+
const rawTotal = cleanNumericAmount(c.total || rawTaxable);
|
|
912
|
+
const rawCgst = cleanNumericAmount(c.cgst_amount || 0);
|
|
913
|
+
const rawSgst = cleanNumericAmount(c.sgst_amount || 0);
|
|
914
|
+
const rawIgst = cleanNumericAmount(c.igst_amount || 0);
|
|
915
|
+
|
|
916
|
+
records.push({
|
|
917
|
+
source: 'ZOHO_BOOKS_API',
|
|
918
|
+
docType: 'VENDOR_CREDIT',
|
|
919
|
+
rowNumber: rawBills.length + cIdx + 1,
|
|
920
|
+
billId: c.vendor_credit_id || null,
|
|
921
|
+
billNumber: creditNumber,
|
|
922
|
+
originalInvoiceNumber: c.reference_number || c.bill_number || null,
|
|
923
|
+
exactKey,
|
|
924
|
+
alphaNumericKey,
|
|
925
|
+
billDate: normalizeIsoDate(c.date),
|
|
926
|
+
dueDate: null,
|
|
927
|
+
vendorName: (c.vendor_name || 'Unknown Vendor').trim(),
|
|
928
|
+
vendorGstin: normalizedGstinObj.gstin,
|
|
929
|
+
vendorPan: normalizedGstinObj.pan,
|
|
930
|
+
isGstinValid: normalizedGstinObj.isValid,
|
|
931
|
+
posStateCode: normalizedGstinObj.stateCode || null,
|
|
932
|
+
status: String(c.status || 'open').toLowerCase(),
|
|
933
|
+
category: 'Vendor Credit / Purchase Return',
|
|
934
|
+
isBlocked17_5: false,
|
|
935
|
+
isRcm: false,
|
|
936
|
+
isSchedule1DeemedSupply: false,
|
|
937
|
+
taxableAmountInr: roundToPaisa(-Math.abs(rawTaxable)),
|
|
938
|
+
cgstInr: roundToPaisa(-Math.abs(rawCgst)),
|
|
939
|
+
sgstInr: roundToPaisa(-Math.abs(rawSgst)),
|
|
940
|
+
igstInr: roundToPaisa(-Math.abs(rawIgst)),
|
|
941
|
+
totalTaxInr: roundToPaisa(-Math.abs(rawCgst + rawSgst + rawIgst)),
|
|
942
|
+
totalAmountInr: roundToPaisa(-Math.abs(rawTotal)),
|
|
943
|
+
balanceInr: 0,
|
|
944
|
+
isFullyPaid: true
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (data && data.page_context && typeof data.page_context === 'object') {
|
|
949
|
+
records.pageContext = {
|
|
950
|
+
page: Number(data.page_context.page) || 1,
|
|
951
|
+
perPage: Number(data.page_context.per_page) || 200,
|
|
952
|
+
hasMorePage: Boolean(data.page_context.has_more_page),
|
|
953
|
+
total: Number(data.page_context.total) || records.length
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return records;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Parses GST Portal GSTR-2B JSON data into flat normalized invoice entries.
|
|
962
|
+
* Supports standard GSTN schema including b2b and cdnr sections.
|
|
963
|
+
*
|
|
964
|
+
* @param {Object|string} gstr2bData - Raw GSTR-2B JSON or parsed object
|
|
965
|
+
* @returns {Array<Object>}
|
|
966
|
+
*/
|
|
967
|
+
export function parseGstr2bJson(gstr2bData) {
|
|
968
|
+
const root = typeof gstr2bData === 'string' ? JSON.parse(gstr2bData) : gstr2bData;
|
|
969
|
+
const data = root.data || root;
|
|
970
|
+
const recipientGstin = (root.gstin || data.gstin || '').trim().toUpperCase();
|
|
971
|
+
|
|
972
|
+
const normalizedRecords = [];
|
|
973
|
+
|
|
974
|
+
// 1. B2B Invoices
|
|
975
|
+
const b2bList = Array.isArray(data.b2b) ? data.b2b : [];
|
|
976
|
+
for (const supplier of b2bList) {
|
|
977
|
+
const supplierGstin = String(supplier.ctin || '').trim().toUpperCase();
|
|
978
|
+
const normSupplierGstin = normalizeGstin(supplierGstin);
|
|
979
|
+
const supplierName = (supplier.cname || supplier.trade_name || 'Registered Supplier').trim();
|
|
980
|
+
|
|
981
|
+
const invoices = Array.isArray(supplier.inv) ? supplier.inv : [];
|
|
982
|
+
for (const inv of invoices) {
|
|
983
|
+
const rawInvNum = String(inv.inum || '').trim();
|
|
984
|
+
const { exactKey, alphaNumericKey } = normalizeInvoiceNumber(rawInvNum);
|
|
985
|
+
|
|
986
|
+
const invoiceDate = parseGstPortalDate(inv.idt);
|
|
987
|
+
const totalInvoiceValue = cleanNumericAmount(inv.val || 0);
|
|
988
|
+
const posStateCode = String(inv.pos || '').trim();
|
|
989
|
+
const isRcm = String(inv.rchrg || 'N').trim().toUpperCase() === 'Y';
|
|
990
|
+
const itcAvailable = String(inv.itcavl || 'Y').trim().toUpperCase() === 'Y';
|
|
991
|
+
const itcRevReason = String(inv.rsn || '').trim();
|
|
992
|
+
|
|
993
|
+
let taxableSum = 0;
|
|
994
|
+
let igstSum = 0;
|
|
995
|
+
let cgstSum = 0;
|
|
996
|
+
let sgstSum = 0;
|
|
997
|
+
let cessSum = 0;
|
|
998
|
+
|
|
999
|
+
const items = Array.isArray(inv.items || inv.itm_det) ? (inv.items || inv.itm_det) : [];
|
|
1000
|
+
for (const it of items) {
|
|
1001
|
+
taxableSum += cleanNumericAmount(it.txval || 0);
|
|
1002
|
+
igstSum += cleanNumericAmount(it.igst || 0);
|
|
1003
|
+
cgstSum += cleanNumericAmount(it.cgst || 0);
|
|
1004
|
+
sgstSum += cleanNumericAmount(it.sgst || 0);
|
|
1005
|
+
cessSum += cleanNumericAmount(it.cess || 0);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const totalTax = roundToPaisa(igstSum + cgstSum + sgstSum + cessSum);
|
|
1009
|
+
|
|
1010
|
+
normalizedRecords.push({
|
|
1011
|
+
source: 'GSTR_2B',
|
|
1012
|
+
docType: 'INVOICE',
|
|
1013
|
+
invoiceNumber: rawInvNum,
|
|
1014
|
+
exactKey,
|
|
1015
|
+
alphaNumericKey,
|
|
1016
|
+
invoiceDate,
|
|
1017
|
+
supplierGstin: normSupplierGstin.gstin,
|
|
1018
|
+
supplierPan: normSupplierGstin.pan,
|
|
1019
|
+
supplierName,
|
|
1020
|
+
recipientGstin,
|
|
1021
|
+
posStateCode,
|
|
1022
|
+
isRcm,
|
|
1023
|
+
itcAvailable,
|
|
1024
|
+
itcRevReason: itcRevReason || null,
|
|
1025
|
+
taxableAmountInr: roundToPaisa(taxableSum),
|
|
1026
|
+
igstInr: roundToPaisa(igstSum),
|
|
1027
|
+
cgstInr: roundToPaisa(cgstSum),
|
|
1028
|
+
sgstInr: roundToPaisa(sgstSum),
|
|
1029
|
+
cessInr: roundToPaisa(cessSum),
|
|
1030
|
+
totalTaxInr: totalTax,
|
|
1031
|
+
totalAmountInr: roundToPaisa(totalInvoiceValue || (taxableSum + totalTax))
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// 2. CDNR Credit / Debit Notes
|
|
1037
|
+
const cdnrList = Array.isArray(data.cdnr) ? data.cdnr : [];
|
|
1038
|
+
for (const supplier of cdnrList) {
|
|
1039
|
+
const supplierGstin = String(supplier.ctin || '').trim().toUpperCase();
|
|
1040
|
+
const normSupplierGstin = normalizeGstin(supplierGstin);
|
|
1041
|
+
const supplierName = (supplier.cname || supplier.trade_name || 'Registered Supplier').trim();
|
|
1042
|
+
|
|
1043
|
+
const notes = Array.isArray(supplier.nt) ? supplier.nt : [];
|
|
1044
|
+
for (const nt of notes) {
|
|
1045
|
+
const rawNoteNum = String(nt.nt_num || '').trim();
|
|
1046
|
+
const { exactKey, alphaNumericKey } = normalizeInvoiceNumber(rawNoteNum);
|
|
1047
|
+
const noteDate = parseGstPortalDate(nt.nt_dt);
|
|
1048
|
+
const originalInvoiceNum = String(nt.inum || '').trim();
|
|
1049
|
+
const noteType = String(nt.typ || 'C').toUpperCase(); // 'C' = Credit Note, 'D' = Debit Note
|
|
1050
|
+
const isCreditNote = noteType === 'C';
|
|
1051
|
+
|
|
1052
|
+
let multiplier = isCreditNote ? -1 : 1;
|
|
1053
|
+
const totalNoteValue = cleanNumericAmount(nt.val || 0) * multiplier;
|
|
1054
|
+
const posStateCode = String(nt.pos || '').trim();
|
|
1055
|
+
const isRcm = String(nt.rchrg || 'N').trim().toUpperCase() === 'Y';
|
|
1056
|
+
const itcAvailable = String(nt.itcavl || 'Y').trim().toUpperCase() === 'Y';
|
|
1057
|
+
const itcRevReason = String(nt.rsn || '').trim();
|
|
1058
|
+
|
|
1059
|
+
let taxableSum = 0;
|
|
1060
|
+
let igstSum = 0;
|
|
1061
|
+
let cgstSum = 0;
|
|
1062
|
+
let sgstSum = 0;
|
|
1063
|
+
let cessSum = 0;
|
|
1064
|
+
|
|
1065
|
+
const items = Array.isArray(nt.items || nt.itm_det) ? (nt.items || nt.itm_det) : [];
|
|
1066
|
+
for (const it of items) {
|
|
1067
|
+
taxableSum += cleanNumericAmount(it.txval || 0) * multiplier;
|
|
1068
|
+
igstSum += cleanNumericAmount(it.igst || 0) * multiplier;
|
|
1069
|
+
cgstSum += cleanNumericAmount(it.cgst || 0) * multiplier;
|
|
1070
|
+
sgstSum += cleanNumericAmount(it.sgst || 0) * multiplier;
|
|
1071
|
+
cessSum += cleanNumericAmount(it.cess || 0) * multiplier;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const totalTax = roundToPaisa(igstSum + cgstSum + sgstSum + cessSum);
|
|
1075
|
+
|
|
1076
|
+
normalizedRecords.push({
|
|
1077
|
+
source: 'GSTR_2B',
|
|
1078
|
+
docType: isCreditNote ? 'CREDIT_NOTE' : 'DEBIT_NOTE',
|
|
1079
|
+
invoiceNumber: rawNoteNum,
|
|
1080
|
+
originalInvoiceNumber: originalInvoiceNum,
|
|
1081
|
+
exactKey,
|
|
1082
|
+
alphaNumericKey,
|
|
1083
|
+
invoiceDate: noteDate,
|
|
1084
|
+
supplierGstin: normSupplierGstin.gstin,
|
|
1085
|
+
supplierPan: normSupplierGstin.pan,
|
|
1086
|
+
supplierName,
|
|
1087
|
+
recipientGstin,
|
|
1088
|
+
posStateCode,
|
|
1089
|
+
isRcm,
|
|
1090
|
+
itcAvailable,
|
|
1091
|
+
itcRevReason: itcRevReason || null,
|
|
1092
|
+
taxableAmountInr: roundToPaisa(taxableSum),
|
|
1093
|
+
igstInr: roundToPaisa(igstSum),
|
|
1094
|
+
cgstInr: roundToPaisa(cgstSum),
|
|
1095
|
+
sgstInr: roundToPaisa(sgstSum),
|
|
1096
|
+
cessInr: roundToPaisa(cessSum),
|
|
1097
|
+
totalTaxInr: totalTax,
|
|
1098
|
+
totalAmountInr: roundToPaisa(totalNoteValue)
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return normalizedRecords;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Accounts Payable Reconciliation Engine:
|
|
1108
|
+
* Reconciles Zoho Books Bills with Form GSTR-2B.
|
|
1109
|
+
*
|
|
1110
|
+
* Implements decoupled jurisdictional decisions, Rule 37 payment appropriation,
|
|
1111
|
+
* Rule 88B(3) Electronic Credit Ledger balance analysis, and explicit uncertainty routing.
|
|
1112
|
+
*
|
|
1113
|
+
* @param {Object} params
|
|
1114
|
+
* @param {Array<Object>|string} params.zohoBills - Array of parsed Zoho bills or raw CSV text
|
|
1115
|
+
* @param {Array<Object>|string} params.gstr2b - Array of parsed GSTR-2B invoices or raw JSON text
|
|
1116
|
+
* @param {Object} [params.options]
|
|
1117
|
+
* @param {number} [params.options.toleranceInr=1.00] - Rounding tolerance threshold (default ₹1.00)
|
|
1118
|
+
* @param {string} [params.options.asOfDate] - Audit date for Rule 37 aging (default today ISO)
|
|
1119
|
+
* @param {string} [params.options.recipientGstin] - Current filing recipient GSTIN
|
|
1120
|
+
* @param {Array<string>} [params.options.recipientRegisteredStates] - Array of state codes where recipient has distinct registrations
|
|
1121
|
+
* @param {Array<string>} [params.options.recipientRegisteredGstins] - Array of recipient pan-India GSTINs
|
|
1122
|
+
* @param {'PROPORTIONATE'|'VALUE_FIRST'|'TAX_FIRST'} [params.options.paymentAppropriationMethod='PROPORTIONATE'] - Payment application logic under Rule 37
|
|
1123
|
+
* @param {number} [params.options.minimumEclBalanceInr] - Lowest daily closing balance in Electronic Credit Ledger for Rule 88B(3)
|
|
1124
|
+
* @param {'UTILIZED'|'UNUTILIZED'|'UNKNOWN'} [params.options.eclUtilizationStatus='UTILIZED'] - ECL status fallback if minimumEclBalanceInr omitted
|
|
1125
|
+
* @param {number} [params.options.goodsInTransitWindowDays=15] - Threshold days to classify missing books bill as goods in transit
|
|
1126
|
+
* @returns {Object} Full reconciliation payload with metrics and audit trail
|
|
1127
|
+
*/
|
|
1128
|
+
export function reconcileZohoGstr2b({ zohoBills, gstr2b, options = {} }) {
|
|
1129
|
+
const {
|
|
1130
|
+
toleranceInr = 1.00,
|
|
1131
|
+
asOfDate = new Date().toISOString().slice(0, 10),
|
|
1132
|
+
recipientGstin = null,
|
|
1133
|
+
recipientRegisteredStates = [],
|
|
1134
|
+
recipientRegisteredGstins = [],
|
|
1135
|
+
paymentAppropriationMethod = 'PROPORTIONATE',
|
|
1136
|
+
recipientTaxScheme: rawRecipientScheme = 'REGULAR',
|
|
1137
|
+
recipientScheme,
|
|
1138
|
+
isCompositionDealer = false,
|
|
1139
|
+
minimumEclBalanceInr = undefined,
|
|
1140
|
+
eclLedgerHistory = [],
|
|
1141
|
+
eclUtilizationStatus = 'UTILIZED',
|
|
1142
|
+
goodsInTransitWindowDays = 15
|
|
1143
|
+
} = options;
|
|
1144
|
+
|
|
1145
|
+
const normalizedRecipientScheme = isCompositionDealer
|
|
1146
|
+
? 'COMPOSITION'
|
|
1147
|
+
: (/COMPOSITION/i.test(String(recipientScheme || rawRecipientScheme || ''))
|
|
1148
|
+
? 'COMPOSITION'
|
|
1149
|
+
: (/UNKNOWN|INDETERMINATE/i.test(String(recipientScheme || rawRecipientScheme || ''))
|
|
1150
|
+
? 'INDETERMINATE'
|
|
1151
|
+
: 'REGULAR'));
|
|
1152
|
+
|
|
1153
|
+
const tolerancePaise = toIntegerPaise(toleranceInr, 'toleranceInr');
|
|
1154
|
+
|
|
1155
|
+
// 1. Ingest & Normalize Inputs
|
|
1156
|
+
const rawBills = typeof zohoBills === 'string' ? parseZohoBillsCsv(zohoBills) : (Array.isArray(zohoBills) ? zohoBills : []);
|
|
1157
|
+
const raw2b = typeof gstr2b === 'string' ? parseGstr2bJson(gstr2b) : (Array.isArray(gstr2b) ? gstr2b : []);
|
|
1158
|
+
|
|
1159
|
+
const parsedBills = rawBills.map((b, idx) => {
|
|
1160
|
+
const billNum = String(b.billNumber || b.invoiceNumber || b.bill_number || b.invoice_number || `BILL-${idx + 1}`).trim();
|
|
1161
|
+
const keys = (b.exactKey && b.alphaNumericKey) ? { exactKey: b.exactKey, alphaNumericKey: b.alphaNumericKey } : normalizeInvoiceNumber(billNum);
|
|
1162
|
+
const vendorGstin = String(b.vendorGstin || b.gstin || b.gst_no || b.vendor_gstin || '').trim().toUpperCase();
|
|
1163
|
+
const gstinObj = vendorGstin ? normalizeGstin(vendorGstin) : null;
|
|
1164
|
+
const isGstinValid = typeof b.isGstinValid === 'boolean' ? b.isGstinValid : (options.validateGstinChecksum === true && gstinObj ? gstinObj.isValid : undefined);
|
|
1165
|
+
let taxableAmountInr = cleanNumericAmount(b.taxableAmountInr ?? b.taxable_amount ?? b.sub_total ?? 0);
|
|
1166
|
+
let cgstInr = cleanNumericAmount(b.cgstInr ?? b.cgst_amount ?? 0);
|
|
1167
|
+
let sgstInr = cleanNumericAmount(b.sgstInr ?? b.sgst_amount ?? 0);
|
|
1168
|
+
let igstInr = cleanNumericAmount(b.igstInr ?? b.igst_amount ?? 0);
|
|
1169
|
+
|
|
1170
|
+
const itemsArr = Array.isArray(b.lineItems) ? b.lineItems : (Array.isArray(b.line_items) ? b.line_items : []);
|
|
1171
|
+
if (itemsArr.length > 0 && taxableAmountInr === 0 && cgstInr === 0 && sgstInr === 0 && igstInr === 0) {
|
|
1172
|
+
for (const item of itemsArr) {
|
|
1173
|
+
taxableAmountInr += cleanNumericAmount(item.taxableAmount ?? item.taxable_amount ?? item.item_total ?? 0);
|
|
1174
|
+
cgstInr += cleanNumericAmount(item.cgstAmount ?? item.cgst_amount ?? 0);
|
|
1175
|
+
sgstInr += cleanNumericAmount(item.sgstAmount ?? item.sgst_amount ?? 0);
|
|
1176
|
+
igstInr += cleanNumericAmount(item.igstAmount ?? item.igst_amount ?? 0);
|
|
1177
|
+
}
|
|
1178
|
+
taxableAmountInr = roundToPaisa(taxableAmountInr);
|
|
1179
|
+
cgstInr = roundToPaisa(cgstInr);
|
|
1180
|
+
sgstInr = roundToPaisa(sgstInr);
|
|
1181
|
+
igstInr = roundToPaisa(igstInr);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const totalTaxInr = b.totalTaxInr !== undefined ? cleanNumericAmount(b.totalTaxInr) : roundToPaisa(cgstInr + sgstInr + igstInr);
|
|
1185
|
+
const totalAmountInr = b.totalAmountInr !== undefined ? cleanNumericAmount(b.totalAmountInr) : roundToPaisa(taxableAmountInr + totalTaxInr);
|
|
1186
|
+
const balanceInr = b.balanceInr !== undefined ? cleanNumericAmount(b.balanceInr) : (b.balance !== undefined ? cleanNumericAmount(b.balance) : (b.isFullyPaid || b.status === 'paid' ? 0 : totalAmountInr));
|
|
1187
|
+
const isFullyPaid = typeof b.isFullyPaid === 'boolean' ? b.isFullyPaid : (balanceInr <= 0 || b.status === 'paid');
|
|
1188
|
+
const posStateCode = b.posStateCode !== undefined ? b.posStateCode : (gstinObj ? gstinObj.stateCode : null);
|
|
1189
|
+
const isBlocked17_5 = typeof b.isBlocked17_5 === 'boolean' ? b.isBlocked17_5 : /motor|vehicle|food|beverage|catering|club|fitness|personal|gift/i.test(b.category || '');
|
|
1190
|
+
|
|
1191
|
+
const descText = Array.isArray(b.lineItems) ? b.lineItems.map(l => l.description || '').join(' ') : '';
|
|
1192
|
+
const isCommercialRent = /commercial.*rent|office.*rent|property.*rent|warehouse.*rent|renting.*commercial|godown.*rent/i.test((b.category || '') + ' ' + descText) || Boolean(b.isCommercialRent || b.isCommercialRentRcm);
|
|
1193
|
+
const isUnregLandlord = (!vendorGstin || vendorGstin === 'UNREGISTERED' || vendorGstin.length < 15);
|
|
1194
|
+
const billDateStr = b.billDate || null;
|
|
1195
|
+
|
|
1196
|
+
let isCommercialRentRcm = false;
|
|
1197
|
+
let isCompositionExcludedFromRentRcm = false;
|
|
1198
|
+
let requiresRentSchemeVerification = false;
|
|
1199
|
+
let isInterveningCompositionRegularizedPaid = false;
|
|
1200
|
+
let isInterveningCompositionRegularizedUnpaid = false;
|
|
1201
|
+
let requiresInterveningRcmPaymentVerification = false;
|
|
1202
|
+
|
|
1203
|
+
if (isCommercialRent && isUnregLandlord) {
|
|
1204
|
+
if (!billDateStr || billDateStr < '2024-10-10') {
|
|
1205
|
+
isCommercialRentRcm = false;
|
|
1206
|
+
} else if (billDateStr >= '2024-10-10' && billDateStr < '2025-01-16') {
|
|
1207
|
+
// Intervening period under 55th GST Council meeting recommendation (regularized "as is where is")
|
|
1208
|
+
if (normalizedRecipientScheme === 'COMPOSITION') {
|
|
1209
|
+
const hasPaid = b.isRcmPaid === true || b.isRcmTaxPaidInCash === true || b.rcmPaymentStatus === 'PAID' || b.is_rcm_paid === true;
|
|
1210
|
+
const hasUnpaid = b.isRcmPaid === false || b.isRcmTaxPaidInCash === false || b.rcmPaymentStatus === 'UNPAID' || b.is_rcm_paid === false;
|
|
1211
|
+
if (hasPaid) {
|
|
1212
|
+
isInterveningCompositionRegularizedPaid = true;
|
|
1213
|
+
} else if (hasUnpaid) {
|
|
1214
|
+
isInterveningCompositionRegularizedUnpaid = true;
|
|
1215
|
+
} else {
|
|
1216
|
+
requiresInterveningRcmPaymentVerification = true;
|
|
1217
|
+
}
|
|
1218
|
+
isCommercialRentRcm = false;
|
|
1219
|
+
} else if (normalizedRecipientScheme === 'INDETERMINATE') {
|
|
1220
|
+
requiresRentSchemeVerification = true;
|
|
1221
|
+
isCommercialRentRcm = false;
|
|
1222
|
+
} else {
|
|
1223
|
+
isCommercialRentRcm = true;
|
|
1224
|
+
}
|
|
1225
|
+
} else {
|
|
1226
|
+
// On or after 16 Jan 2025: Notification No. 07/2025-Central Tax (Rate)
|
|
1227
|
+
if (normalizedRecipientScheme === 'COMPOSITION') {
|
|
1228
|
+
isCompositionExcludedFromRentRcm = true;
|
|
1229
|
+
isCommercialRentRcm = false;
|
|
1230
|
+
} else if (normalizedRecipientScheme === 'INDETERMINATE') {
|
|
1231
|
+
requiresRentSchemeVerification = true;
|
|
1232
|
+
isCommercialRentRcm = false;
|
|
1233
|
+
} else {
|
|
1234
|
+
isCommercialRentRcm = true;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (b.isCompositionExcludedFromRentRcm !== undefined) {
|
|
1240
|
+
isCompositionExcludedFromRentRcm = Boolean(b.isCompositionExcludedFromRentRcm);
|
|
1241
|
+
}
|
|
1242
|
+
if (b.requiresRentSchemeVerification !== undefined) {
|
|
1243
|
+
requiresRentSchemeVerification = Boolean(b.requiresRentSchemeVerification);
|
|
1244
|
+
}
|
|
1245
|
+
if (b.isInterveningCompositionRegularizedPaid !== undefined) {
|
|
1246
|
+
isInterveningCompositionRegularizedPaid = Boolean(b.isInterveningCompositionRegularizedPaid);
|
|
1247
|
+
}
|
|
1248
|
+
if (b.isInterveningCompositionRegularizedUnpaid !== undefined) {
|
|
1249
|
+
isInterveningCompositionRegularizedUnpaid = Boolean(b.isInterveningCompositionRegularizedUnpaid);
|
|
1250
|
+
}
|
|
1251
|
+
if (b.requiresInterveningRcmPaymentVerification !== undefined) {
|
|
1252
|
+
requiresInterveningRcmPaymentVerification = Boolean(b.requiresInterveningRcmPaymentVerification);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const isRcmExcludedOrPending = isCompositionExcludedFromRentRcm || requiresRentSchemeVerification || isInterveningCompositionRegularizedPaid || isInterveningCompositionRegularizedUnpaid || requiresInterveningRcmPaymentVerification;
|
|
1256
|
+
if (isRcmExcludedOrPending) {
|
|
1257
|
+
isCommercialRentRcm = false;
|
|
1258
|
+
} else if (b.isCommercialRentRcm !== undefined && !isCommercialRent) {
|
|
1259
|
+
isCommercialRentRcm = Boolean(b.isCommercialRentRcm);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const isRcm = Boolean((!isRcmExcludedOrPending && (b.isRcm || /true|yes|y|1/i.test(String(b.is_rcm || '')))) || isCommercialRentRcm);
|
|
1263
|
+
const isSchedule1DeemedSupply = Boolean(b.isSchedule1DeemedSupply || /distinct|related|deemed|schedule\s*1/i.test(b.category || ''));
|
|
1264
|
+
|
|
1265
|
+
return {
|
|
1266
|
+
...b,
|
|
1267
|
+
billNumber: billNum,
|
|
1268
|
+
exactKey: keys.exactKey,
|
|
1269
|
+
alphaNumericKey: keys.alphaNumericKey,
|
|
1270
|
+
vendorGstin,
|
|
1271
|
+
vendorPan: b.vendorPan || (gstinObj ? gstinObj.pan : null),
|
|
1272
|
+
isGstinValid,
|
|
1273
|
+
posStateCode,
|
|
1274
|
+
isBlocked17_5,
|
|
1275
|
+
isRcm,
|
|
1276
|
+
isCommercialRentRcm,
|
|
1277
|
+
isCompositionExcludedFromRentRcm,
|
|
1278
|
+
requiresRentSchemeVerification,
|
|
1279
|
+
isInterveningCompositionRegularizedPaid,
|
|
1280
|
+
isInterveningCompositionRegularizedUnpaid,
|
|
1281
|
+
requiresInterveningRcmPaymentVerification,
|
|
1282
|
+
isSchedule1DeemedSupply,
|
|
1283
|
+
taxableAmountInr,
|
|
1284
|
+
cgstInr,
|
|
1285
|
+
sgstInr,
|
|
1286
|
+
igstInr,
|
|
1287
|
+
totalTaxInr,
|
|
1288
|
+
totalAmountInr,
|
|
1289
|
+
balanceInr,
|
|
1290
|
+
isFullyPaid
|
|
1291
|
+
};
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const parsed2b = raw2b.map((r, idx) => {
|
|
1295
|
+
const invNum = String(r.invoiceNumber || r.billNumber || r.inum || r.invoice_number || `INV-${idx + 1}`).trim();
|
|
1296
|
+
const keys = (r.exactKey && r.alphaNumericKey) ? { exactKey: r.exactKey, alphaNumericKey: r.alphaNumericKey } : normalizeInvoiceNumber(invNum);
|
|
1297
|
+
const supplierGstin = String(r.supplierGstin || r.ctin || r.gstin || '').trim().toUpperCase();
|
|
1298
|
+
const gstinObj = supplierGstin ? normalizeGstin(supplierGstin) : null;
|
|
1299
|
+
const isGstinValid = typeof r.isGstinValid === 'boolean' ? r.isGstinValid : (options.validateGstinChecksum === true && gstinObj ? gstinObj.isValid : undefined);
|
|
1300
|
+
const taxableAmountInr = cleanNumericAmount(r.taxableAmountInr ?? r.taxable_amount ?? r.txval ?? 0);
|
|
1301
|
+
const cgstInr = cleanNumericAmount(r.cgstInr ?? r.cgst_amount ?? 0);
|
|
1302
|
+
const sgstInr = cleanNumericAmount(r.sgstInr ?? r.sgst_amount ?? 0);
|
|
1303
|
+
const igstInr = cleanNumericAmount(r.igstInr ?? r.igst_amount ?? 0);
|
|
1304
|
+
const totalTaxInr = r.totalTaxInr !== undefined ? cleanNumericAmount(r.totalTaxInr) : roundToPaisa(cgstInr + sgstInr + igstInr);
|
|
1305
|
+
const totalAmountInr = r.totalAmountInr !== undefined ? cleanNumericAmount(r.totalAmountInr) : roundToPaisa(taxableAmountInr + totalTaxInr);
|
|
1306
|
+
const posStateCode = r.posStateCode !== undefined ? r.posStateCode : (gstinObj ? gstinObj.stateCode : null);
|
|
1307
|
+
const isRcm = Boolean(r.isRcm || /true|yes|y|1/i.test(String(r.is_rcm || '')));
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
...r,
|
|
1311
|
+
invoiceNumber: invNum,
|
|
1312
|
+
exactKey: keys.exactKey,
|
|
1313
|
+
alphaNumericKey: keys.alphaNumericKey,
|
|
1314
|
+
supplierGstin,
|
|
1315
|
+
isGstinValid,
|
|
1316
|
+
posStateCode,
|
|
1317
|
+
isRcm,
|
|
1318
|
+
taxableAmountInr,
|
|
1319
|
+
cgstInr,
|
|
1320
|
+
sgstInr,
|
|
1321
|
+
igstInr,
|
|
1322
|
+
totalTaxInr,
|
|
1323
|
+
totalAmountInr
|
|
1324
|
+
};
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const recipientFilingState = recipientGstin ? recipientGstin.slice(0, 2) : null;
|
|
1328
|
+
|
|
1329
|
+
// Assemble comprehensive recipient state portfolio (distinct persons under Section 25(4))
|
|
1330
|
+
const recipientStateSet = new Set();
|
|
1331
|
+
if (recipientFilingState) {
|
|
1332
|
+
recipientStateSet.add(recipientFilingState);
|
|
1333
|
+
}
|
|
1334
|
+
for (const g of recipientRegisteredGstins) {
|
|
1335
|
+
if (typeof g === 'string' && g.length >= 2) {
|
|
1336
|
+
recipientStateSet.add(g.slice(0, 2));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
for (const s of recipientRegisteredStates) {
|
|
1340
|
+
if (typeof s === 'string' && s.length >= 2) {
|
|
1341
|
+
recipientStateSet.add(s.slice(0, 2));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Index GSTR-2B by composite key with duplicate detection
|
|
1346
|
+
const gstr2bExactMap = new Map();
|
|
1347
|
+
const gstr2bAlphaMap = new Map();
|
|
1348
|
+
const duplicate2bKeys = new Set();
|
|
1349
|
+
const matched2bSet = new Set();
|
|
1350
|
+
|
|
1351
|
+
for (let i = 0; i < parsed2b.length; i++) {
|
|
1352
|
+
const record = parsed2b[i];
|
|
1353
|
+
const gstinKey = record.supplierGstin || 'UNKNOWN_GSTIN';
|
|
1354
|
+
const exactComposite = `${gstinKey}|${record.exactKey}`;
|
|
1355
|
+
const alphaComposite = `${gstinKey}|${record.alphaNumericKey}`;
|
|
1356
|
+
|
|
1357
|
+
if (gstr2bExactMap.has(exactComposite)) {
|
|
1358
|
+
duplicate2bKeys.add(exactComposite);
|
|
1359
|
+
}
|
|
1360
|
+
if (gstr2bAlphaMap.has(alphaComposite)) {
|
|
1361
|
+
duplicate2bKeys.add(alphaComposite);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
gstr2bExactMap.set(exactComposite, { record, index: i });
|
|
1365
|
+
gstr2bAlphaMap.set(alphaComposite, { record, index: i });
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Detect duplicate bills within Zoho Books
|
|
1369
|
+
const seenBooksExactKeys = new Set();
|
|
1370
|
+
const duplicateBooksKeys = new Set();
|
|
1371
|
+
for (const bill of parsedBills) {
|
|
1372
|
+
const gstinKey = bill.vendorGstin || 'UNKNOWN_GSTIN';
|
|
1373
|
+
const exactComposite = `${gstinKey}|${bill.exactKey}`;
|
|
1374
|
+
if (seenBooksExactKeys.has(exactComposite)) {
|
|
1375
|
+
duplicateBooksKeys.add(exactComposite);
|
|
1376
|
+
}
|
|
1377
|
+
seenBooksExactKeys.add(exactComposite);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const items = [];
|
|
1381
|
+
|
|
1382
|
+
let totalBooksTaxablePaise = 0;
|
|
1383
|
+
let totalBooksTaxPaise = 0;
|
|
1384
|
+
let total2bTaxablePaise = 0;
|
|
1385
|
+
let total2bTaxPaise = 0;
|
|
1386
|
+
|
|
1387
|
+
let matchedEligibleCount = 0;
|
|
1388
|
+
let eligibleItcPaise = 0;
|
|
1389
|
+
|
|
1390
|
+
let rcmUnregisteredEligibleCount = 0;
|
|
1391
|
+
let rcmUnregisteredEligibleItcPaise = 0;
|
|
1392
|
+
|
|
1393
|
+
let compositionExcludedCount = 0;
|
|
1394
|
+
let compositionExcludedItcPaise = 0;
|
|
1395
|
+
|
|
1396
|
+
let interveningRegularizedPaidCount = 0;
|
|
1397
|
+
let interveningRegularizedUnpaidCount = 0;
|
|
1398
|
+
let interveningRegularizedTaxPaise = 0;
|
|
1399
|
+
|
|
1400
|
+
let amountMismatchCount = 0;
|
|
1401
|
+
let mismatchedItcPaise = 0;
|
|
1402
|
+
|
|
1403
|
+
let blocked17_5Count = 0;
|
|
1404
|
+
let blocked17_5ItcPaise = 0;
|
|
1405
|
+
|
|
1406
|
+
let posMismatchCount = 0;
|
|
1407
|
+
let posMismatchItcPaise = 0;
|
|
1408
|
+
|
|
1409
|
+
let routableToBranchCount = 0;
|
|
1410
|
+
let routableToBranchItcPaise = 0;
|
|
1411
|
+
|
|
1412
|
+
let strandedLocalCount = 0;
|
|
1413
|
+
let strandedLocalItcPaise = 0;
|
|
1414
|
+
|
|
1415
|
+
let gstr2bPortalIneligibleCount = 0;
|
|
1416
|
+
let gstr2bPortalIneligibleItcPaise = 0;
|
|
1417
|
+
|
|
1418
|
+
let requiresHumanVerificationCount = 0;
|
|
1419
|
+
let requiresHumanVerificationItcPaise = 0;
|
|
1420
|
+
|
|
1421
|
+
let missingIn2bCount = 0;
|
|
1422
|
+
let atRiskUnfiledItcPaise = 0;
|
|
1423
|
+
|
|
1424
|
+
let rule37OverdueCount = 0;
|
|
1425
|
+
let rule37ReversalAmountPaise = 0;
|
|
1426
|
+
let rule37InterestExposurePaise = 0;
|
|
1427
|
+
|
|
1428
|
+
// 2. Iterate Zoho Bills & Match against GSTR-2B
|
|
1429
|
+
for (const bill of parsedBills) {
|
|
1430
|
+
const gstinKey = bill.vendorGstin || 'UNKNOWN_GSTIN';
|
|
1431
|
+
const exactComposite = `${gstinKey}|${bill.exactKey}`;
|
|
1432
|
+
const alphaComposite = `${gstinKey}|${bill.alphaNumericKey}`;
|
|
1433
|
+
|
|
1434
|
+
const booksTaxablePaise = toIntegerPaise(bill.taxableAmountInr);
|
|
1435
|
+
const booksTaxPaise = toIntegerPaise(bill.totalTaxInr);
|
|
1436
|
+
totalBooksTaxablePaise += booksTaxablePaise;
|
|
1437
|
+
totalBooksTaxPaise += booksTaxPaise;
|
|
1438
|
+
|
|
1439
|
+
let matchEntry = gstr2bExactMap.get(exactComposite) || gstr2bAlphaMap.get(alphaComposite) || null;
|
|
1440
|
+
const isRcm = bill.isRcm || (matchEntry && matchEntry.record.isRcm);
|
|
1441
|
+
|
|
1442
|
+
// ── Rule 37 180-Day Aging & Section 50(1) Evaluation ──
|
|
1443
|
+
let isRule37Overdue = false;
|
|
1444
|
+
let daysElapsedSinceBill = 0;
|
|
1445
|
+
let rule37Status = 'COMPLIANT_OR_NOT_APPLICABLE';
|
|
1446
|
+
let rule37ReversalTaxInr = 0;
|
|
1447
|
+
let rule37InterestInr = 0;
|
|
1448
|
+
let rule37InterestExplanation = '';
|
|
1449
|
+
let isReclaimableUponPayment = false;
|
|
1450
|
+
let paymentAppropriationDetails = null;
|
|
1451
|
+
|
|
1452
|
+
const isFullyPaid = bill.isFullyPaid || bill.balanceInr <= 0;
|
|
1453
|
+
|
|
1454
|
+
if (bill.billDate) {
|
|
1455
|
+
daysElapsedSinceBill = calculateDaysElapsed(bill.billDate, asOfDate);
|
|
1456
|
+
|
|
1457
|
+
if (isRcm) {
|
|
1458
|
+
rule37Status = 'EXEMPT_RCM_SUPPLY'; // Proviso 1 to Rule 37(1)
|
|
1459
|
+
} else if (bill.isSchedule1DeemedSupply) {
|
|
1460
|
+
rule37Status = 'EXEMPT_SCHEDULE_1_DEEMED_SUPPLY'; // Proviso 2 to Rule 37(1)
|
|
1461
|
+
} else if (isFullyPaid) {
|
|
1462
|
+
rule37Status = 'COMPLIANT_FULLY_PAID';
|
|
1463
|
+
} else if (daysElapsedSinceBill <= 180) {
|
|
1464
|
+
rule37Status = 'WITHIN_180_DAYS_WINDOW';
|
|
1465
|
+
} else {
|
|
1466
|
+
// Bill exceeds 180 days unpaid!
|
|
1467
|
+
isRule37Overdue = true;
|
|
1468
|
+
rule37OverdueCount++;
|
|
1469
|
+
rule37Status = 'OVERDUE_REVERSAL_REQUIRED';
|
|
1470
|
+
isReclaimableUponPayment = true;
|
|
1471
|
+
|
|
1472
|
+
const totalAmt = bill.totalAmountInr;
|
|
1473
|
+
const balanceAmt = bill.balanceInr;
|
|
1474
|
+
const taxableAmt = bill.taxableAmountInr;
|
|
1475
|
+
const taxAmt = bill.totalTaxInr;
|
|
1476
|
+
const paidAmt = Math.max(0, totalAmt - balanceAmt);
|
|
1477
|
+
|
|
1478
|
+
let reversalPaise = 0;
|
|
1479
|
+
let valuePaid = 0;
|
|
1480
|
+
let taxPaid = 0;
|
|
1481
|
+
|
|
1482
|
+
if (paymentAppropriationMethod === 'VALUE_FIRST') {
|
|
1483
|
+
valuePaid = Math.min(taxableAmt, paidAmt);
|
|
1484
|
+
taxPaid = Math.max(0, paidAmt - taxableAmt);
|
|
1485
|
+
const unpaidTax = Math.max(0, taxAmt - taxPaid);
|
|
1486
|
+
reversalPaise = toIntegerPaise(unpaidTax);
|
|
1487
|
+
} else if (paymentAppropriationMethod === 'TAX_FIRST') {
|
|
1488
|
+
taxPaid = Math.min(taxAmt, paidAmt);
|
|
1489
|
+
valuePaid = Math.max(0, paidAmt - taxAmt);
|
|
1490
|
+
// Failure to pay value along with tax still triggers reversal proportionate to unpaid consideration
|
|
1491
|
+
const unpaidRatio = totalAmt > 0 ? (balanceAmt / totalAmt) : 1;
|
|
1492
|
+
reversalPaise = Math.round(booksTaxPaise * Math.min(1, Math.max(0, unpaidRatio)));
|
|
1493
|
+
} else {
|
|
1494
|
+
// PROPORTIONATE (Standard statutory baseline)
|
|
1495
|
+
const unpaidRatio = totalAmt > 0 ? (balanceAmt / totalAmt) : 1;
|
|
1496
|
+
valuePaid = Math.round(taxableAmt * (1 - unpaidRatio) * 100) / 100;
|
|
1497
|
+
taxPaid = Math.round(taxAmt * (1 - unpaidRatio) * 100) / 100;
|
|
1498
|
+
reversalPaise = Math.round(booksTaxPaise * Math.min(1, Math.max(0, unpaidRatio)));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
rule37ReversalAmountPaise += reversalPaise;
|
|
1502
|
+
rule37ReversalTaxInr = fromIntegerPaise(reversalPaise);
|
|
1503
|
+
|
|
1504
|
+
paymentAppropriationDetails = {
|
|
1505
|
+
method: paymentAppropriationMethod,
|
|
1506
|
+
isStatutoryStandard: paymentAppropriationMethod === 'PROPORTIONATE',
|
|
1507
|
+
statutoryNote: paymentAppropriationMethod === 'PROPORTIONATE'
|
|
1508
|
+
? 'CGST Act Section 16(2) second proviso read with Rule 37(1) canonical statutory baseline: proportional reversal on unpaid consideration and tax.'
|
|
1509
|
+
: `Alternative accounting allocation assumption (${paymentAppropriationMethod}). In statutory audit and tax litigation, proportional allocation remains the default baseline unless contractually documented.`,
|
|
1510
|
+
totalInvoiceInr: totalAmt,
|
|
1511
|
+
balanceDueInr: balanceAmt,
|
|
1512
|
+
amountPaidInr: paidAmt,
|
|
1513
|
+
imputedValuePaidInr: valuePaid,
|
|
1514
|
+
imputedTaxPaidInr: taxPaid,
|
|
1515
|
+
reversalTaxInr: rule37ReversalTaxInr
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
// Section 50(1) interest evaluated against Rule 88B(3) ECL balance
|
|
1519
|
+
const overdueDaysPast180 = daysElapsedSinceBill - 180;
|
|
1520
|
+
|
|
1521
|
+
if (Array.isArray(eclLedgerHistory) && eclLedgerHistory.length > 0) {
|
|
1522
|
+
const overdueStartDate = calculateOverdueStartDate(bill.billDate);
|
|
1523
|
+
const eclRes = calculateEclUtilizationInterest({
|
|
1524
|
+
unreversedItcInr: rule37ReversalTaxInr,
|
|
1525
|
+
overdueStartDate,
|
|
1526
|
+
asOfDate,
|
|
1527
|
+
reversalDate: bill.reversalDate || null,
|
|
1528
|
+
eclLedgerHistory
|
|
1529
|
+
});
|
|
1530
|
+
rule37InterestInr = eclRes.totalInterestInr;
|
|
1531
|
+
rule37InterestExplanation = eclRes.explanation;
|
|
1532
|
+
rule37InterestExposurePaise += eclRes.totalInterestPaise;
|
|
1533
|
+
} else if (minimumEclBalanceInr !== undefined && minimumEclBalanceInr !== null) {
|
|
1534
|
+
const minEcl = cleanNumericAmount(minimumEclBalanceInr);
|
|
1535
|
+
const wrongfulUtilizedInr = Math.max(0, roundToPaisa(rule37ReversalTaxInr - minEcl));
|
|
1536
|
+
if (wrongfulUtilizedInr <= 0) {
|
|
1537
|
+
rule37InterestInr = 0;
|
|
1538
|
+
rule37InterestExplanation = `Rule 88B(3) Proviso: Minimum Electronic Credit Ledger balance (₹${minEcl.toLocaleString('en-IN')}) exceeded unreversed ITC (₹${rule37ReversalTaxInr.toLocaleString('en-IN')}). Credit remained unutilized; zero interest payable.`;
|
|
1539
|
+
} else {
|
|
1540
|
+
const interestPaise = Math.round(toIntegerPaise(wrongfulUtilizedInr) * (0.18 / 365) * overdueDaysPast180);
|
|
1541
|
+
rule37InterestExposurePaise += interestPaise;
|
|
1542
|
+
rule37InterestInr = fromIntegerPaise(interestPaise);
|
|
1543
|
+
rule37InterestExplanation = `Rule 88B(3): Minimum ECL balance (₹${minEcl.toLocaleString('en-IN')}) dipped below unreversed ITC by ₹${wrongfulUtilizedInr.toLocaleString('en-IN')}. 18% annual interest computed on utilized balance for ${overdueDaysPast180} overdue days.`;
|
|
1544
|
+
}
|
|
1545
|
+
} else if (eclUtilizationStatus === 'UNUTILIZED') {
|
|
1546
|
+
rule37InterestInr = 0;
|
|
1547
|
+
rule37InterestExplanation = 'Rule 88B(3) Proviso: Upstream attestation confirms ECL balance never fell below unreversed ITC. Zero interest payable.';
|
|
1548
|
+
} else if (eclUtilizationStatus === 'UTILIZED') {
|
|
1549
|
+
const interestPaise = Math.round(reversalPaise * (0.18 / 365) * overdueDaysPast180);
|
|
1550
|
+
rule37InterestExposurePaise += interestPaise;
|
|
1551
|
+
rule37InterestInr = fromIntegerPaise(interestPaise);
|
|
1552
|
+
rule37InterestExplanation = `Section 50(1): 18% annual interest computed on full reversal amount (₹${rule37ReversalTaxInr.toLocaleString('en-IN')}) across ${overdueDaysPast180} overdue days.`;
|
|
1553
|
+
} else {
|
|
1554
|
+
rule37InterestInr = 0;
|
|
1555
|
+
rule37InterestExplanation = 'ECL balance history unavailable. Rule 88B(3) utilization amount cannot be determined without ledger closing balances. Human verification required.';
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// ── Uncertainty Check 1: Invalid Supplier GSTIN Checksum ──
|
|
1561
|
+
if (bill.vendorGstin && bill.isGstinValid === false) {
|
|
1562
|
+
requiresHumanVerificationCount++;
|
|
1563
|
+
requiresHumanVerificationItcPaise += booksTaxPaise;
|
|
1564
|
+
|
|
1565
|
+
items.push({
|
|
1566
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1567
|
+
uncertaintyReason: 'INVALID_SUPPLIER_GSTIN_CHECKSUM',
|
|
1568
|
+
itcStatus: 'INDETERMINATE_INVALID_IDENTIFIER',
|
|
1569
|
+
statutorySection: 'CGST Act Section 22/25 & Rule 8 (Taxpayer Identity Verification)',
|
|
1570
|
+
vendorName: bill.vendorName,
|
|
1571
|
+
vendorGstin: bill.vendorGstin,
|
|
1572
|
+
billNumber: bill.billNumber,
|
|
1573
|
+
billDate: bill.billDate,
|
|
1574
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1575
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1576
|
+
gstr2bTaxableInr: 0,
|
|
1577
|
+
gstr2bTaxInr: 0,
|
|
1578
|
+
varianceTaxableInr: bill.taxableAmountInr,
|
|
1579
|
+
varianceTaxInr: bill.totalTaxInr,
|
|
1580
|
+
isRule37Overdue,
|
|
1581
|
+
daysElapsedSinceBill,
|
|
1582
|
+
rule37Status,
|
|
1583
|
+
rule37ReversalTaxInr,
|
|
1584
|
+
rule37InterestInr,
|
|
1585
|
+
rule37InterestExplanation,
|
|
1586
|
+
isReclaimableUponPayment,
|
|
1587
|
+
paymentAppropriationDetails,
|
|
1588
|
+
jurisdictionDecisions: {
|
|
1589
|
+
taxClassification: 'INDETERMINATE',
|
|
1590
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1591
|
+
itcEligibility: 'INDETERMINATE',
|
|
1592
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1593
|
+
},
|
|
1594
|
+
statutoryAction: 'Supplier GSTIN failed ISO Mod-36 checksum validation. Withhold automated processing; human review required to verify vendor master or demand corrected tax invoice.',
|
|
1595
|
+
auditEvidence: {
|
|
1596
|
+
inBooks: true,
|
|
1597
|
+
inGstr2b: false,
|
|
1598
|
+
invalidGstin: bill.vendorGstin
|
|
1599
|
+
},
|
|
1600
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1601
|
+
bill,
|
|
1602
|
+
record2b: null,
|
|
1603
|
+
matchType: 'NONE',
|
|
1604
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1605
|
+
statutorySection: 'CGST Act Section 22/25 & Rule 8 (Taxpayer Identity Verification)',
|
|
1606
|
+
jurisdictionDecisions: {
|
|
1607
|
+
taxClassification: 'INDETERMINATE',
|
|
1608
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1609
|
+
itcEligibility: 'INDETERMINATE',
|
|
1610
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1611
|
+
},
|
|
1612
|
+
uncertaintyReason: 'INVALID_SUPPLIER_GSTIN_CHECKSUM',
|
|
1613
|
+
asOfDate
|
|
1614
|
+
})
|
|
1615
|
+
});
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// ── Uncertainty Check 2: Ambiguous Duplicate Invoices in Books or GSTR-2B ──
|
|
1620
|
+
if (duplicateBooksKeys.has(exactComposite) || (matchEntry && duplicate2bKeys.has(exactComposite))) {
|
|
1621
|
+
requiresHumanVerificationCount++;
|
|
1622
|
+
requiresHumanVerificationItcPaise += booksTaxPaise;
|
|
1623
|
+
|
|
1624
|
+
items.push({
|
|
1625
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1626
|
+
uncertaintyReason: 'AMBIGUOUS_DUPLICATE_INVOICE_MATCH',
|
|
1627
|
+
itcStatus: 'INDETERMINATE_AMBIGUOUS_BILLING',
|
|
1628
|
+
statutorySection: 'CGST Act Section 31 & General Accounting Internal Controls',
|
|
1629
|
+
vendorName: bill.vendorName,
|
|
1630
|
+
vendorGstin: bill.vendorGstin,
|
|
1631
|
+
billNumber: bill.billNumber,
|
|
1632
|
+
billDate: bill.billDate,
|
|
1633
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1634
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1635
|
+
gstr2bTaxableInr: matchEntry ? matchEntry.record.taxableAmountInr : 0,
|
|
1636
|
+
gstr2bTaxInr: matchEntry ? matchEntry.record.totalTaxInr : 0,
|
|
1637
|
+
varianceTaxableInr: 0,
|
|
1638
|
+
varianceTaxInr: 0,
|
|
1639
|
+
isRule37Overdue,
|
|
1640
|
+
daysElapsedSinceBill,
|
|
1641
|
+
rule37Status,
|
|
1642
|
+
rule37ReversalTaxInr,
|
|
1643
|
+
rule37InterestInr,
|
|
1644
|
+
rule37InterestExplanation,
|
|
1645
|
+
isReclaimableUponPayment,
|
|
1646
|
+
paymentAppropriationDetails,
|
|
1647
|
+
jurisdictionDecisions: {
|
|
1648
|
+
taxClassification: 'INDETERMINATE',
|
|
1649
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1650
|
+
itcEligibility: 'INDETERMINATE',
|
|
1651
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1652
|
+
},
|
|
1653
|
+
statutoryAction: 'Multiple candidate records detected sharing the identical supplier GSTIN and invoice number. Automated engine refuses to guess match. Human auditor must confirm if this represents duplicate billing or an amended voucher.',
|
|
1654
|
+
auditEvidence: {
|
|
1655
|
+
inBooks: true,
|
|
1656
|
+
inGstr2b: Boolean(matchEntry),
|
|
1657
|
+
duplicateCompositeKey: exactComposite
|
|
1658
|
+
},
|
|
1659
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1660
|
+
bill,
|
|
1661
|
+
record2b: matchEntry ? matchEntry.record : null,
|
|
1662
|
+
matchType: exactComposite === `${gstinKey}|${(matchEntry && matchEntry.record.exactKey)}` ? 'EXACT' : 'ALPHANUMERIC',
|
|
1663
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1664
|
+
statutorySection: 'CGST Act Section 31 & General Accounting Internal Controls',
|
|
1665
|
+
jurisdictionDecisions: {
|
|
1666
|
+
taxClassification: 'INDETERMINATE',
|
|
1667
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1668
|
+
itcEligibility: 'INDETERMINATE',
|
|
1669
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1670
|
+
},
|
|
1671
|
+
uncertaintyReason: 'AMBIGUOUS_DUPLICATE_INVOICE_MATCH',
|
|
1672
|
+
asOfDate
|
|
1673
|
+
})
|
|
1674
|
+
});
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// ── Uncertainty Check 3: Contradictory Tax Split ──
|
|
1679
|
+
const hasLocalTax = (bill.cgstInr > 0 || bill.sgstInr > 0);
|
|
1680
|
+
const hasIgst = bill.igstInr > 0;
|
|
1681
|
+
if (hasLocalTax && hasIgst) {
|
|
1682
|
+
requiresHumanVerificationCount++;
|
|
1683
|
+
requiresHumanVerificationItcPaise += booksTaxPaise;
|
|
1684
|
+
|
|
1685
|
+
items.push({
|
|
1686
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1687
|
+
uncertaintyReason: 'CONTRADICTORY_TAX_SPLIT',
|
|
1688
|
+
itcStatus: 'DEFECTIVE_TAX_INVOICE_SPLIT',
|
|
1689
|
+
statutorySection: 'CGST Act Section 31 & IGST Act Section 5',
|
|
1690
|
+
vendorName: bill.vendorName,
|
|
1691
|
+
vendorGstin: bill.vendorGstin,
|
|
1692
|
+
billNumber: bill.billNumber,
|
|
1693
|
+
billDate: bill.billDate,
|
|
1694
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1695
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1696
|
+
gstr2bTaxableInr: matchEntry ? matchEntry.record.taxableAmountInr : 0,
|
|
1697
|
+
gstr2bTaxInr: matchEntry ? matchEntry.record.totalTaxInr : 0,
|
|
1698
|
+
varianceTaxableInr: 0,
|
|
1699
|
+
varianceTaxInr: 0,
|
|
1700
|
+
isRule37Overdue,
|
|
1701
|
+
daysElapsedSinceBill,
|
|
1702
|
+
rule37Status,
|
|
1703
|
+
rule37ReversalTaxInr,
|
|
1704
|
+
rule37InterestInr,
|
|
1705
|
+
rule37InterestExplanation,
|
|
1706
|
+
isReclaimableUponPayment,
|
|
1707
|
+
paymentAppropriationDetails,
|
|
1708
|
+
jurisdictionDecisions: {
|
|
1709
|
+
taxClassification: 'INDETERMINATE',
|
|
1710
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1711
|
+
itcEligibility: 'INELIGIBLE_DEFECTIVE_INVOICE',
|
|
1712
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1713
|
+
},
|
|
1714
|
+
statutoryAction: 'Invoice contains contradictory tax components (concurrent CGST/SGST and IGST). Legally defective on its face under Section 31. Demand amended invoice from supplier.',
|
|
1715
|
+
auditEvidence: {
|
|
1716
|
+
cgstInr: bill.cgstInr,
|
|
1717
|
+
sgstInr: bill.sgstInr,
|
|
1718
|
+
igstInr: bill.igstInr
|
|
1719
|
+
},
|
|
1720
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1721
|
+
bill,
|
|
1722
|
+
record2b: matchEntry ? matchEntry.record : null,
|
|
1723
|
+
matchType: matchEntry ? (exactComposite === `${gstinKey}|${matchEntry.record.exactKey}` ? 'EXACT' : 'ALPHANUMERIC') : 'UNMATCHED',
|
|
1724
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1725
|
+
statutorySection: 'CGST Act Section 31 & IGST Act Section 5',
|
|
1726
|
+
jurisdictionDecisions: {
|
|
1727
|
+
taxClassification: 'INDETERMINATE',
|
|
1728
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1729
|
+
itcEligibility: 'INELIGIBLE_DEFECTIVE_INVOICE',
|
|
1730
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1731
|
+
},
|
|
1732
|
+
uncertaintyReason: 'CONTRADICTORY_TAX_SPLIT',
|
|
1733
|
+
asOfDate
|
|
1734
|
+
})
|
|
1735
|
+
});
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// ── Missing in GSTR-2B or Unregistered RCM Supply ──
|
|
1740
|
+
if (!matchEntry) {
|
|
1741
|
+
// Check for Indeterminate Recipient Scheme (Notification 07/2025-CT(Rate))
|
|
1742
|
+
if (bill.requiresRentSchemeVerification) {
|
|
1743
|
+
requiresHumanVerificationCount++;
|
|
1744
|
+
requiresHumanVerificationItcPaise += booksTaxPaise;
|
|
1745
|
+
|
|
1746
|
+
items.push({
|
|
1747
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1748
|
+
uncertaintyReason: 'INDETERMINATE_RECIPIENT_REGISTRATION_SCHEME',
|
|
1749
|
+
itcStatus: 'INDETERMINATE_REGISTRATION_SCHEME',
|
|
1750
|
+
statutorySection: 'CGST Act Section 9(3) read with Notification No. 07/2025-CT(Rate)',
|
|
1751
|
+
vendorName: bill.vendorName,
|
|
1752
|
+
vendorGstin: bill.vendorGstin || 'UNREGISTERED',
|
|
1753
|
+
billNumber: bill.billNumber,
|
|
1754
|
+
billDate: bill.billDate,
|
|
1755
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1756
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1757
|
+
gstr2bTaxableInr: 0,
|
|
1758
|
+
gstr2bTaxInr: 0,
|
|
1759
|
+
varianceTaxableInr: bill.taxableAmountInr,
|
|
1760
|
+
varianceTaxInr: bill.totalTaxInr,
|
|
1761
|
+
isRule37Overdue: false,
|
|
1762
|
+
daysElapsedSinceBill,
|
|
1763
|
+
rule37Status: 'EXEMPT_RCM_SUPPLY',
|
|
1764
|
+
rule37ReversalTaxInr: 0,
|
|
1765
|
+
rule37InterestInr: 0,
|
|
1766
|
+
rule37InterestExplanation: '',
|
|
1767
|
+
isReclaimableUponPayment: false,
|
|
1768
|
+
paymentAppropriationDetails: null,
|
|
1769
|
+
isRcm: false,
|
|
1770
|
+
jurisdictionDecisions: {
|
|
1771
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1772
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1773
|
+
itcEligibility: 'INDETERMINATE',
|
|
1774
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1775
|
+
},
|
|
1776
|
+
statutoryAction: 'Notification No. 07/2025-CT(Rate) w.e.f. 16 Jan 2025 excludes composition taxpayers (Section 10) from Entry 5AB commercial rent RCM. Verify recipient GST registration scheme (Regular vs Section 10 Composition) to determine RCM liability.',
|
|
1777
|
+
auditEvidence: {
|
|
1778
|
+
inBooks: true,
|
|
1779
|
+
inGstr2b: false,
|
|
1780
|
+
matchType: 'UNMATCHED',
|
|
1781
|
+
isCommercialRent: true,
|
|
1782
|
+
requiresRentSchemeVerification: true
|
|
1783
|
+
},
|
|
1784
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1785
|
+
bill,
|
|
1786
|
+
record2b: null,
|
|
1787
|
+
matchType: 'UNMATCHED',
|
|
1788
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1789
|
+
statutorySection: 'CGST Act Section 9(3) read with Notification No. 07/2025-CT(Rate)',
|
|
1790
|
+
jurisdictionDecisions: {
|
|
1791
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1792
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1793
|
+
itcEligibility: 'INDETERMINATE',
|
|
1794
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1795
|
+
},
|
|
1796
|
+
uncertaintyReason: 'INDETERMINATE_RECIPIENT_REGISTRATION_SCHEME',
|
|
1797
|
+
asOfDate
|
|
1798
|
+
})
|
|
1799
|
+
});
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Check for Intervening Period RCM Payment Verification (55th GST Council Meeting)
|
|
1804
|
+
if (bill.requiresInterveningRcmPaymentVerification) {
|
|
1805
|
+
requiresHumanVerificationCount++;
|
|
1806
|
+
requiresHumanVerificationItcPaise += booksTaxPaise;
|
|
1807
|
+
|
|
1808
|
+
items.push({
|
|
1809
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1810
|
+
uncertaintyReason: 'INDETERMINATE_INTERVENING_RCM_DISCHARGE',
|
|
1811
|
+
itcStatus: 'INDETERMINATE_INTERVENING_RCM_DISCHARGE',
|
|
1812
|
+
statutorySection: 'CGST Act Section 9(3) read with 55th GST Council Meeting Recommendations',
|
|
1813
|
+
vendorName: bill.vendorName,
|
|
1814
|
+
vendorGstin: bill.vendorGstin || 'UNREGISTERED',
|
|
1815
|
+
billNumber: bill.billNumber,
|
|
1816
|
+
billDate: bill.billDate,
|
|
1817
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1818
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1819
|
+
gstr2bTaxableInr: 0,
|
|
1820
|
+
gstr2bTaxInr: 0,
|
|
1821
|
+
varianceTaxableInr: bill.taxableAmountInr,
|
|
1822
|
+
varianceTaxInr: bill.totalTaxInr,
|
|
1823
|
+
isRule37Overdue: false,
|
|
1824
|
+
daysElapsedSinceBill,
|
|
1825
|
+
rule37Status: 'EXEMPT_RCM_SUPPLY',
|
|
1826
|
+
rule37ReversalTaxInr: 0,
|
|
1827
|
+
rule37InterestInr: 0,
|
|
1828
|
+
rule37InterestExplanation: '',
|
|
1829
|
+
isReclaimableUponPayment: false,
|
|
1830
|
+
paymentAppropriationDetails: null,
|
|
1831
|
+
isRcm: false,
|
|
1832
|
+
jurisdictionDecisions: {
|
|
1833
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1834
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1835
|
+
itcEligibility: 'INDETERMINATE',
|
|
1836
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1837
|
+
},
|
|
1838
|
+
statutoryAction: 'Intervening period (10 Oct 2024 to 15 Jan 2025) commercial rent regularized on "as is where is" basis per 55th GST Council meeting. Verify historical cash ledger to confirm whether RCM tax was discharged.',
|
|
1839
|
+
auditEvidence: {
|
|
1840
|
+
inBooks: true,
|
|
1841
|
+
inGstr2b: false,
|
|
1842
|
+
matchType: 'UNMATCHED',
|
|
1843
|
+
isCommercialRent: true,
|
|
1844
|
+
requiresInterveningRcmPaymentVerification: true
|
|
1845
|
+
},
|
|
1846
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1847
|
+
bill,
|
|
1848
|
+
record2b: null,
|
|
1849
|
+
matchType: 'UNMATCHED',
|
|
1850
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
1851
|
+
statutorySection: 'CGST Act Section 9(3) read with 55th GST Council Meeting Recommendations',
|
|
1852
|
+
jurisdictionDecisions: {
|
|
1853
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1854
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
1855
|
+
itcEligibility: 'INDETERMINATE',
|
|
1856
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
1857
|
+
},
|
|
1858
|
+
uncertaintyReason: 'INDETERMINATE_INTERVENING_RCM_DISCHARGE',
|
|
1859
|
+
asOfDate
|
|
1860
|
+
})
|
|
1861
|
+
});
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// Check for Intervening Period Regularized Paid (55th GST Council Meeting)
|
|
1866
|
+
if (bill.isInterveningCompositionRegularizedPaid) {
|
|
1867
|
+
interveningRegularizedPaidCount++;
|
|
1868
|
+
interveningRegularizedTaxPaise += booksTaxPaise;
|
|
1869
|
+
|
|
1870
|
+
items.push({
|
|
1871
|
+
disposition: 'RCM_COMPOSITION_INTERVENING_REGULARIZED_PAID',
|
|
1872
|
+
itcStatus: 'INELIGIBLE_COMPOSITION_SCHEME_SEC10_4',
|
|
1873
|
+
statutorySection: 'CGST Act Section 9(3) read with 55th GST Council Meeting Recommendations & Section 10(4)',
|
|
1874
|
+
vendorName: bill.vendorName,
|
|
1875
|
+
vendorGstin: bill.vendorGstin || 'UNREGISTERED',
|
|
1876
|
+
billNumber: bill.billNumber,
|
|
1877
|
+
billDate: bill.billDate,
|
|
1878
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1879
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1880
|
+
gstr2bTaxableInr: 0,
|
|
1881
|
+
gstr2bTaxInr: 0,
|
|
1882
|
+
varianceTaxableInr: 0,
|
|
1883
|
+
varianceTaxInr: 0,
|
|
1884
|
+
isRule37Overdue: false,
|
|
1885
|
+
daysElapsedSinceBill,
|
|
1886
|
+
rule37Status: 'EXEMPT_NON_GST_SUPPLY',
|
|
1887
|
+
rule37ReversalTaxInr: 0,
|
|
1888
|
+
rule37InterestInr: 0,
|
|
1889
|
+
rule37InterestExplanation: '',
|
|
1890
|
+
isReclaimableUponPayment: false,
|
|
1891
|
+
paymentAppropriationDetails: null,
|
|
1892
|
+
isRcm: false,
|
|
1893
|
+
isCompositionExcluded: true,
|
|
1894
|
+
isInterveningRegularizedPaid: true,
|
|
1895
|
+
jurisdictionDecisions: {
|
|
1896
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1897
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
1898
|
+
itcEligibility: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
1899
|
+
operationalRouting: 'BOOK_AS_EXPENSE_NO_ITC'
|
|
1900
|
+
},
|
|
1901
|
+
statutoryAction: 'Intervening period (10 Oct 2024 to 15 Jan 2025) regularized on "as is where is" basis per 55th GST Council meeting. RCM tax already discharged in cash cannot be claimed as refund; expensed under Section 10(4).',
|
|
1902
|
+
auditEvidence: {
|
|
1903
|
+
inBooks: true,
|
|
1904
|
+
inGstr2b: false,
|
|
1905
|
+
matchType: 'INTERVENING_REGULARIZED_PAID',
|
|
1906
|
+
isCompositionRegularizedPaid: true
|
|
1907
|
+
},
|
|
1908
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1909
|
+
bill,
|
|
1910
|
+
record2b: null,
|
|
1911
|
+
matchType: 'INTERVENING_REGULARIZED_PAID',
|
|
1912
|
+
disposition: 'RCM_COMPOSITION_INTERVENING_REGULARIZED_PAID',
|
|
1913
|
+
statutorySection: 'CGST Act Section 9(3) read with 55th GST Council Meeting Recommendations & Section 10(4)',
|
|
1914
|
+
jurisdictionDecisions: {
|
|
1915
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1916
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
1917
|
+
itcEligibility: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
1918
|
+
operationalRouting: 'BOOK_AS_EXPENSE_NO_ITC'
|
|
1919
|
+
},
|
|
1920
|
+
uncertaintyReason: null,
|
|
1921
|
+
asOfDate
|
|
1922
|
+
})
|
|
1923
|
+
});
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// Check for Intervening Period Regularized Unpaid (55th GST Council Meeting)
|
|
1928
|
+
if (bill.isInterveningCompositionRegularizedUnpaid) {
|
|
1929
|
+
interveningRegularizedUnpaidCount++;
|
|
1930
|
+
interveningRegularizedTaxPaise += booksTaxPaise;
|
|
1931
|
+
|
|
1932
|
+
items.push({
|
|
1933
|
+
disposition: 'RCM_COMPOSITION_INTERVENING_REGULARIZED_UNPAID',
|
|
1934
|
+
itcStatus: 'INELIGIBLE_COMPOSITION_SCHEME_SEC10_4',
|
|
1935
|
+
statutorySection: 'CGST Act Section 9(3) read with 55th GST Council Meeting Recommendations & Section 10(4)',
|
|
1936
|
+
vendorName: bill.vendorName,
|
|
1937
|
+
vendorGstin: bill.vendorGstin || 'UNREGISTERED',
|
|
1938
|
+
billNumber: bill.billNumber,
|
|
1939
|
+
billDate: bill.billDate,
|
|
1940
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
1941
|
+
booksTaxInr: bill.totalTaxInr,
|
|
1942
|
+
gstr2bTaxableInr: 0,
|
|
1943
|
+
gstr2bTaxInr: 0,
|
|
1944
|
+
varianceTaxableInr: 0,
|
|
1945
|
+
varianceTaxInr: 0,
|
|
1946
|
+
isRule37Overdue: false,
|
|
1947
|
+
daysElapsedSinceBill,
|
|
1948
|
+
rule37Status: 'EXEMPT_NON_GST_SUPPLY',
|
|
1949
|
+
rule37ReversalTaxInr: 0,
|
|
1950
|
+
rule37InterestInr: 0,
|
|
1951
|
+
rule37InterestExplanation: '',
|
|
1952
|
+
isReclaimableUponPayment: false,
|
|
1953
|
+
paymentAppropriationDetails: null,
|
|
1954
|
+
isRcm: false,
|
|
1955
|
+
isCompositionExcluded: true,
|
|
1956
|
+
isInterveningRegularizedUnpaid: true,
|
|
1957
|
+
jurisdictionDecisions: {
|
|
1958
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1959
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
1960
|
+
itcEligibility: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
1961
|
+
operationalRouting: 'BOOK_AS_EXPENSE_NO_ITC'
|
|
1962
|
+
},
|
|
1963
|
+
statutoryAction: 'Intervening period (10 Oct 2024 to 15 Jan 2025) regularized on "as is where is" basis per 55th GST Council meeting. No retrospective recovery or penal demand payable by composition taxpayer; zero RCM cash discharge required; consideration expensed.',
|
|
1964
|
+
auditEvidence: {
|
|
1965
|
+
inBooks: true,
|
|
1966
|
+
inGstr2b: false,
|
|
1967
|
+
matchType: 'INTERVENING_REGULARIZED_UNPAID',
|
|
1968
|
+
isCompositionRegularizedUnpaid: true
|
|
1969
|
+
},
|
|
1970
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
1971
|
+
bill,
|
|
1972
|
+
record2b: null,
|
|
1973
|
+
matchType: 'INTERVENING_REGULARIZED_UNPAID',
|
|
1974
|
+
disposition: 'RCM_COMPOSITION_INTERVENING_REGULARIZED_UNPAID',
|
|
1975
|
+
statutorySection: 'CGST Act Section 9(3) read with 55th GST Council Meeting Recommendations & Section 10(4)',
|
|
1976
|
+
jurisdictionDecisions: {
|
|
1977
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
1978
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
1979
|
+
itcEligibility: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
1980
|
+
operationalRouting: 'BOOK_AS_EXPENSE_NO_ITC'
|
|
1981
|
+
},
|
|
1982
|
+
uncertaintyReason: null,
|
|
1983
|
+
asOfDate
|
|
1984
|
+
})
|
|
1985
|
+
});
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// Check for Composition Scheme Exclusion (Notification 07/2025-CT(Rate))
|
|
1990
|
+
if (bill.isCompositionExcludedFromRentRcm) {
|
|
1991
|
+
compositionExcludedCount++;
|
|
1992
|
+
compositionExcludedItcPaise += booksTaxPaise;
|
|
1993
|
+
|
|
1994
|
+
items.push({
|
|
1995
|
+
disposition: 'COMPOSITION_EXCLUDED_RCM_SUPPLY',
|
|
1996
|
+
itcStatus: 'INELIGIBLE_COMPOSITION_SCHEME_SEC10_4',
|
|
1997
|
+
statutorySection: 'CGST Act Section 9(3) read with Notification No. 07/2025-CT(Rate) & Section 10(4)',
|
|
1998
|
+
vendorName: bill.vendorName,
|
|
1999
|
+
vendorGstin: bill.vendorGstin || 'UNREGISTERED',
|
|
2000
|
+
billNumber: bill.billNumber,
|
|
2001
|
+
billDate: bill.billDate,
|
|
2002
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2003
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2004
|
+
gstr2bTaxableInr: 0,
|
|
2005
|
+
gstr2bTaxInr: 0,
|
|
2006
|
+
varianceTaxableInr: 0,
|
|
2007
|
+
varianceTaxInr: 0,
|
|
2008
|
+
isRule37Overdue: false,
|
|
2009
|
+
daysElapsedSinceBill,
|
|
2010
|
+
rule37Status: 'EXEMPT_NON_GST_SUPPLY',
|
|
2011
|
+
rule37ReversalTaxInr: 0,
|
|
2012
|
+
rule37InterestInr: 0,
|
|
2013
|
+
rule37InterestExplanation: '',
|
|
2014
|
+
isReclaimableUponPayment: false,
|
|
2015
|
+
paymentAppropriationDetails: null,
|
|
2016
|
+
isRcm: false,
|
|
2017
|
+
isCompositionExcluded: true,
|
|
2018
|
+
jurisdictionDecisions: {
|
|
2019
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
2020
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
2021
|
+
itcEligibility: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
2022
|
+
operationalRouting: 'BOOK_AS_EXPENSE_NO_ITC'
|
|
2023
|
+
},
|
|
2024
|
+
statutoryAction: 'Commercial property renting from unregistered person is excluded from RCM for composition taxpayers w.e.f. 16 January 2025 per Notification No. 07/2025-CT(Rate). No reverse charge tax payable; zero ITC available under Section 10(4). Book full consideration as business expense.',
|
|
2025
|
+
auditEvidence: {
|
|
2026
|
+
inBooks: true,
|
|
2027
|
+
inGstr2b: false,
|
|
2028
|
+
matchType: 'COMPOSITION_EXCLUDED_RCM',
|
|
2029
|
+
isCompositionExcluded: true
|
|
2030
|
+
},
|
|
2031
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2032
|
+
bill,
|
|
2033
|
+
record2b: null,
|
|
2034
|
+
matchType: 'COMPOSITION_EXCLUDED_RCM',
|
|
2035
|
+
disposition: 'COMPOSITION_EXCLUDED_RCM_SUPPLY',
|
|
2036
|
+
statutorySection: 'CGST Act Section 9(3) read with Notification No. 07/2025-CT(Rate) & Section 10(4)',
|
|
2037
|
+
jurisdictionDecisions: {
|
|
2038
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
2039
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
2040
|
+
itcEligibility: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
2041
|
+
operationalRouting: 'BOOK_AS_EXPENSE_NO_ITC'
|
|
2042
|
+
},
|
|
2043
|
+
uncertaintyReason: null,
|
|
2044
|
+
asOfDate
|
|
2045
|
+
})
|
|
2046
|
+
});
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
if (isRcm && (!bill.vendorGstin || bill.vendorGstin === 'UNREGISTERED' || bill.isCommercialRentRcm)) {
|
|
2051
|
+
rcmUnregisteredEligibleCount++;
|
|
2052
|
+
rcmUnregisteredEligibleItcPaise += booksTaxPaise;
|
|
2053
|
+
eligibleItcPaise += booksTaxPaise;
|
|
2054
|
+
|
|
2055
|
+
const statutorySection = bill.isCommercialRentRcm
|
|
2056
|
+
? 'CGST Act Section 9(3) read with Notification No. 09/2024-CT(Rate) Entry 5AB & Section 31(3)(f)'
|
|
2057
|
+
: 'CGST Act Section 9(3)/9(4) read with Section 31(3)(f)';
|
|
2058
|
+
|
|
2059
|
+
const statutoryAction = bill.isCommercialRentRcm
|
|
2060
|
+
? 'Commercial property renting by unregistered supplier (Entry 5AB, Notification No. 09/2024-CT(Rate) w.e.f. 10 Oct 2024). Recipient must issue self-invoice (Sec 31(3)(f)), payment voucher (Sec 31(3)(g)), pay 18% GST in cash in Table 3.1(d), and claim ITC in Table 4(A)(3).'
|
|
2061
|
+
: 'Inward supply liable to reverse charge from unregistered person. Recipient must issue self-invoice (Sec 31(3)(f)), pay tax in cash in Table 3.1(d), and claim ITC in Table 4(A)(3).';
|
|
2062
|
+
|
|
2063
|
+
items.push({
|
|
2064
|
+
disposition: 'RCM_UNREGISTERED_SUPPLIER_ELIGIBLE',
|
|
2065
|
+
itcStatus: 'ELIGIBLE_RCM_TABLE_4A3',
|
|
2066
|
+
statutorySection,
|
|
2067
|
+
vendorName: bill.vendorName,
|
|
2068
|
+
vendorGstin: bill.vendorGstin || 'UNREGISTERED',
|
|
2069
|
+
billNumber: bill.billNumber,
|
|
2070
|
+
billDate: bill.billDate,
|
|
2071
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2072
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2073
|
+
gstr2bTaxableInr: 0,
|
|
2074
|
+
gstr2bTaxInr: 0,
|
|
2075
|
+
varianceTaxableInr: 0,
|
|
2076
|
+
varianceTaxInr: 0,
|
|
2077
|
+
isRule37Overdue: false,
|
|
2078
|
+
daysElapsedSinceBill,
|
|
2079
|
+
rule37Status: 'EXEMPT_RCM_SUPPLY',
|
|
2080
|
+
rule37ReversalTaxInr: 0,
|
|
2081
|
+
rule37InterestInr: 0,
|
|
2082
|
+
rule37InterestExplanation: '',
|
|
2083
|
+
isReclaimableUponPayment: false,
|
|
2084
|
+
paymentAppropriationDetails: null,
|
|
2085
|
+
isRcm: true,
|
|
2086
|
+
isCommercialRentRcm: Boolean(bill.isCommercialRentRcm),
|
|
2087
|
+
jurisdictionDecisions: {
|
|
2088
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
2089
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
2090
|
+
itcEligibility: 'ELIGIBLE_INWARD_RCM',
|
|
2091
|
+
operationalRouting: 'ISSUE_SELF_INVOICE_AND_PAY_CASH_TABLE_3_1_D'
|
|
2092
|
+
},
|
|
2093
|
+
statutoryAction,
|
|
2094
|
+
auditEvidence: {
|
|
2095
|
+
inBooks: true,
|
|
2096
|
+
inGstr2b: false,
|
|
2097
|
+
matchType: 'RCM_UNREGISTERED_SUPPLY',
|
|
2098
|
+
isRcm: true,
|
|
2099
|
+
isCommercialRentRcm: Boolean(bill.isCommercialRentRcm)
|
|
2100
|
+
},
|
|
2101
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2102
|
+
bill,
|
|
2103
|
+
record2b: null,
|
|
2104
|
+
matchType: 'RCM_UNREGISTERED_SUPPLY',
|
|
2105
|
+
disposition: 'RCM_UNREGISTERED_SUPPLIER_ELIGIBLE',
|
|
2106
|
+
statutorySection,
|
|
2107
|
+
jurisdictionDecisions: {
|
|
2108
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
2109
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
2110
|
+
itcEligibility: 'ELIGIBLE_INWARD_RCM',
|
|
2111
|
+
operationalRouting: 'ISSUE_SELF_INVOICE_AND_PAY_CASH_TABLE_3_1_D'
|
|
2112
|
+
},
|
|
2113
|
+
uncertaintyReason: null,
|
|
2114
|
+
asOfDate
|
|
2115
|
+
})
|
|
2116
|
+
});
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
missingIn2bCount++;
|
|
2121
|
+
atRiskUnfiledItcPaise += booksTaxPaise;
|
|
2122
|
+
|
|
2123
|
+
const isTimingDifference = daysElapsedSinceBill <= 30;
|
|
2124
|
+
const disposition = isTimingDifference ? 'MISSING_IN_GSTR2B_TIMING' : 'MISSING_IN_GSTR2B';
|
|
2125
|
+
const itcStatus = isTimingDifference ? 'PROVISIONAL_PENDING_NEXT_GSTR2B' : 'PROVISIONAL_AT_RISK_RESTRICTED';
|
|
2126
|
+
|
|
2127
|
+
items.push({
|
|
2128
|
+
disposition,
|
|
2129
|
+
itcStatus,
|
|
2130
|
+
statutorySection: 'CGST Act Section 16(2)(aa) read with Rule 36(4)',
|
|
2131
|
+
vendorName: bill.vendorName,
|
|
2132
|
+
vendorGstin: bill.vendorGstin,
|
|
2133
|
+
billNumber: bill.billNumber,
|
|
2134
|
+
billDate: bill.billDate,
|
|
2135
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2136
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2137
|
+
gstr2bTaxableInr: 0,
|
|
2138
|
+
gstr2bTaxInr: 0,
|
|
2139
|
+
varianceTaxableInr: bill.taxableAmountInr,
|
|
2140
|
+
varianceTaxInr: bill.totalTaxInr,
|
|
2141
|
+
isRule37Overdue,
|
|
2142
|
+
daysElapsedSinceBill,
|
|
2143
|
+
rule37Status,
|
|
2144
|
+
rule37ReversalTaxInr,
|
|
2145
|
+
rule37InterestInr,
|
|
2146
|
+
rule37InterestExplanation,
|
|
2147
|
+
isReclaimableUponPayment,
|
|
2148
|
+
paymentAppropriationDetails,
|
|
2149
|
+
jurisdictionDecisions: {
|
|
2150
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
2151
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
2152
|
+
itcEligibility: 'PROVISIONAL_PENDING_FILING',
|
|
2153
|
+
operationalRouting: 'WITHHOLD_CLAIM_PENDING_SUPPLIER_GSTR1'
|
|
2154
|
+
},
|
|
2155
|
+
statutoryAction: isTimingDifference
|
|
2156
|
+
? 'Invoice dated within recent tax cycle. May appear in subsequent Form GSTR-2B or vendor IFF filing. Withhold claim until populated.'
|
|
2157
|
+
: 'Invoice unfiled by vendor. Provisional ITC restricted under Section 16(2)(aa) & Rule 36(4). Withhold claim in GSTR-3B Table 4(A)(5). Issue vendor notice.',
|
|
2158
|
+
auditEvidence: {
|
|
2159
|
+
inBooks: true,
|
|
2160
|
+
inGstr2b: false,
|
|
2161
|
+
matchType: 'UNMATCHED',
|
|
2162
|
+
isTimingDifference
|
|
2163
|
+
},
|
|
2164
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2165
|
+
bill,
|
|
2166
|
+
record2b: null,
|
|
2167
|
+
matchType: 'UNMATCHED',
|
|
2168
|
+
disposition,
|
|
2169
|
+
statutorySection: 'CGST Act Section 16(2)(aa) read with Rule 36(4)',
|
|
2170
|
+
jurisdictionDecisions: {
|
|
2171
|
+
taxClassification: hasIgst ? 'INTERSTATE_CORRECT' : 'INTRASTATE_CORRECT',
|
|
2172
|
+
recipientJurisdiction: 'FILING_GSTIN_MATCHES_POS',
|
|
2173
|
+
itcEligibility: 'PROVISIONAL_PENDING_FILING',
|
|
2174
|
+
operationalRouting: 'WITHHOLD_CLAIM_PENDING_SUPPLIER_GSTR1'
|
|
2175
|
+
},
|
|
2176
|
+
uncertaintyReason: null,
|
|
2177
|
+
asOfDate
|
|
2178
|
+
})
|
|
2179
|
+
});
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// Mark 2B record as matched
|
|
2184
|
+
matched2bSet.add(matchEntry.index);
|
|
2185
|
+
const record2b = matchEntry.record;
|
|
2186
|
+
|
|
2187
|
+
const gstr2bTaxablePaise = toIntegerPaise(record2b.taxableAmountInr);
|
|
2188
|
+
const gstr2bTaxPaise = toIntegerPaise(record2b.totalTaxInr);
|
|
2189
|
+
|
|
2190
|
+
const diffTaxablePaise = Math.abs(booksTaxablePaise - gstr2bTaxablePaise);
|
|
2191
|
+
const diffTaxPaise = Math.abs(booksTaxPaise - gstr2bTaxPaise);
|
|
2192
|
+
|
|
2193
|
+
// ── 4-Way Place of Supply (POS) Triangulation & Decoupled Decisions ──
|
|
2194
|
+
const supplierStateCode = record2b.supplierGstin ? record2b.supplierGstin.slice(0, 2) : (bill.vendorGstin ? bill.vendorGstin.slice(0, 2) : null);
|
|
2195
|
+
const posStateCode = record2b.posStateCode || bill.posStateCode || null;
|
|
2196
|
+
|
|
2197
|
+
// Uncertainty Check 4: Missing POS
|
|
2198
|
+
if (!posStateCode) {
|
|
2199
|
+
requiresHumanVerificationCount++;
|
|
2200
|
+
requiresHumanVerificationItcPaise += booksTaxPaise;
|
|
2201
|
+
|
|
2202
|
+
const itemMatchType = exactComposite === `${gstinKey}|${record2b.exactKey}` ? 'EXACT' : 'ALPHANUMERIC';
|
|
2203
|
+
items.push({
|
|
2204
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
2205
|
+
uncertaintyReason: 'MISSING_POS_DATA',
|
|
2206
|
+
itcStatus: 'INDETERMINATE_MISSING_JURISDICTION',
|
|
2207
|
+
statutorySection: 'IGST Act Section 10 & 12 (Place of Supply Determination)',
|
|
2208
|
+
vendorName: bill.vendorName,
|
|
2209
|
+
vendorGstin: bill.vendorGstin,
|
|
2210
|
+
billNumber: bill.billNumber,
|
|
2211
|
+
billDate: bill.billDate,
|
|
2212
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2213
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2214
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2215
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2216
|
+
varianceTaxableInr: fromIntegerPaise(diffTaxablePaise),
|
|
2217
|
+
varianceTaxInr: fromIntegerPaise(diffTaxPaise),
|
|
2218
|
+
isRule37Overdue,
|
|
2219
|
+
daysElapsedSinceBill,
|
|
2220
|
+
rule37Status,
|
|
2221
|
+
rule37ReversalTaxInr,
|
|
2222
|
+
rule37InterestInr,
|
|
2223
|
+
rule37InterestExplanation,
|
|
2224
|
+
isReclaimableUponPayment,
|
|
2225
|
+
paymentAppropriationDetails,
|
|
2226
|
+
jurisdictionDecisions: {
|
|
2227
|
+
taxClassification: 'INDETERMINATE',
|
|
2228
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
2229
|
+
itcEligibility: 'INDETERMINATE',
|
|
2230
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
2231
|
+
},
|
|
2232
|
+
statutoryAction: 'Place of Supply information is missing or indeterminate in both Books and GSTR-2B. Human intervention required to ascertain place of supply from shipping or service delivery documentation.',
|
|
2233
|
+
auditEvidence: { inBooks: true, inGstr2b: true, missingPos: true },
|
|
2234
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2235
|
+
bill,
|
|
2236
|
+
record2b,
|
|
2237
|
+
matchType: itemMatchType,
|
|
2238
|
+
disposition: 'REQUIRES_HUMAN_VERIFICATION',
|
|
2239
|
+
statutorySection: 'IGST Act Section 10 & 12 (Place of Supply Determination)',
|
|
2240
|
+
jurisdictionDecisions: {
|
|
2241
|
+
taxClassification: 'INDETERMINATE',
|
|
2242
|
+
recipientJurisdiction: 'INDETERMINATE',
|
|
2243
|
+
itcEligibility: 'INDETERMINATE',
|
|
2244
|
+
operationalRouting: 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
2245
|
+
},
|
|
2246
|
+
uncertaintyReason: 'MISSING_POS_DATA',
|
|
2247
|
+
asOfDate
|
|
2248
|
+
})
|
|
2249
|
+
});
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const isIntrastateLevy = (record2b.cgstInr > 0 || record2b.sgstInr > 0 || bill.cgstInr > 0 || bill.sgstInr > 0);
|
|
2254
|
+
const isSupplierLocal = supplierStateCode && posStateCode && supplierStateCode === posStateCode;
|
|
2255
|
+
|
|
2256
|
+
// Decoupled Decision 1: Tax Determination under IGST Act
|
|
2257
|
+
let taxClassification = 'INTRASTATE_CORRECT';
|
|
2258
|
+
if (isSupplierLocal) {
|
|
2259
|
+
taxClassification = isIntrastateLevy ? 'INTRASTATE_CORRECT' : 'ERRONEOUS_INTERSTATE_LEVY';
|
|
2260
|
+
} else {
|
|
2261
|
+
taxClassification = isIntrastateLevy ? 'ERRONEOUS_INTRASTATE_LEVY' : 'INTERSTATE_CORRECT';
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// Decoupled Decision 2: Recipient Jurisdiction
|
|
2265
|
+
let recipientJurisdiction = 'FILING_GSTIN_MATCHES_POS';
|
|
2266
|
+
if (recipientFilingState && posStateCode && recipientFilingState !== posStateCode) {
|
|
2267
|
+
if (recipientStateSet.has(posStateCode)) {
|
|
2268
|
+
recipientJurisdiction = 'REGISTERED_DISTINCT_PERSON_BRANCH';
|
|
2269
|
+
} else {
|
|
2270
|
+
recipientJurisdiction = 'UNREGISTERED_IN_POS_STATE';
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Decoupled Decision 3 & 4: ITC Eligibility & Operational Routing
|
|
2275
|
+
let itcEligibility = 'ELIGIBLE_NORMAL';
|
|
2276
|
+
let operationalRouting = 'CLAIM_IN_CURRENT_GSTR3B_TABLE_4A5';
|
|
2277
|
+
|
|
2278
|
+
if (record2b.itcAvailable === false) {
|
|
2279
|
+
itcEligibility = 'INELIGIBLE_PORTAL_FLAGGED';
|
|
2280
|
+
operationalRouting = 'REPORT_IN_GSTR3B_TABLE_4B1_PERMANENT_REVERSAL';
|
|
2281
|
+
} else if (bill.isBlocked17_5) {
|
|
2282
|
+
itcEligibility = 'INELIGIBLE_BLOCKED_SEC_17_5';
|
|
2283
|
+
operationalRouting = 'REPORT_IN_GSTR3B_TABLE_4B1_PERMANENT_REVERSAL';
|
|
2284
|
+
} else if (recipientJurisdiction === 'REGISTERED_DISTINCT_PERSON_BRANCH') {
|
|
2285
|
+
itcEligibility = 'ELIGIBLE_IN_BRANCH_RETURN';
|
|
2286
|
+
operationalRouting = 'ROUTE_TO_BRANCH_GSTIN';
|
|
2287
|
+
} else if (recipientJurisdiction === 'UNREGISTERED_IN_POS_STATE') {
|
|
2288
|
+
const isStatutoryImmovableStay = /hotel|lodge|lodging|room stay|guest house|accommodation|campsite|catering|restaurant|real estate|immovable/i.test(bill.category || '');
|
|
2289
|
+
const isB2bEventManagement = /event|conference|exhibition|seminar|summit|event management/i.test(bill.category || '');
|
|
2290
|
+
|
|
2291
|
+
if (isStatutoryImmovableStay && isSupplierLocal) {
|
|
2292
|
+
// Section 12(3)(a): Immovable lodging POS is property location. Since recipient lacks registration in this state, local CGST/SGST is stranded.
|
|
2293
|
+
itcEligibility = 'INELIGIBLE_STRANDED_LOCAL_LEVY';
|
|
2294
|
+
operationalRouting = 'BOOK_AS_EXPENSE_NO_ITC';
|
|
2295
|
+
} else if (isB2bEventManagement) {
|
|
2296
|
+
// IGST Act Section 12(7)(a)(i): POS for organizing event/conference to a registered person is the location of the recipient!
|
|
2297
|
+
// If supplier charged local CGST/SGST, it is an ERRONEOUS_INTRASTATE_LEVY. Supplier MUST issue credit note and re-invoice with IGST.
|
|
2298
|
+
itcEligibility = 'INELIGIBLE_DEFECTIVE_INVOICE';
|
|
2299
|
+
operationalRouting = 'REQUEST_VENDOR_CREDIT_NOTE_AMENDMENT';
|
|
2300
|
+
} else {
|
|
2301
|
+
itcEligibility = 'INELIGIBLE_DEFECTIVE_INVOICE';
|
|
2302
|
+
operationalRouting = 'REQUEST_VENDOR_CREDIT_NOTE_AMENDMENT';
|
|
2303
|
+
}
|
|
2304
|
+
} else if (diffTaxablePaise > tolerancePaise || diffTaxPaise > tolerancePaise) {
|
|
2305
|
+
itcEligibility = 'ELIGIBLE_NORMAL';
|
|
2306
|
+
operationalRouting = 'REQUEST_VENDOR_CREDIT_NOTE_AMENDMENT';
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
const jurisdictionDecisions = {
|
|
2310
|
+
taxClassification,
|
|
2311
|
+
recipientJurisdiction,
|
|
2312
|
+
itcEligibility,
|
|
2313
|
+
operationalRouting
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
const matchType = exactComposite === `${gstinKey}|${record2b.exactKey}` ? 'EXACT' : 'ALPHANUMERIC';
|
|
2317
|
+
|
|
2318
|
+
if (record2b.itcAvailable === false) {
|
|
2319
|
+
// Disposition: GSTR2B_INELIGIBLE_FLAGGED_BY_GSTN
|
|
2320
|
+
gstr2bPortalIneligibleCount++;
|
|
2321
|
+
gstr2bPortalIneligibleItcPaise += booksTaxPaise;
|
|
2322
|
+
|
|
2323
|
+
items.push({
|
|
2324
|
+
disposition: 'GSTR2B_INELIGIBLE_FLAGGED_BY_GSTN',
|
|
2325
|
+
itcStatus: 'INELIGIBLE_PORTAL_FLAGGED',
|
|
2326
|
+
statutorySection: 'CGST Act Section 16(4) / 16(2) (GSTN Ineligibility Flag)',
|
|
2327
|
+
vendorName: bill.vendorName,
|
|
2328
|
+
vendorGstin: bill.vendorGstin,
|
|
2329
|
+
billNumber: bill.billNumber,
|
|
2330
|
+
billDate: bill.billDate,
|
|
2331
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2332
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2333
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2334
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2335
|
+
varianceTaxableInr: fromIntegerPaise(diffTaxablePaise),
|
|
2336
|
+
varianceTaxInr: fromIntegerPaise(diffTaxPaise),
|
|
2337
|
+
isRule37Overdue,
|
|
2338
|
+
daysElapsedSinceBill,
|
|
2339
|
+
rule37Status,
|
|
2340
|
+
rule37ReversalTaxInr,
|
|
2341
|
+
rule37InterestInr,
|
|
2342
|
+
rule37InterestExplanation,
|
|
2343
|
+
isReclaimableUponPayment,
|
|
2344
|
+
paymentAppropriationDetails,
|
|
2345
|
+
jurisdictionDecisions,
|
|
2346
|
+
statutoryAction: `Form GSTR-2B has flagged this credit as ineligible: ${record2b.itcRevReason || 'Section 16(4) time-barred or supplier/recipient restriction'}. Do not avail in GSTR-3B Table 4(A)(5).`,
|
|
2347
|
+
auditEvidence: {
|
|
2348
|
+
inBooks: true,
|
|
2349
|
+
inGstr2b: true,
|
|
2350
|
+
itcRevReason: record2b.itcRevReason
|
|
2351
|
+
},
|
|
2352
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2353
|
+
bill,
|
|
2354
|
+
record2b,
|
|
2355
|
+
matchType,
|
|
2356
|
+
disposition: 'GSTR2B_INELIGIBLE_FLAGGED_BY_GSTN',
|
|
2357
|
+
statutorySection: 'CGST Act Section 16(4) / 16(2) (GSTN Ineligibility Flag)',
|
|
2358
|
+
jurisdictionDecisions,
|
|
2359
|
+
uncertaintyReason: null,
|
|
2360
|
+
asOfDate
|
|
2361
|
+
})
|
|
2362
|
+
});
|
|
2363
|
+
} else if (bill.isBlocked17_5) {
|
|
2364
|
+
// Disposition: MATCHED_INELIGIBLE_STATUTORY_BLOCK_17_5
|
|
2365
|
+
blocked17_5Count++;
|
|
2366
|
+
blocked17_5ItcPaise += booksTaxPaise;
|
|
2367
|
+
|
|
2368
|
+
items.push({
|
|
2369
|
+
disposition: 'MATCHED_INELIGIBLE_STATUTORY_BLOCK_17_5',
|
|
2370
|
+
itcStatus: 'INELIGIBLE_REVERSIBLE_TABLE_4B1',
|
|
2371
|
+
statutorySection: 'CGST Act Section 17(5) (Statutorily Blocked Input Tax Credit)',
|
|
2372
|
+
vendorName: bill.vendorName,
|
|
2373
|
+
vendorGstin: bill.vendorGstin,
|
|
2374
|
+
billNumber: bill.billNumber,
|
|
2375
|
+
billDate: bill.billDate,
|
|
2376
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2377
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2378
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2379
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2380
|
+
varianceTaxableInr: fromIntegerPaise(diffTaxablePaise),
|
|
2381
|
+
varianceTaxInr: fromIntegerPaise(diffTaxPaise),
|
|
2382
|
+
isRule37Overdue,
|
|
2383
|
+
daysElapsedSinceBill,
|
|
2384
|
+
rule37Status,
|
|
2385
|
+
rule37ReversalTaxInr,
|
|
2386
|
+
rule37InterestInr,
|
|
2387
|
+
rule37InterestExplanation,
|
|
2388
|
+
isReclaimableUponPayment,
|
|
2389
|
+
paymentAppropriationDetails,
|
|
2390
|
+
jurisdictionDecisions,
|
|
2391
|
+
statutoryAction: 'Report in GSTR-3B Table 4(B)(1) as Ineligible ITC. Do not credit to electronic ledger.',
|
|
2392
|
+
auditEvidence: {
|
|
2393
|
+
inBooks: true,
|
|
2394
|
+
inGstr2b: true,
|
|
2395
|
+
matchType,
|
|
2396
|
+
blockedCategory: bill.category || 'Section 17(5) Item'
|
|
2397
|
+
},
|
|
2398
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2399
|
+
bill,
|
|
2400
|
+
record2b,
|
|
2401
|
+
matchType,
|
|
2402
|
+
disposition: 'MATCHED_INELIGIBLE_STATUTORY_BLOCK_17_5',
|
|
2403
|
+
statutorySection: 'CGST Act Section 17(5) (Statutorily Blocked Input Tax Credit)',
|
|
2404
|
+
jurisdictionDecisions,
|
|
2405
|
+
uncertaintyReason: null,
|
|
2406
|
+
asOfDate
|
|
2407
|
+
})
|
|
2408
|
+
});
|
|
2409
|
+
} else if (operationalRouting === 'ROUTE_TO_BRANCH_GSTIN') {
|
|
2410
|
+
// Disposition: MATCHED_ROUTABLE_TO_BRANCH_GSTIN
|
|
2411
|
+
routableToBranchCount++;
|
|
2412
|
+
routableToBranchItcPaise += booksTaxPaise;
|
|
2413
|
+
|
|
2414
|
+
items.push({
|
|
2415
|
+
disposition: 'MATCHED_ROUTABLE_TO_BRANCH_GSTIN',
|
|
2416
|
+
itcStatus: 'ROUTABLE_TO_ANOTHER_BRANCH_GSTIN',
|
|
2417
|
+
statutorySection: 'CGST Act Section 25(4) read with IGST Act Section 12 (Distinct Persons)',
|
|
2418
|
+
vendorName: bill.vendorName,
|
|
2419
|
+
vendorGstin: bill.vendorGstin,
|
|
2420
|
+
billNumber: bill.billNumber,
|
|
2421
|
+
billDate: bill.billDate,
|
|
2422
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2423
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2424
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2425
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2426
|
+
varianceTaxableInr: fromIntegerPaise(diffTaxablePaise),
|
|
2427
|
+
varianceTaxInr: fromIntegerPaise(diffTaxPaise),
|
|
2428
|
+
isRule37Overdue,
|
|
2429
|
+
daysElapsedSinceBill,
|
|
2430
|
+
rule37Status,
|
|
2431
|
+
rule37ReversalTaxInr,
|
|
2432
|
+
rule37InterestInr,
|
|
2433
|
+
rule37InterestExplanation,
|
|
2434
|
+
isReclaimableUponPayment,
|
|
2435
|
+
paymentAppropriationDetails,
|
|
2436
|
+
jurisdictionDecisions,
|
|
2437
|
+
statutoryAction: `Intra-state supply in State ${posStateCode}. Taxpayer holds valid registration in State ${posStateCode}. Route this voucher to branch GSTIN for claim in that state's GSTR-3B.`,
|
|
2438
|
+
auditEvidence: {
|
|
2439
|
+
inBooks: true,
|
|
2440
|
+
inGstr2b: true,
|
|
2441
|
+
posStateCode,
|
|
2442
|
+
recipientFilingState,
|
|
2443
|
+
recipientRegisteredStates: Array.from(recipientStateSet)
|
|
2444
|
+
},
|
|
2445
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2446
|
+
bill,
|
|
2447
|
+
record2b,
|
|
2448
|
+
matchType,
|
|
2449
|
+
disposition: 'MATCHED_ROUTABLE_TO_BRANCH_GSTIN',
|
|
2450
|
+
statutorySection: 'CGST Act Section 25(4) read with IGST Act Section 12 (Distinct Persons)',
|
|
2451
|
+
jurisdictionDecisions,
|
|
2452
|
+
uncertaintyReason: null,
|
|
2453
|
+
asOfDate
|
|
2454
|
+
})
|
|
2455
|
+
});
|
|
2456
|
+
} else if (operationalRouting === 'BOOK_AS_EXPENSE_NO_ITC') {
|
|
2457
|
+
// Disposition: MATCHED_INELIGIBLE_STRANDED_LOCAL_CGST_SGST
|
|
2458
|
+
strandedLocalCount++;
|
|
2459
|
+
strandedLocalItcPaise += booksTaxPaise;
|
|
2460
|
+
|
|
2461
|
+
items.push({
|
|
2462
|
+
disposition: 'MATCHED_INELIGIBLE_STRANDED_LOCAL_CGST_SGST',
|
|
2463
|
+
itcStatus: 'INELIGIBLE_STRANDED_LOCAL_LEVY',
|
|
2464
|
+
statutorySection: 'IGST Act Section 12(3)/12(4) & CGST Act Section 16(2)(c)',
|
|
2465
|
+
vendorName: bill.vendorName,
|
|
2466
|
+
vendorGstin: bill.vendorGstin,
|
|
2467
|
+
billNumber: bill.billNumber,
|
|
2468
|
+
billDate: bill.billDate,
|
|
2469
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2470
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2471
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2472
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2473
|
+
varianceTaxableInr: fromIntegerPaise(diffTaxablePaise),
|
|
2474
|
+
varianceTaxInr: fromIntegerPaise(diffTaxPaise),
|
|
2475
|
+
isRule37Overdue,
|
|
2476
|
+
daysElapsedSinceBill,
|
|
2477
|
+
rule37Status,
|
|
2478
|
+
rule37ReversalTaxInr,
|
|
2479
|
+
rule37InterestInr,
|
|
2480
|
+
rule37InterestExplanation,
|
|
2481
|
+
isReclaimableUponPayment,
|
|
2482
|
+
paymentAppropriationDetails,
|
|
2483
|
+
jurisdictionDecisions,
|
|
2484
|
+
statutoryAction: `Mandatory intra-state supply in supplier state under Section 12(3)/12(4) (e.g. hotel/catering). Recipient lacks registration in State ${posStateCode}. Credit is stranded; absorb as operating expense.`,
|
|
2485
|
+
auditEvidence: {
|
|
2486
|
+
inBooks: true,
|
|
2487
|
+
inGstr2b: true,
|
|
2488
|
+
posStateCode,
|
|
2489
|
+
recipientFilingState
|
|
2490
|
+
},
|
|
2491
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2492
|
+
bill,
|
|
2493
|
+
record2b,
|
|
2494
|
+
matchType,
|
|
2495
|
+
disposition: 'MATCHED_INELIGIBLE_STRANDED_LOCAL_CGST_SGST',
|
|
2496
|
+
statutorySection: 'IGST Act Section 12(3)/12(4) & CGST Act Section 16(2)(c)',
|
|
2497
|
+
jurisdictionDecisions,
|
|
2498
|
+
uncertaintyReason: null,
|
|
2499
|
+
asOfDate
|
|
2500
|
+
})
|
|
2501
|
+
});
|
|
2502
|
+
} else if (taxClassification === 'ERRONEOUS_INTRASTATE_LEVY' || (recipientJurisdiction === 'UNREGISTERED_IN_POS_STATE' && operationalRouting === 'REQUEST_VENDOR_CREDIT_NOTE_AMENDMENT')) {
|
|
2503
|
+
// Disposition: MATCHED_INELIGIBLE_PLACE_OF_SUPPLY_MISMATCH
|
|
2504
|
+
posMismatchCount++;
|
|
2505
|
+
posMismatchItcPaise += booksTaxPaise;
|
|
2506
|
+
|
|
2507
|
+
items.push({
|
|
2508
|
+
disposition: 'MATCHED_INELIGIBLE_PLACE_OF_SUPPLY_MISMATCH',
|
|
2509
|
+
itcStatus: 'INELIGIBLE_DEFECTIVE_INVOICE_POS_ERROR',
|
|
2510
|
+
statutorySection: 'CGST Act Section 16(2)(c) read with IGST Act Section 10(1)(a) & Section 12',
|
|
2511
|
+
vendorName: bill.vendorName,
|
|
2512
|
+
vendorGstin: bill.vendorGstin,
|
|
2513
|
+
billNumber: bill.billNumber,
|
|
2514
|
+
billDate: bill.billDate,
|
|
2515
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2516
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2517
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2518
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2519
|
+
varianceTaxableInr: fromIntegerPaise(diffTaxablePaise),
|
|
2520
|
+
varianceTaxInr: fromIntegerPaise(diffTaxPaise),
|
|
2521
|
+
isRule37Overdue,
|
|
2522
|
+
daysElapsedSinceBill,
|
|
2523
|
+
rule37Status,
|
|
2524
|
+
rule37ReversalTaxInr,
|
|
2525
|
+
rule37InterestInr,
|
|
2526
|
+
rule37InterestExplanation,
|
|
2527
|
+
isReclaimableUponPayment,
|
|
2528
|
+
paymentAppropriationDetails,
|
|
2529
|
+
jurisdictionDecisions,
|
|
2530
|
+
statutoryAction: `Supplier erroneously charged local CGST/SGST of State ${posStateCode} instead of IGST to State ${recipientFilingState}. Demand vendor credit note and corrected IGST invoice.`,
|
|
2531
|
+
auditEvidence: {
|
|
2532
|
+
inBooks: true,
|
|
2533
|
+
inGstr2b: true,
|
|
2534
|
+
posStateCode,
|
|
2535
|
+
recipientFilingState
|
|
2536
|
+
},
|
|
2537
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2538
|
+
bill,
|
|
2539
|
+
record2b,
|
|
2540
|
+
matchType,
|
|
2541
|
+
disposition: 'MATCHED_INELIGIBLE_PLACE_OF_SUPPLY_MISMATCH',
|
|
2542
|
+
statutorySection: 'CGST Act Section 16(2)(c) read with IGST Act Section 10(1)(a) & Section 12',
|
|
2543
|
+
jurisdictionDecisions,
|
|
2544
|
+
uncertaintyReason: null,
|
|
2545
|
+
asOfDate
|
|
2546
|
+
})
|
|
2547
|
+
});
|
|
2548
|
+
} else if (diffTaxablePaise > tolerancePaise || diffTaxPaise > tolerancePaise) {
|
|
2549
|
+
// Disposition: MISMATCH_AMOUNT_VALUE
|
|
2550
|
+
amountMismatchCount++;
|
|
2551
|
+
mismatchedItcPaise += booksTaxPaise;
|
|
2552
|
+
|
|
2553
|
+
items.push({
|
|
2554
|
+
disposition: 'MISMATCH_AMOUNT_VALUE',
|
|
2555
|
+
itcStatus: 'VALUE_DISCREPANCY_PENDING_RESOLUTION',
|
|
2556
|
+
statutorySection: 'CGST Act Section 16(2)(aa) & Rule 36(4)',
|
|
2557
|
+
vendorName: bill.vendorName,
|
|
2558
|
+
vendorGstin: bill.vendorGstin,
|
|
2559
|
+
billNumber: bill.billNumber,
|
|
2560
|
+
billDate: bill.billDate,
|
|
2561
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2562
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2563
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2564
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2565
|
+
varianceTaxableInr: fromIntegerPaise(booksTaxablePaise - gstr2bTaxablePaise),
|
|
2566
|
+
varianceTaxInr: fromIntegerPaise(booksTaxPaise - gstr2bTaxPaise),
|
|
2567
|
+
isRule37Overdue,
|
|
2568
|
+
daysElapsedSinceBill,
|
|
2569
|
+
rule37Status,
|
|
2570
|
+
rule37ReversalTaxInr,
|
|
2571
|
+
rule37InterestInr,
|
|
2572
|
+
rule37InterestExplanation,
|
|
2573
|
+
isReclaimableUponPayment,
|
|
2574
|
+
paymentAppropriationDetails,
|
|
2575
|
+
jurisdictionDecisions,
|
|
2576
|
+
statutoryAction: `Taxable or tax amount exceeds ₹${toleranceInr} tolerance. Restrict claim to lower GSTR-2B amount; seek vendor credit note or amendment.`,
|
|
2577
|
+
auditEvidence: {
|
|
2578
|
+
inBooks: true,
|
|
2579
|
+
inGstr2b: true,
|
|
2580
|
+
matchType,
|
|
2581
|
+
varianceExceeded: true
|
|
2582
|
+
},
|
|
2583
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2584
|
+
bill,
|
|
2585
|
+
record2b,
|
|
2586
|
+
matchType,
|
|
2587
|
+
disposition: 'MISMATCH_AMOUNT_VALUE',
|
|
2588
|
+
statutorySection: 'CGST Act Section 16(2)(aa) & Rule 36(4)',
|
|
2589
|
+
jurisdictionDecisions,
|
|
2590
|
+
uncertaintyReason: null,
|
|
2591
|
+
asOfDate
|
|
2592
|
+
})
|
|
2593
|
+
});
|
|
2594
|
+
} else {
|
|
2595
|
+
// Disposition: MATCHED_ELIGIBLE
|
|
2596
|
+
matchedEligibleCount++;
|
|
2597
|
+
eligibleItcPaise += booksTaxPaise;
|
|
2598
|
+
|
|
2599
|
+
items.push({
|
|
2600
|
+
disposition: 'MATCHED_ELIGIBLE',
|
|
2601
|
+
itcStatus: isRcm ? 'ELIGIBLE_RCM_TABLE_4A3' : 'ELIGIBLE_CREDIT_TABLE_4A5',
|
|
2602
|
+
statutorySection: 'CGST Act Section 16(2) read with Section 16(1)',
|
|
2603
|
+
vendorName: bill.vendorName,
|
|
2604
|
+
vendorGstin: bill.vendorGstin,
|
|
2605
|
+
billNumber: bill.billNumber,
|
|
2606
|
+
billDate: bill.billDate,
|
|
2607
|
+
booksTaxableInr: bill.taxableAmountInr,
|
|
2608
|
+
booksTaxInr: bill.totalTaxInr,
|
|
2609
|
+
gstr2bTaxableInr: record2b.taxableAmountInr,
|
|
2610
|
+
gstr2bTaxInr: record2b.totalTaxInr,
|
|
2611
|
+
varianceTaxableInr: 0,
|
|
2612
|
+
varianceTaxInr: 0,
|
|
2613
|
+
isRule37Overdue,
|
|
2614
|
+
daysElapsedSinceBill,
|
|
2615
|
+
rule37Status,
|
|
2616
|
+
rule37ReversalTaxInr,
|
|
2617
|
+
rule37InterestInr,
|
|
2618
|
+
rule37InterestExplanation,
|
|
2619
|
+
isReclaimableUponPayment,
|
|
2620
|
+
paymentAppropriationDetails,
|
|
2621
|
+
isRcm,
|
|
2622
|
+
jurisdictionDecisions,
|
|
2623
|
+
statutoryAction: isRule37Overdue
|
|
2624
|
+
? `Eligible in GSTR-2B but >180 days unpaid (Rule 37). Must reverse ₹${rule37ReversalTaxInr} in Table 4(B)(2). Re-avail upon subsequent payment.`
|
|
2625
|
+
: (isRcm
|
|
2626
|
+
? '100% matched RCM supply. Pay liability in cash in Table 3.1(d); avail credit in GSTR-3B Table 4(A)(3).'
|
|
2627
|
+
: '100% matched and compliant. Avail full credit in GSTR-3B Table 4(A)(5).'),
|
|
2628
|
+
auditEvidence: {
|
|
2629
|
+
inBooks: true,
|
|
2630
|
+
inGstr2b: true,
|
|
2631
|
+
matchType,
|
|
2632
|
+
varianceExceeded: false,
|
|
2633
|
+
isRcm
|
|
2634
|
+
},
|
|
2635
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2636
|
+
bill,
|
|
2637
|
+
record2b,
|
|
2638
|
+
matchType,
|
|
2639
|
+
disposition: 'MATCHED_ELIGIBLE',
|
|
2640
|
+
statutorySection: 'CGST Act Section 16(2) read with Section 16(1)',
|
|
2641
|
+
jurisdictionDecisions,
|
|
2642
|
+
uncertaintyReason: null,
|
|
2643
|
+
asOfDate
|
|
2644
|
+
})
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// 3. Scan Unmatched GSTR-2B Invoices (Missing in Books)
|
|
2650
|
+
let missingInBooksCount = 0;
|
|
2651
|
+
let missingInBooksTaxPaise = 0;
|
|
2652
|
+
|
|
2653
|
+
for (let i = 0; i < parsed2b.length; i++) {
|
|
2654
|
+
const record = parsed2b[i];
|
|
2655
|
+
total2bTaxablePaise += toIntegerPaise(record.taxableAmountInr);
|
|
2656
|
+
total2bTaxPaise += toIntegerPaise(record.totalTaxInr);
|
|
2657
|
+
|
|
2658
|
+
if (!matched2bSet.has(i)) {
|
|
2659
|
+
missingInBooksCount++;
|
|
2660
|
+
const taxPaise = toIntegerPaise(record.totalTaxInr);
|
|
2661
|
+
missingInBooksTaxPaise += taxPaise;
|
|
2662
|
+
|
|
2663
|
+
const recordDateObj = new Date(record.invoiceDate);
|
|
2664
|
+
const asOfDateObj = new Date(asOfDate);
|
|
2665
|
+
const daysSinceInvoice = isNaN(recordDateObj) ? 999 : Math.floor((asOfDateObj - recordDateObj) / (1000 * 60 * 60 * 24));
|
|
2666
|
+
const isGoodsInTransit = daysSinceInvoice <= goodsInTransitWindowDays;
|
|
2667
|
+
|
|
2668
|
+
const disposition = isGoodsInTransit ? 'MISSING_IN_BOOKS_GOODS_IN_TRANSIT' : 'MISSING_IN_BOOKS';
|
|
2669
|
+
const itcStatus = isGoodsInTransit ? 'GOODS_IN_TRANSIT_PENDING_RECEIPT' : 'UNRECORDED_PURCHASE_IN_BOOKS';
|
|
2670
|
+
|
|
2671
|
+
const jurisdictionDecisions = {
|
|
2672
|
+
taxClassification: (record.cgstInr > 0 || record.sgstInr > 0) ? 'INTRASTATE_CORRECT' : 'INTERSTATE_CORRECT',
|
|
2673
|
+
recipientJurisdiction: recipientStateSet.has(record.posStateCode) ? 'FILING_GSTIN_MATCHES_POS' : 'UNREGISTERED_IN_POS_STATE',
|
|
2674
|
+
itcEligibility: isGoodsInTransit ? 'PROVISIONAL_PENDING_FILING' : 'INDETERMINATE',
|
|
2675
|
+
operationalRouting: isGoodsInTransit ? 'PHYSICAL_GOODS_RECEIPT_VERIFICATION_PENDING' : 'MANUAL_HUMAN_INVESTIGATION_REQUIRED'
|
|
2676
|
+
};
|
|
2677
|
+
|
|
2678
|
+
const statutorySection = isGoodsInTransit ? 'CGST Act Section 16(2)(b) (Receipt of Goods Condition)' : 'General Accounting Principles & GST Compliance';
|
|
2679
|
+
|
|
2680
|
+
items.push({
|
|
2681
|
+
disposition,
|
|
2682
|
+
itcStatus,
|
|
2683
|
+
statutorySection,
|
|
2684
|
+
vendorName: record.supplierName,
|
|
2685
|
+
vendorGstin: record.supplierGstin,
|
|
2686
|
+
billNumber: record.invoiceNumber,
|
|
2687
|
+
billDate: record.invoiceDate,
|
|
2688
|
+
booksTaxableInr: 0,
|
|
2689
|
+
booksTaxInr: 0,
|
|
2690
|
+
gstr2bTaxableInr: record.taxableAmountInr,
|
|
2691
|
+
gstr2bTaxInr: record.totalTaxInr,
|
|
2692
|
+
varianceTaxableInr: -record.taxableAmountInr,
|
|
2693
|
+
varianceTaxInr: -record.totalTaxInr,
|
|
2694
|
+
isRule37Overdue: false,
|
|
2695
|
+
daysElapsedSinceBill: 0,
|
|
2696
|
+
rule37Status: 'NOT_IN_BOOKS',
|
|
2697
|
+
rule37ReversalTaxInr: 0,
|
|
2698
|
+
rule37InterestInr: 0,
|
|
2699
|
+
rule37InterestExplanation: '',
|
|
2700
|
+
isReclaimableUponPayment: false,
|
|
2701
|
+
jurisdictionDecisions,
|
|
2702
|
+
statutoryAction: isGoodsInTransit
|
|
2703
|
+
? `Invoice reported in GSTR-2B dated ${record.invoiceDate} (within ${goodsInTransitWindowDays} days). Verify physical receipt of goods per Section 16(2)(b) before recording bill.`
|
|
2704
|
+
: 'Invoice reported in GSTR-2B by supplier but missing in Zoho Books. Review purchase orders or confirm if goods/services received.',
|
|
2705
|
+
auditEvidence: {
|
|
2706
|
+
inBooks: false,
|
|
2707
|
+
inGstr2b: true,
|
|
2708
|
+
docType: record.docType,
|
|
2709
|
+
posStateCode: record.posStateCode,
|
|
2710
|
+
isGoodsInTransit
|
|
2711
|
+
},
|
|
2712
|
+
evidenceProvenance: buildEvidenceProvenance({
|
|
2713
|
+
bill: null,
|
|
2714
|
+
record2b: record,
|
|
2715
|
+
matchType: 'UNMATCHED',
|
|
2716
|
+
disposition,
|
|
2717
|
+
statutorySection,
|
|
2718
|
+
jurisdictionDecisions,
|
|
2719
|
+
uncertaintyReason: null,
|
|
2720
|
+
asOfDate
|
|
2721
|
+
})
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// Cumulative Portfolio-Level Rule 88B(3) Audit across overlapping overdue bills
|
|
2727
|
+
let cumulativePortfolioEclAudit = null;
|
|
2728
|
+
if (Array.isArray(eclLedgerHistory) && eclLedgerHistory.length > 0) {
|
|
2729
|
+
const overdueBills = items.filter(i => i.isRule37Overdue);
|
|
2730
|
+
if (overdueBills.length > 0) {
|
|
2731
|
+
cumulativePortfolioEclAudit = calculatePortfolioEclUtilizationInterest({
|
|
2732
|
+
overdueBills,
|
|
2733
|
+
eclLedgerHistory,
|
|
2734
|
+
asOfDate
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
const summary = {
|
|
2740
|
+
totalBooksBillsCount: parsedBills.length,
|
|
2741
|
+
totalGstr2bRecordsCount: parsed2b.length,
|
|
2742
|
+
matchedEligibleCount,
|
|
2743
|
+
rcmUnregisteredEligibleCount,
|
|
2744
|
+
compositionExcludedCount,
|
|
2745
|
+
interveningRegularizedPaidCount,
|
|
2746
|
+
interveningRegularizedUnpaidCount,
|
|
2747
|
+
amountMismatchCount,
|
|
2748
|
+
blocked17_5Count,
|
|
2749
|
+
posMismatchCount,
|
|
2750
|
+
routableToBranchCount,
|
|
2751
|
+
strandedLocalCount,
|
|
2752
|
+
gstr2bPortalIneligibleCount,
|
|
2753
|
+
requiresHumanVerificationCount,
|
|
2754
|
+
missingIn2bCount,
|
|
2755
|
+
missingInBooksCount,
|
|
2756
|
+
rule37OverdueCount,
|
|
2757
|
+
|
|
2758
|
+
totalBooksTaxableInr: fromIntegerPaise(totalBooksTaxablePaise),
|
|
2759
|
+
totalBooksTaxInr: fromIntegerPaise(totalBooksTaxPaise),
|
|
2760
|
+
totalGstr2bTaxableInr: fromIntegerPaise(total2bTaxablePaise),
|
|
2761
|
+
totalGstr2bTaxInr: fromIntegerPaise(total2bTaxPaise),
|
|
2762
|
+
|
|
2763
|
+
eligibleItcInr: fromIntegerPaise(eligibleItcPaise),
|
|
2764
|
+
rcmUnregisteredEligibleItcInr: fromIntegerPaise(rcmUnregisteredEligibleItcPaise),
|
|
2765
|
+
compositionExcludedItcInr: fromIntegerPaise(compositionExcludedItcPaise),
|
|
2766
|
+
interveningRegularizedTaxInr: fromIntegerPaise(interveningRegularizedTaxPaise),
|
|
2767
|
+
atRiskUnfiledItcInr: fromIntegerPaise(atRiskUnfiledItcPaise),
|
|
2768
|
+
blocked17_5ItcInr: fromIntegerPaise(blocked17_5ItcPaise),
|
|
2769
|
+
posMismatchItcInr: fromIntegerPaise(posMismatchItcPaise),
|
|
2770
|
+
routableToBranchItcInr: fromIntegerPaise(routableToBranchItcPaise),
|
|
2771
|
+
strandedLocalItcInr: fromIntegerPaise(strandedLocalItcPaise),
|
|
2772
|
+
gstr2bPortalIneligibleItcInr: fromIntegerPaise(gstr2bPortalIneligibleItcPaise),
|
|
2773
|
+
requiresHumanVerificationItcInr: fromIntegerPaise(requiresHumanVerificationItcPaise),
|
|
2774
|
+
mismatchedItcInr: fromIntegerPaise(mismatchedItcPaise),
|
|
2775
|
+
missingInBooksTaxInr: fromIntegerPaise(missingInBooksTaxPaise),
|
|
2776
|
+
|
|
2777
|
+
rule37ReversalAmountInr: fromIntegerPaise(rule37ReversalAmountPaise),
|
|
2778
|
+
rule37InterestExposureInr: fromIntegerPaise(rule37InterestExposurePaise),
|
|
2779
|
+
paymentAppropriationMethod,
|
|
2780
|
+
paymentAppropriationLegalStatus: paymentAppropriationMethod === 'PROPORTIONATE' ? 'STATUTORY_MANDATE' : 'ACCOUNTING_ALLOCATION_ASSUMPTION',
|
|
2781
|
+
recipientTaxScheme: normalizedRecipientScheme,
|
|
2782
|
+
minimumEclBalanceInr: minimumEclBalanceInr !== undefined ? cleanNumericAmount(minimumEclBalanceInr) : null,
|
|
2783
|
+
eclUtilizationStatus,
|
|
2784
|
+
eclLedgerHistoryAudit: (Array.isArray(eclLedgerHistory) && eclLedgerHistory.length > 0),
|
|
2785
|
+
cumulativePortfolioEclAudit,
|
|
2786
|
+
|
|
2787
|
+
reconciliationStatus: (missingIn2bCount === 0 && amountMismatchCount === 0 && missingInBooksCount === 0 && posMismatchCount === 0 && blocked17_5Count === 0 && requiresHumanVerificationCount === 0) ? 'FULLY_RECONCILED' : 'EXCEPTIONS_IDENTIFIED',
|
|
2788
|
+
isAuthorizedToDisburse: false
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2791
|
+
return {
|
|
2792
|
+
status: 'SUCCESS',
|
|
2793
|
+
auditDate: asOfDate,
|
|
2794
|
+
summary,
|
|
2795
|
+
reconciliationItems: items
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* Generates an executive Markdown audit exception memorandum for the AP & Tax team.
|
|
2801
|
+
* Provides transparent, reproducible evidence for every transaction.
|
|
2802
|
+
*
|
|
2803
|
+
* @param {Object} reconResult - Output of reconcileZohoGstr2b
|
|
2804
|
+
* @returns {string} Markdown memorandum
|
|
2805
|
+
*/
|
|
2806
|
+
export function generateGstr2bAuditMarkdownReport(reconResult) {
|
|
2807
|
+
if (!reconResult || reconResult.status !== 'SUCCESS') {
|
|
2808
|
+
throw new Error('[@mudraforge/statutory] Invalid reconciliation result passed to generateGstr2bAuditMarkdownReport.');
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const s = reconResult.summary;
|
|
2812
|
+
const fmt = n => 'INR ' + Number(n || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
2813
|
+
|
|
2814
|
+
let md = '# ZOHO BOOKS & GSTR-2B AP RECONCILIATION AUDIT MEMORANDUM\n\n';
|
|
2815
|
+
md += `**Execution Date**: \`${reconResult.auditDate}\` \n`;
|
|
2816
|
+
md += `**Statutory Mandate**: CGST Act Section 16(2)(aa), Section 17(5), & Rule 37 read with Section 50 & Rule 88B(3) \n`;
|
|
2817
|
+
md += `**Payment Appropriation Policy**: \`${s.paymentAppropriationMethod}\` \n`;
|
|
2818
|
+
if (s.minimumEclBalanceInr !== null) {
|
|
2819
|
+
md += `**Rule 88B(3) Minimum ECL Balance**: \`${fmt(s.minimumEclBalanceInr)}\` \n`;
|
|
2820
|
+
}
|
|
2821
|
+
md += `**Reconciliation Disposition**: **\`${s.reconciliationStatus}\`** \n\n`;
|
|
2822
|
+
|
|
2823
|
+
md += '## 1. Executive Summary & ITC Health Matrix\n\n';
|
|
2824
|
+
md += '| Metric | Books (Zoho) | GSTR-2B (GSTN) | Variance / Risk |\n';
|
|
2825
|
+
md += '| :--- | :--- | :--- | :--- |\n';
|
|
2826
|
+
md += `| **Total Vouchers Scanned** | \`${s.totalBooksBillsCount}\` bills | \`${s.totalGstr2bRecordsCount}\` records | \`${s.missingIn2bCount + s.missingInBooksCount}\` unmatched |\n`;
|
|
2827
|
+
md += `| **Taxable Goods & Services Base** | ${fmt(s.totalBooksTaxableInr)} | ${fmt(s.totalGstr2bTaxableInr)} | ${fmt(s.totalBooksTaxableInr - s.totalGstr2bTaxableInr)} |\n`;
|
|
2828
|
+
md += `| **Input Tax Credit (ITC) Total** | ${fmt(s.totalBooksTaxInr)} | ${fmt(s.totalGstr2bTaxInr)} | ${fmt(s.totalBooksTaxInr - s.totalGstr2bTaxInr)} |\n`;
|
|
2829
|
+
md += `| **100% Eligible ITC (Table 4(A)(5))** | — | — | **${fmt(s.eligibleItcInr)}** |\n`;
|
|
2830
|
+
if (s.rcmUnregisteredEligibleCount > 0) {
|
|
2831
|
+
md += `| **Eligible Inward RCM Supplies (Table 4(A)(3))** | — | — | **${fmt(s.rcmUnregisteredEligibleItcInr)}** (\`${s.rcmUnregisteredEligibleCount}\` vouchers) |\n`;
|
|
2832
|
+
}
|
|
2833
|
+
if (s.compositionExcludedCount > 0) {
|
|
2834
|
+
md += `| **Commercial Rent Excluded u/s 10 (Not RCM)** | — | — | **${fmt(s.compositionExcludedItcInr)}** (\`${s.compositionExcludedCount}\` vouchers expensed) |\n`;
|
|
2835
|
+
}
|
|
2836
|
+
if (s.interveningRegularizedPaidCount > 0 || s.interveningRegularizedUnpaidCount > 0) {
|
|
2837
|
+
md += `| **Intervening Rent Regularized (As-Is-Where-Is)** | — | — | **${fmt(s.interveningRegularizedTaxInr)}** (\`${s.interveningRegularizedPaidCount}\` paid / \`${s.interveningRegularizedUnpaidCount}\` unpaid regularized) |\n`;
|
|
2838
|
+
}
|
|
2839
|
+
md += `| **Requires Human Verification (Uncertainty)** | — | — | **${fmt(s.requiresHumanVerificationItcInr)}** (\`${s.requiresHumanVerificationCount}\` items) |\n`;
|
|
2840
|
+
md += `| **Provisional / At-Risk ITC (Missing in 2B)** | — | — | **${fmt(s.atRiskUnfiledItcInr)}** (Withhold) |\n`;
|
|
2841
|
+
md += `| **Blocked Ineligible ITC (Sec 17(5))** | — | — | **${fmt(s.blocked17_5ItcInr)}** (Table 4(B)(1)) |\n`;
|
|
2842
|
+
md += `| **Routable to Other Branch GSTINs** | — | — | **${fmt(s.routableToBranchItcInr)}** (Port to Branch) |\n`;
|
|
2843
|
+
md += `| **Stranded Local State Levies (Sec 12(3))** | — | — | **${fmt(s.strandedLocalItcInr)}** (Absorb as Expense) |\n`;
|
|
2844
|
+
md += `| **Place of Supply (POS) Defective Invoices** | — | — | **${fmt(s.posMismatchItcInr)}** (Vendor Credit Note Req) |\n`;
|
|
2845
|
+
md += `| **Rule 37 Overdue Reversal (>180 Days)** | — | — | **${fmt(s.rule37ReversalAmountInr)}** (Table 4(B)(2)) |\n`;
|
|
2846
|
+
md += `| **Rule 37 Sec 50(1) Delayed Interest Exposure** | — | — | **${fmt(s.rule37InterestExposureInr)}** |\n\n`;
|
|
2847
|
+
|
|
2848
|
+
if (s.cumulativePortfolioEclAudit) {
|
|
2849
|
+
const port = s.cumulativePortfolioEclAudit;
|
|
2850
|
+
md += `### Cumulative Portfolio-Level Rule 88B(3) ECL Utilization\n\n`;
|
|
2851
|
+
md += `- **Active Overdue Bills Pooled**: \`${reconResult.reconciliationItems.filter(i => i.isRule37Overdue).length}\`\n`;
|
|
2852
|
+
md += `- **Total Rule 88B(3) Interest Liability**: **${fmt(port.totalInterestInr)}**\n`;
|
|
2853
|
+
md += `- **Utilized Shortfall Days**: \`${port.utilizedDaysCount}\` days (Zero Interest Days: \`${port.zeroUtilizationDaysCount}\`)\n`;
|
|
2854
|
+
md += `- **Peak Portfolio Shortfall**: ${fmt(port.maxPortfolioUtilizationInr)}\n`;
|
|
2855
|
+
md += `- **Audit Finding**: ${port.explanation}\n\n`;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
const exceptions = reconResult.reconciliationItems.filter(i => (
|
|
2859
|
+
i.disposition !== 'MATCHED_ELIGIBLE' &&
|
|
2860
|
+
i.disposition !== 'RCM_UNREGISTERED_SUPPLIER_ELIGIBLE' &&
|
|
2861
|
+
i.disposition !== 'COMPOSITION_EXCLUDED_RCM_SUPPLY' &&
|
|
2862
|
+
i.disposition !== 'RCM_COMPOSITION_INTERVENING_REGULARIZED_PAID' &&
|
|
2863
|
+
i.disposition !== 'RCM_COMPOSITION_INTERVENING_REGULARIZED_UNPAID'
|
|
2864
|
+
));
|
|
2865
|
+
|
|
2866
|
+
if (exceptions.length === 0) {
|
|
2867
|
+
md += '## 2. Statutory Audit Verdict\n\n';
|
|
2868
|
+
md += '> **100% CLEAN AP AUDIT**: Every purchase bill recorded in Zoho Books is corroborated by an eligible GSTR-2B return filed by registered suppliers with zero variance, zero blocked credit exposure, zero Rule 37 180-day overdue liabilities, and zero data ambiguities.\n';
|
|
2869
|
+
return md;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
md += `## 2. Actionable AP Exceptions Register (${exceptions.length} Items)\n\n`;
|
|
2873
|
+
md += '| Vendor GSTIN | Vendor Name | Bill No | Bill Date | Books ITC | GSTR-2B ITC | Disposition | Reason / Statutory Action Required |\n';
|
|
2874
|
+
md += '| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |\n';
|
|
2875
|
+
|
|
2876
|
+
for (const ex of exceptions) {
|
|
2877
|
+
const reasonText = ex.uncertaintyReason ? `[${ex.uncertaintyReason}] ` : '';
|
|
2878
|
+
md += `| \`${ex.vendorGstin || 'N/A'}\` | ${ex.vendorName} | \`${ex.billNumber}\` | ${ex.billDate || 'N/A'} | ${fmt(ex.booksTaxInr)} | ${fmt(ex.gstr2bTaxInr)} | **\`${ex.disposition}\`** | ${reasonText}${ex.statutoryAction} |\n`;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
md += '\n## 3. Statutory Assurance & Governance Invariants\n\n';
|
|
2882
|
+
md += '- **Zero Money Movement Authorization**: `isAuthorizedToDisburse: false`. Statutory reconciliation assesses compliance but never authorizes disbursement.\n';
|
|
2883
|
+
md += '- **Rule 37 Proportional Reversal Mandate**: Computed in accordance with CGST Act Section 16(2) second proviso read with Rule 37(1).\n';
|
|
2884
|
+
md += '- **Rule 88B(3) Electronic Credit Ledger Benchmark**: Interest attaches strictly to utilized credit; unutilized balances incur 0.00% penal interest.\n';
|
|
2885
|
+
md += '- **Epistemic Integrity Standard**: Incomplete records or unverified identifiers fail open to human inspection rather than manufactured certainty.\n\n';
|
|
2886
|
+
md += '---\n*Report generated deterministically by MudraForge Statutory Engine under OKF v0.2 substrate.*\n';
|
|
2887
|
+
return md;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// ── Internal Helpers ────────────────────────────────────────────────────────
|
|
2891
|
+
|
|
2892
|
+
function parseCsvRow(rowText, delimiter = ',') {
|
|
2893
|
+
const result = [];
|
|
2894
|
+
let insideQuotes = false;
|
|
2895
|
+
let current = '';
|
|
2896
|
+
|
|
2897
|
+
for (let i = 0; i < rowText.length; i++) {
|
|
2898
|
+
const char = rowText[i];
|
|
2899
|
+
if (char === '"') {
|
|
2900
|
+
if (insideQuotes && rowText[i + 1] === '"') {
|
|
2901
|
+
current += '"';
|
|
2902
|
+
i++;
|
|
2903
|
+
} else {
|
|
2904
|
+
insideQuotes = !insideQuotes;
|
|
2905
|
+
}
|
|
2906
|
+
} else if (char === delimiter && !insideQuotes) {
|
|
2907
|
+
result.push(current.trim());
|
|
2908
|
+
current = '';
|
|
2909
|
+
} else {
|
|
2910
|
+
current += char;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
result.push(current.trim());
|
|
2914
|
+
return result;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
function findColIndex(headers, candidateNames) {
|
|
2918
|
+
for (const name of candidateNames) {
|
|
2919
|
+
const idx = headers.indexOf(name);
|
|
2920
|
+
if (idx !== -1) return idx;
|
|
2921
|
+
}
|
|
2922
|
+
for (const name of candidateNames) {
|
|
2923
|
+
if (name.length <= 3) continue;
|
|
2924
|
+
const idx = headers.findIndex(h => h.includes(name));
|
|
2925
|
+
if (idx !== -1) return idx;
|
|
2926
|
+
}
|
|
2927
|
+
return -1;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
function normalizeIsoDate(raw) {
|
|
2931
|
+
if (!raw) return null;
|
|
2932
|
+
const str = String(raw).trim();
|
|
2933
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str;
|
|
2934
|
+
const parts = str.split(/[-/]/);
|
|
2935
|
+
if (parts.length === 3) {
|
|
2936
|
+
if (parts[0].length === 2 && parts[2].length === 4) {
|
|
2937
|
+
return `${parts[2]}-${parts[1].padStart(2, '0')}-${parts[0].padStart(2, '0')}`;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return null;
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
function parseGstPortalDate(raw) {
|
|
2944
|
+
if (!raw) return null;
|
|
2945
|
+
const str = String(raw).trim();
|
|
2946
|
+
const parts = str.split(/[-/]/);
|
|
2947
|
+
if (parts.length === 3) {
|
|
2948
|
+
if (parts[0].length === 2 && parts[2].length === 4) {
|
|
2949
|
+
return `${parts[2]}-${parts[1].padStart(2, '0')}-${parts[0].padStart(2, '0')}`;
|
|
2950
|
+
}
|
|
2951
|
+
if (parts[0].length === 4 && parts[2].length === 2) {
|
|
2952
|
+
return `${parts[0]}-${parts[1].padStart(2, '0')}-${parts[2].padStart(2, '0')}`;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return str;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
function calculateDaysElapsed(fromDateStr, toDateStr) {
|
|
2959
|
+
if (!fromDateStr || !toDateStr) return 0;
|
|
2960
|
+
const fromMs = new Date(fromDateStr).getTime();
|
|
2961
|
+
const toMs = new Date(toDateStr).getTime();
|
|
2962
|
+
if (isNaN(fromMs) || isNaN(toMs)) return 0;
|
|
2963
|
+
return Math.max(0, Math.floor((toMs - fromMs) / (1000 * 60 * 60 * 24)));
|
|
2964
|
+
}
|