@trajectonomics/tax 0.0.0-stage → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1480 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_clamp = require("../clamp-BKG7kjVf.cjs");
3
+ // src/th/parameters-brackets.ts
4
+ /**
5
+ * Section 48(1) progressive net-income brackets for Tax Year 2026 (THB major units, rates as ratios).
6
+ *
7
+ * @statute Thailand Revenue Code Section 48(1); Royal Decree No. 650.
8
+ */
9
+ const TH_2026_BRACKETS = [
10
+ {
11
+ upTo: 15e4,
12
+ rate: 0
13
+ },
14
+ {
15
+ upTo: 3e5,
16
+ rate: .05
17
+ },
18
+ {
19
+ upTo: 5e5,
20
+ rate: .1
21
+ },
22
+ {
23
+ upTo: 75e4,
24
+ rate: .15
25
+ },
26
+ {
27
+ upTo: 1e6,
28
+ rate: .2
29
+ },
30
+ {
31
+ upTo: 2e6,
32
+ rate: .25
33
+ },
34
+ {
35
+ upTo: 5e6,
36
+ rate: .3
37
+ },
38
+ {
39
+ upTo: null,
40
+ rate: .35
41
+ }
42
+ ];
43
+ // src/th/parameters-special.ts
44
+ /**
45
+ * Section 40(4) final withholding rates for Tax Year 2026.
46
+ *
47
+ * @statute Thailand Revenue Code Sections 40(4), 50(2)(g), 57 ter.
48
+ */
49
+ const TH_2026_FINAL_WITHHOLDING = {
50
+ interestRate: .15,
51
+ dividendRate: .1,
52
+ digitalAssetRate: .15
53
+ };
54
+ /**
55
+ * Section 48(5) separate sheet parameters for Tax Year 2026: exemption, expenses, and schedule.
56
+ * The schedule is the Section 48(1) ladder without its 0% tier (same upper bounds and rates).
57
+ *
58
+ * @statute Thailand Revenue Code Section 48(5), Section 42(1).
59
+ */
60
+ const TH_2026_SEPARATE_SHEET = {
61
+ minYearsOfService: 5,
62
+ exemptionWageDays: 400,
63
+ exemptionCeiling: 6e5,
64
+ expensePerYearOfService: 7e3,
65
+ remainderExpenseRate: .5,
66
+ brackets: TH_2026_BRACKETS.filter((bracket) => bracket.rate > 0)
67
+ };
68
+ // src/th/parameters.ts
69
+ /**
70
+ * Thailand Statutory Tax Parameters for Tax Year 2026 (Revenue Code ภ.ง.ด.90/91).
71
+ *
72
+ * @statute Thailand Revenue Code Sections 40, 42 bis, 47, 47 bis, 48(1)–(5); Royal Decrees No. 11, 650.
73
+ */
74
+ const TH_2026_PARAMETERS = {
75
+ taxYear: 2026,
76
+ currency: "THB",
77
+ expenses: {
78
+ section40_1_2: {
79
+ rate: .5,
80
+ ceiling: 1e5
81
+ },
82
+ section40_3: {
83
+ rate: .5,
84
+ ceiling: 1e5
85
+ },
86
+ section40_4: { rate: 0 },
87
+ section40_5: {
88
+ building: .3,
89
+ agriculturalLand: .2,
90
+ nonAgriculturalLand: .15,
91
+ vehicles: .3,
92
+ other: .1
93
+ },
94
+ section40_6: {
95
+ medical: .6,
96
+ other: .3
97
+ },
98
+ section40_7: { rate: .6 },
99
+ section40_8: {
100
+ generalRate: .6,
101
+ entertainerFirstTierRate: .6,
102
+ entertainerFirstTierThreshold: 3e5,
103
+ entertainerSecondTierRate: .4,
104
+ entertainerCeiling: 6e5
105
+ }
106
+ },
107
+ allowances: {
108
+ personal: 6e4,
109
+ spouse: 6e4,
110
+ child: {
111
+ basePerChild: 3e4,
112
+ bonusSecondPlusBorn2018OrLater: 3e4,
113
+ bonusCutoffYear: 2018
114
+ },
115
+ parent: {
116
+ perPerson: 3e4,
117
+ maxCount: 4
118
+ },
119
+ disabledPerPerson: 6e4,
120
+ socialSecurityFundCeiling: 10500,
121
+ lifeInsurance: {
122
+ combinedCeiling: 1e5,
123
+ healthCeiling: 25e3
124
+ },
125
+ parentHealthInsuranceCeiling: 15e3,
126
+ spouseLifeInsuranceCeiling: 1e4,
127
+ mortgageInterestCeiling: 1e5,
128
+ retirementCluster: {
129
+ combinedCeiling: 5e5,
130
+ rmf: {
131
+ maxRate: .3,
132
+ ceiling: 5e5
133
+ },
134
+ ssf: {
135
+ maxRate: .3,
136
+ ceiling: 2e5
137
+ },
138
+ pvd: {
139
+ maxRate: .15,
140
+ ceiling: 5e5
141
+ },
142
+ gpf: {
143
+ maxRate: .3,
144
+ ceiling: 5e5
145
+ },
146
+ nsf: { ceiling: 3e4 },
147
+ pensionLifeInsurance: {
148
+ maxRate: .15,
149
+ ceiling: 2e5
150
+ }
151
+ },
152
+ thaiEsg: {
153
+ maxRate: .3,
154
+ ceiling: 3e5
155
+ },
156
+ donations: {
157
+ donation2xMultiplier: 2,
158
+ donation2xMaxRate: .1,
159
+ donation1xMaxRate: .1
160
+ }
161
+ },
162
+ brackets: TH_2026_BRACKETS,
163
+ minimumGrossTax: {
164
+ rate: .005,
165
+ incomeThreshold: 12e4,
166
+ taxThreshold: 5e3
167
+ },
168
+ finalWithholding: TH_2026_FINAL_WITHHOLDING,
169
+ separateSheet: TH_2026_SEPARATE_SHEET,
170
+ realEstate: {
171
+ expenseTable: [
172
+ {
173
+ minYears: 1,
174
+ rate: .92
175
+ },
176
+ {
177
+ minYears: 2,
178
+ rate: .84
179
+ },
180
+ {
181
+ minYears: 3,
182
+ rate: .77
183
+ },
184
+ {
185
+ minYears: 4,
186
+ rate: .71
187
+ },
188
+ {
189
+ minYears: 5,
190
+ rate: .65
191
+ },
192
+ {
193
+ minYears: 6,
194
+ rate: .6
195
+ },
196
+ {
197
+ minYears: 7,
198
+ rate: .55
199
+ },
200
+ {
201
+ minYears: 8,
202
+ rate: .5
203
+ }
204
+ ],
205
+ inheritanceExpenseRate: .5,
206
+ maxHoldingYears: 10,
207
+ brackets: TH_2026_SEPARATE_SHEET.brackets,
208
+ taxCapRate: .2
209
+ }
210
+ };
211
+ // src/th/types/special-input.ts
212
+ /**
213
+ * Election values for a Section 40(4) source: keep source withholding as final tax, or include
214
+ * the income in the regular progressive computation. Plain `"final"`/`"include"` strings are
215
+ * accepted wherever {@link ThFinalWithholdingElection} is expected.
216
+ *
217
+ * @statute Thailand Revenue Code Sections 40(4), 50(2)(g), 57 ter.
218
+ */
219
+ const ThFinalWithholdingElection = {
220
+ Final: "final",
221
+ Include: "include"
222
+ };
223
+ /**
224
+ * Kind of Section 48(5) lump sum. Only labour-law `"severance"` carries the 400-day / 600,000 THB
225
+ * exemption; other `"retirement"` payments (e.g. a pension fund lump sum) get none.
226
+ * Plain `"severance"`/`"retirement"` strings are accepted wherever
227
+ * {@link ThSeparateSheetPaymentType} is expected.
228
+ *
229
+ * @statute Thailand Revenue Code Section 48(5); Section 42(1) (severance exemption).
230
+ */
231
+ const ThSeparateSheetPaymentType = {
232
+ Severance: "severance",
233
+ Retirement: "retirement"
234
+ };
235
+ /**
236
+ * How a Section 48(4) property was acquired. Inherited or gifted property takes a flat 50%
237
+ * expense; commercially acquired (or other) property takes the holding-year table. The two are
238
+ * never combined. Plain `"commercial"`/`"inheritance_or_gift"` strings are accepted wherever
239
+ * {@link ThRealEstateAcquisitionType} is expected.
240
+ *
241
+ * @statute Thailand Revenue Code Section 48(4); Ministerial Regulation No. 126.
242
+ */
243
+ const ThRealEstateAcquisitionType = {
244
+ Commercial: "commercial",
245
+ InheritanceOrGift: "inheritance_or_gift"
246
+ };
247
+ /**
248
+ * Which expense deduction a Section 48(4) computation used, reported in the breakdown.
249
+ *
250
+ * @statute Thailand Revenue Code Section 48(4); Ministerial Regulation No. 126.
251
+ */
252
+ const ThRealEstateExpenseBasis = {
253
+ HoldingPeriodTable: "holding_period_table",
254
+ FlatInheritance: "flat_inheritance"
255
+ };
256
+ // src/th/units.ts
257
+ /**
258
+ * Converts a THB major-unit amount to integer satang.
259
+ *
260
+ * Purpose: Single THB-bound entry point so stages never repeat the currency threading.
261
+ * Math: Decimal-exact conversion via `toMinorUnits` (half away from zero beyond 2 decimals).
262
+ * Units: THB major units in, integer satang out.
263
+ * Throws: TaxValidationError if the amount is negative (unless allowed), non-finite, or unsafe.
264
+ *
265
+ * @param amount Amount in THB.
266
+ * @param field Diagnostic field name reported on failure.
267
+ * @statute Thailand Revenue Code Section 40 (amounts expressed in baht).
268
+ */
269
+ function toSatang(amount, field) {
270
+ return require_clamp.toMinorUnits(amount, TH_2026_PARAMETERS.currency, field);
271
+ }
272
+ /**
273
+ * Converts integer satang back to THB major units.
274
+ *
275
+ * Purpose: Counterpart of {@link toSatang}.
276
+ * Math: `satang / 100`, fixed to two decimals.
277
+ * Units: Integer satang in, THB major units out.
278
+ * Throws: TaxValidationError if the input is not a safe integer (or is negative without
279
+ * `allowNegative`).
280
+ *
281
+ * @param satang Integer satang.
282
+ * @param field Diagnostic field name reported on failure.
283
+ * @param options Set `allowNegative` for refund balances.
284
+ * @statute Thailand Revenue Code Section 40 (amounts expressed in baht).
285
+ */
286
+ function fromSatang(satang, field, options) {
287
+ return require_clamp.fromMinorUnits(satang, TH_2026_PARAMETERS.currency, field, options);
288
+ }
289
+ /**
290
+ * Sums non-negative THB amounts exactly in integer satang.
291
+ *
292
+ * Purpose: Avoids binary float drift when aggregating gross amounts.
293
+ * Math: Converts each amount to satang, adds, converts back to major units.
294
+ * Units: THB major units in and out.
295
+ * Throws: TaxValidationError if any amount is negative or non-finite, or the sum is not a safe integer.
296
+ *
297
+ * @param values Amounts in THB.
298
+ * @param field Diagnostic field name reported on failure.
299
+ * @statute Thailand Revenue Code Section 40 (gross income aggregation).
300
+ */
301
+ function sumThAmounts(values, field) {
302
+ return fromSatang(values.reduce((sum, value) => sum + toSatang(value, field), 0), field);
303
+ }
304
+ // src/th/stages/allowances-donations.ts
305
+ /**
306
+ * Calculates qualified 2x and general 1x donation allowances.
307
+ *
308
+ * Math: 2x donations capped at 10% of net before donations; 1x donations capped at 10% of remaining net.
309
+ * Units: THB major units.
310
+ *
311
+ * @statute Thailand Revenue Code Section 47(7); Royal Decrees No. 428, 558.
312
+ */
313
+ function calculateDonationAllowances(donation2x, donation1x, netBeforeDonations) {
314
+ const p = TH_2026_PARAMETERS.allowances.donations;
315
+ const net = Math.max(0, netBeforeDonations);
316
+ const raw2x = donation2x ?? 0;
317
+ require_clamp.validateMonetaryAmount(raw2x, "allowance.donation_2x");
318
+ const eligible2x = raw2x * p.donation2xMultiplier;
319
+ const cap2x = Math.floor(net * p.donation2xMaxRate);
320
+ const allowed2x = Math.min(eligible2x, cap2x);
321
+ const clamped2x = {
322
+ requested: raw2x,
323
+ allowed: allowed2x,
324
+ cap: cap2x,
325
+ wasClamped: eligible2x > cap2x
326
+ };
327
+ const remainingNet = Math.max(0, net - allowed2x);
328
+ const raw1x = donation1x ?? 0;
329
+ require_clamp.validateMonetaryAmount(raw1x, "allowance.donation_1x");
330
+ const cap1x = Math.floor(remainingNet * p.donation1xMaxRate);
331
+ const allowed1x = Math.min(raw1x, cap1x);
332
+ const breakdown = {
333
+ donation2x: clamped2x,
334
+ donation1x: {
335
+ requested: raw1x,
336
+ allowed: allowed1x,
337
+ cap: cap1x,
338
+ wasClamped: raw1x > cap1x
339
+ },
340
+ total: allowed2x + allowed1x
341
+ };
342
+ const netTaxableIncome = Math.max(0, remainingNet - allowed1x);
343
+ return {
344
+ breakdown,
345
+ allowedDonationsTotal: breakdown.total,
346
+ netTaxableIncome
347
+ };
348
+ }
349
+ // src/th/stages/allowances-family.ts
350
+ const unclamped = (n) => ({
351
+ requested: n,
352
+ allowed: n,
353
+ cap: n,
354
+ wasClamped: false
355
+ });
356
+ function countBonusChildren(children, cutoff) {
357
+ const sorted = [...children].sort((a, b) => (a.birthYear ?? 0) - (b.birthYear ?? 0));
358
+ let bonus = 0;
359
+ for (let i = 1; i < sorted.length; i++) if ((sorted[i]?.birthYear ?? 0) >= cutoff) bonus++;
360
+ return bonus;
361
+ }
362
+ function computeChildAllowance(children) {
363
+ if (!children) return unclamped(0);
364
+ const p = TH_2026_PARAMETERS.allowances.child;
365
+ if (Array.isArray(children)) {
366
+ const bonus = countBonusChildren(children, p.bonusCutoffYear);
367
+ return unclamped(children.length * p.basePerChild + bonus * p.bonusSecondPlusBorn2018OrLater);
368
+ }
369
+ const counts = children;
370
+ require_clamp.validateMonetaryAmount(counts.totalCount ?? 0, "allowance.children");
371
+ const count = Math.max(0, Math.floor(counts.totalCount ?? 0));
372
+ const bonusReq = Math.max(0, Math.floor(counts.secondPlusBorn2018OrLaterCount ?? 0));
373
+ const bonus = Math.min(bonusReq, Math.max(0, count - 1));
374
+ return unclamped(count * p.basePerChild + bonus * p.bonusSecondPlusBorn2018OrLater);
375
+ }
376
+ function computeParentCount(parents, status) {
377
+ if (parents === void 0) return 0;
378
+ if (typeof parents === "number") {
379
+ require_clamp.validateMonetaryAmount(parents, "allowance.parents");
380
+ return Math.floor(parents);
381
+ }
382
+ let count = (parents.taxpayerFather ? 1 : 0) + (parents.taxpayerMother ? 1 : 0);
383
+ if (status === "married_no_income_spouse") count += (parents.spouseFather ? 1 : 0) + (parents.spouseMother ? 1 : 0);
384
+ return count;
385
+ }
386
+ /**
387
+ * Calculates family allowances (personal, spouse, children, parents, disabled).
388
+ *
389
+ * @statute Thailand Revenue Code Section 47(1)(a)–(f).
390
+ */
391
+ function calculateFamilyAllowances(input = {}, filingStatus = "single") {
392
+ const p = TH_2026_PARAMETERS.allowances;
393
+ const personalReq = input.personal ?? p.personal;
394
+ const personal = require_clamp.clampCap(personalReq, p.personal, "allowance.personal");
395
+ const isSpouseEligible = filingStatus === "married_no_income_spouse" || filingStatus === "married_joint";
396
+ const spouseRequested = input.spouse ?? (isSpouseEligible ? p.spouse : 0);
397
+ const spouse = isSpouseEligible ? require_clamp.clampCap(spouseRequested, p.spouse, "allowance.spouse") : {
398
+ requested: spouseRequested,
399
+ allowed: 0,
400
+ cap: 0,
401
+ wasClamped: spouseRequested > 0
402
+ };
403
+ const children = computeChildAllowance(input.children);
404
+ const parentsRequested = computeParentCount(input.parents, filingStatus) * p.parent.perPerson;
405
+ const parents = require_clamp.clampCap(parentsRequested, p.parent.perPerson * p.parent.maxCount, "allowance.parents");
406
+ const disabledRequested = (input.disabledCareCount ?? 0) * p.disabledPerPerson;
407
+ require_clamp.validateMonetaryAmount(disabledRequested, "allowance.disabled");
408
+ return {
409
+ personal,
410
+ spouse,
411
+ children,
412
+ parents,
413
+ disabled: unclamped(disabledRequested)
414
+ };
415
+ }
416
+ // src/th/stages/allowances-insurance.ts
417
+ /**
418
+ * Calculates insurance, social security, and housing mortgage interest allowances.
419
+ *
420
+ * @statute Thailand Revenue Code Section 47(1)(d), (g), (h), (j).
421
+ */
422
+ function calculateInsuranceAllowances(input = {}, filingStatus = "single") {
423
+ const p = TH_2026_PARAMETERS.allowances;
424
+ const socialSecurity = require_clamp.clampCap(input.socialSecurity ?? 0, p.socialSecurityFundCeiling, "allowance.social_security");
425
+ const healthClamped = require_clamp.clampCap(input.healthInsurance ?? 0, p.lifeInsurance.healthCeiling, "allowance.health_insurance");
426
+ const rawLife = input.lifeInsurance ?? 0;
427
+ if (input.lifeInsurance !== void 0) require_clamp.validateMonetaryAmount(rawLife, "allowance.life_health_combined");
428
+ const lifeCombinedRequested = rawLife + (input.healthInsurance ?? 0);
429
+ const lifeEffectiveRequested = rawLife + healthClamped.allowed;
430
+ const lifeCombined = require_clamp.clampCap(lifeEffectiveRequested, p.lifeInsurance.combinedCeiling, "allowance.life_health_combined");
431
+ const lifeInsurance = {
432
+ requested: lifeCombinedRequested,
433
+ allowed: lifeCombined.allowed,
434
+ cap: p.lifeInsurance.combinedCeiling,
435
+ wasClamped: lifeCombinedRequested > lifeCombined.allowed
436
+ };
437
+ const parentHealthInsurance = require_clamp.clampCap(input.parentHealthInsurance ?? 0, p.parentHealthInsuranceCeiling, "allowance.parent_health_insurance");
438
+ const isSpouseEligible = filingStatus === "married_no_income_spouse";
439
+ const spouseLifeReq = input.spouseLifeInsurance ?? 0;
440
+ return {
441
+ socialSecurity,
442
+ lifeInsurance,
443
+ parentHealthInsurance,
444
+ spouseLifeInsurance: isSpouseEligible ? require_clamp.clampCap(spouseLifeReq, p.spouseLifeInsuranceCeiling, "allowance.spouse_life_insurance") : {
445
+ requested: spouseLifeReq,
446
+ allowed: 0,
447
+ cap: 0,
448
+ wasClamped: spouseLifeReq > 0
449
+ },
450
+ mortgageInterest: require_clamp.clampCap(input.mortgageInterest ?? 0, p.mortgageInterestCeiling, "allowance.mortgage_interest")
451
+ };
452
+ }
453
+ // src/th/stages/allowances-retirement.ts
454
+ /**
455
+ * Calculates Retirement Cluster and ThaiESG allowances.
456
+ *
457
+ * @statute Thailand Revenue Code Section 47; Ministerial Regulations No. 126, 390.
458
+ */
459
+ function calculateRetirementAllowances(input = {}, assessableIncome, wageBase) {
460
+ const p = TH_2026_PARAMETERS.allowances.retirementCluster;
461
+ const currency = TH_2026_PARAMETERS.currency;
462
+ const wage = Math.max(wageBase, 0);
463
+ const rmf = require_clamp.clampCap(input.rmf ?? 0, {
464
+ base: assessableIncome,
465
+ rate: p.rmf.maxRate,
466
+ ceiling: p.rmf.ceiling,
467
+ currency
468
+ }, "allowance.retirement.rmf");
469
+ const ssf = require_clamp.clampCap(input.ssf ?? 0, {
470
+ base: assessableIncome,
471
+ rate: p.ssf.maxRate,
472
+ ceiling: p.ssf.ceiling,
473
+ currency
474
+ }, "allowance.retirement.ssf");
475
+ const pvd = require_clamp.clampCap(input.pvd ?? 0, {
476
+ base: wage,
477
+ rate: p.pvd.maxRate,
478
+ ceiling: p.pvd.ceiling,
479
+ currency
480
+ }, "allowance.retirement.pvd");
481
+ const gpf = require_clamp.clampCap(input.gpf ?? 0, {
482
+ base: wage,
483
+ rate: p.gpf.maxRate,
484
+ ceiling: p.gpf.ceiling,
485
+ currency
486
+ }, "allowance.retirement.gpf");
487
+ const nsf = require_clamp.clampCap(input.nsf ?? 0, p.nsf.ceiling, "allowance.retirement.nsf");
488
+ const pensionLifeInsurance = require_clamp.clampCap(input.pensionLifeInsurance ?? 0, {
489
+ base: assessableIncome,
490
+ rate: p.pensionLifeInsurance.maxRate,
491
+ ceiling: p.pensionLifeInsurance.ceiling,
492
+ currency
493
+ }, "allowance.retirement.pension_life");
494
+ const individualSum = rmf.allowed + ssf.allowed + pvd.allowed + gpf.allowed + nsf.allowed + pensionLifeInsurance.allowed;
495
+ const combined = require_clamp.clampCap(individualSum, p.combinedCeiling, "allowance.retirement_cluster");
496
+ const retirementCluster = {
497
+ rmf,
498
+ ssf,
499
+ pvd,
500
+ gpf,
501
+ nsf,
502
+ pensionLifeInsurance,
503
+ combined
504
+ };
505
+ const thaiEsgParam = TH_2026_PARAMETERS.allowances.thaiEsg;
506
+ const thaiEsg = require_clamp.clampCap(input.thaiEsg ?? 0, {
507
+ base: assessableIncome,
508
+ rate: thaiEsgParam.maxRate,
509
+ ceiling: thaiEsgParam.ceiling,
510
+ currency
511
+ }, "allowance.thai_esg");
512
+ return {
513
+ retirementCluster,
514
+ thaiEsg,
515
+ allowedClusterTotal: combined.allowed,
516
+ allowedThaiEsgTotal: thaiEsg.allowed
517
+ };
518
+ }
519
+ // src/th/stages/allowances.ts
520
+ /**
521
+ * Aggregates all personal, family, insurance, retirement, and donation allowances.
522
+ *
523
+ * @statute Thailand Revenue Code Section 47.
524
+ */
525
+ function calculateAllowances(input = {}, filingStatus = "single", assessableTotal, expensesTotal, wageBase) {
526
+ const fam = calculateFamilyAllowances(input, filingStatus);
527
+ const ins = calculateInsuranceAllowances(input, filingStatus);
528
+ const ret = calculateRetirementAllowances(input, assessableTotal, wageBase);
529
+ const nonDonationTotal = fam.personal.allowed + fam.spouse.allowed + fam.children.allowed + fam.parents.allowed + fam.disabled.allowed + ins.socialSecurity.allowed + ins.lifeInsurance.allowed + ins.parentHealthInsurance.allowed + ins.spouseLifeInsurance.allowed + ins.mortgageInterest.allowed + ret.allowedClusterTotal + ret.allowedThaiEsgTotal;
530
+ const netBeforeDonations = Math.max(0, assessableTotal - expensesTotal - nonDonationTotal);
531
+ const don = calculateDonationAllowances(input.donation2x, input.donation1x, netBeforeDonations);
532
+ const total = nonDonationTotal + don.allowedDonationsTotal;
533
+ return {
534
+ breakdown: {
535
+ personal: fam.personal,
536
+ spouse: fam.spouse,
537
+ children: fam.children,
538
+ parents: fam.parents,
539
+ disabled: fam.disabled,
540
+ socialSecurity: ins.socialSecurity,
541
+ lifeInsurance: ins.lifeInsurance,
542
+ parentHealthInsurance: ins.parentHealthInsurance,
543
+ spouseLifeInsurance: ins.spouseLifeInsurance,
544
+ mortgageInterest: ins.mortgageInterest,
545
+ retirementCluster: ret.retirementCluster,
546
+ thaiEsg: ret.thaiEsg,
547
+ donations: don.breakdown,
548
+ total
549
+ },
550
+ netTaxableIncome: don.netTaxableIncome
551
+ };
552
+ }
553
+ // src/th/stages/assessable.ts
554
+ function extractElectableAmount(val, field) {
555
+ if (val === void 0) return 0;
556
+ const amount = typeof val === "number" ? val : val.amount;
557
+ require_clamp.validateMonetaryAmount(amount, field);
558
+ if (typeof val === "object" && val.actualExpense !== void 0) require_clamp.validateMonetaryAmount(val.actualExpense, `${field}.actualExpense`);
559
+ return amount;
560
+ }
561
+ function sumSection40_5(input) {
562
+ if (!input) return 0;
563
+ return extractElectableAmount(input.building, "income.section40_5.building") + extractElectableAmount(input.agriculturalLand, "income.section40_5.agriculturalLand") + extractElectableAmount(input.nonAgriculturalLand, "income.section40_5.nonAgriculturalLand") + extractElectableAmount(input.vehicles, "income.section40_5.vehicles") + extractElectableAmount(input.other, "income.section40_5.other");
564
+ }
565
+ function sumSection40_6(input) {
566
+ if (!input) return 0;
567
+ return extractElectableAmount(input.medical, "income.section40_6.medical") + extractElectableAmount(input.law, "income.section40_6.law") + extractElectableAmount(input.engineering, "income.section40_6.engineering") + extractElectableAmount(input.architecture, "income.section40_6.architecture") + extractElectableAmount(input.accounting, "income.section40_6.accounting") + extractElectableAmount(input.fineArts, "income.section40_6.fineArts") + extractElectableAmount(input.other, "income.section40_6.other");
568
+ }
569
+ function sumSection40_8(input) {
570
+ if (input === void 0) return 0;
571
+ if (typeof input === "number" || "amount" in input) return extractElectableAmount(input, "income.section40_8");
572
+ return extractElectableAmount(input.general, "income.section40_8.general") + extractElectableAmount(input.entertainer, "income.section40_8.entertainer");
573
+ }
574
+ /**
575
+ * Normalizes and validates assessable income across Section 40(1)–40(8).
576
+ *
577
+ * @statute Thailand Revenue Code Section 40(1)–(8).
578
+ */
579
+ function calculateAssessableIncome(input = {}) {
580
+ const s1 = extractElectableAmount(input.section40_1, "income.section40_1");
581
+ const s2 = extractElectableAmount(input.section40_2, "income.section40_2");
582
+ const s3 = extractElectableAmount(input.section40_3, "income.section40_3");
583
+ const s4 = extractElectableAmount(typeof input.section40_4 === "number" ? input.section40_4 : input.section40_4?.amount, "income.section40_4");
584
+ const s5 = sumSection40_5(input.section40_5);
585
+ const s6 = sumSection40_6(input.section40_6);
586
+ const s7 = extractElectableAmount(input.section40_7, "income.section40_7");
587
+ const s8 = sumSection40_8(input.section40_8);
588
+ return {
589
+ section40_1: s1,
590
+ section40_2: s2,
591
+ section40_3: s3,
592
+ section40_4: s4,
593
+ section40_5: s5,
594
+ section40_6: s6,
595
+ section40_7: s7,
596
+ section40_8: s8,
597
+ total: s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8,
598
+ grossNonEmploymentIncome: s2 + s3 + s4 + s5 + s6 + s7 + s8
599
+ };
600
+ }
601
+ // src/th/stages/bracket-eval.ts
602
+ /**
603
+ * Evaluates an arbitrary Thai statutory bracket schedule (major-unit bounds) in minor units.
604
+ *
605
+ * Math: Converts bounds and incomes to satang, slices through `evaluateBrackets`, converts back.
606
+ * Units: Inputs and outputs in THB major units; arithmetic in integer satang.
607
+ * Throws: TaxValidationError if incomes are negative or non-finite.
608
+ *
609
+ * @statute Thailand Revenue Code Sections 48(1), 48(4), 48(5).
610
+ */
611
+ function evaluateThBracketSet(taxableIncome, grossIncome, brackets) {
612
+ const currency = TH_2026_PARAMETERS.currency;
613
+ const minorBrackets = brackets.map((b) => ({
614
+ upTo: b.upTo === null ? null : require_clamp.toMinorUnits(b.upTo, currency),
615
+ rate: b.rate
616
+ }));
617
+ const taxableMinor = require_clamp.toMinorUnits(taxableIncome, currency, "taxableIncome");
618
+ const grossMinor = require_clamp.toMinorUnits(grossIncome, currency, "grossIncome");
619
+ const res = require_clamp.evaluateBrackets(taxableMinor, minorBrackets, grossMinor);
620
+ const slices = res.slices.map((s) => ({
621
+ bracketIndex: s.bracketIndex,
622
+ lowerBound: require_clamp.fromMinorUnits(s.lowerBound, currency),
623
+ upperBound: s.upperBound !== null ? require_clamp.fromMinorUnits(s.upperBound, currency) : null,
624
+ rate: s.rate,
625
+ taxableAmount: require_clamp.fromMinorUnits(s.taxableAmount, currency),
626
+ tax: require_clamp.fromMinorUnits(s.tax, currency)
627
+ }));
628
+ const totalTax = require_clamp.fromMinorUnits(res.totalTax, currency);
629
+ return {
630
+ totalTax,
631
+ brackets: {
632
+ totalTax,
633
+ slices,
634
+ effectiveRate: res.effectiveRate,
635
+ marginalRate: res.marginalRate
636
+ }
637
+ };
638
+ }
639
+ /**
640
+ * Evaluates the regular progressive tax brackets for Thailand PIT.
641
+ *
642
+ * Math: Slices taxable income through `TH_2026_PARAMETERS.brackets`.
643
+ * Units: THB major units.
644
+ * Throws: TaxValidationError if incomes are negative or non-finite.
645
+ *
646
+ * @statute Thailand Revenue Code Section 48(1).
647
+ */
648
+ function evaluateThBrackets(taxableIncome, grossIncome) {
649
+ return evaluateThBracketSet(taxableIncome, grossIncome, TH_2026_PARAMETERS.brackets);
650
+ }
651
+ // src/th/stages/combine.ts
652
+ /**
653
+ * Combines regular, separate sheet, real estate, and final withholding taxes into one liability.
654
+ *
655
+ * Purpose: Produces the single audited total and the amount still payable (or refundable).
656
+ * Math: totalCombinedTax = regular + separate sheet + real estate + final withholding (before credits);
657
+ * netTaxPayable = totalCombinedTax - final withholding (already withheld at source)
658
+ * - Section 47 bis dividend credit - withholding tax paid. May be negative (refund).
659
+ * Units: Inputs and outputs in THB major units; arithmetic in integer satang.
660
+ * Throws: TaxValidationError if any component is negative or non-finite.
661
+ *
662
+ * @param input Component taxes, credits, and payments in THB.
663
+ * @statute Thailand Revenue Code Sections 40(4), 47 bis, 48(1)–(5).
664
+ */
665
+ function combineThLiability(input) {
666
+ const regular = toSatang(input.regularTax, "regularTax");
667
+ const separateSheet = toSatang(input.separateSheetTax, "separateSheetTax");
668
+ const realEstate = toSatang(input.realEstateTax, "realEstateTax");
669
+ const finalWithholding = toSatang(input.finalWithholdingTax, "finalWithholdingTax");
670
+ const dividendCredit = toSatang(input.dividendTaxCredit, "dividendTaxCredit");
671
+ const withheld = toSatang(input.withholdingTax, "withholdingTax");
672
+ const total = regular + separateSheet + realEstate + finalWithholding;
673
+ const credits = finalWithholding + dividendCredit + withheld;
674
+ return {
675
+ totalCombinedTax: fromSatang(total, "totalTax"),
676
+ netTaxPayable: fromSatang(total - credits, "netTaxPayable", { allowNegative: true })
677
+ };
678
+ }
679
+ // src/th/stages/expenses-40-1-2.ts
680
+ /**
681
+ * Calculates shared Section 40(1) and Section 40(2) expense deduction.
682
+ *
683
+ * Math: 50% of combined 40(1) + 40(2) income, clamped to 100,000 THB ceiling.
684
+ * Units: THB major units.
685
+ *
686
+ * @statute Thailand Revenue Code Section 42 bis.
687
+ */
688
+ function calculateExpenses40_1_2(s1, s2) {
689
+ const combined = s1 + s2;
690
+ const requested = Math.floor(combined * TH_2026_PARAMETERS.expenses.section40_1_2.rate);
691
+ return require_clamp.clampCap(requested, TH_2026_PARAMETERS.expenses.section40_1_2.ceiling, "expense.40_1_2");
692
+ }
693
+ // src/th/stages/expenses-40-3.ts
694
+ /**
695
+ * Calculates Section 40(3) royalties/goodwill/copyright expense deduction.
696
+ *
697
+ * Math: Standard 50% capped at 100,000 THB or actual expenses capped at income.
698
+ * Units: THB major units.
699
+ *
700
+ * @statute Thailand Revenue Code Section 42 ter; Royal Decree No. 11 Section 4.
701
+ */
702
+ function calculateExpenses40_3(input, incomeAmount) {
703
+ if (incomeAmount <= 0) return {
704
+ requested: 0,
705
+ allowed: 0,
706
+ cap: 0,
707
+ wasClamped: false
708
+ };
709
+ if (typeof input === "object" && input.expenseElection === "actual") {
710
+ const requested = input.actualExpense ?? 0;
711
+ return require_clamp.clampCap(requested, incomeAmount, "expense.40_3");
712
+ }
713
+ const requested = Math.floor(incomeAmount * TH_2026_PARAMETERS.expenses.section40_3.rate);
714
+ return require_clamp.clampCap(requested, TH_2026_PARAMETERS.expenses.section40_3.ceiling, "expense.40_3");
715
+ }
716
+ // src/th/stages/expenses-40-4.ts
717
+ /**
718
+ * Calculates Section 40(4) expense deduction (0% statutory).
719
+ *
720
+ * Math: Thailand Revenue Code permits 0% expense deductions for interest, dividends, crypto.
721
+ * Units: THB major units.
722
+ *
723
+ * @statute Thailand Revenue Code Section 40(4).
724
+ */
725
+ function calculateExpenses40_4(_incomeAmount) {
726
+ return {
727
+ requested: 0,
728
+ allowed: 0,
729
+ cap: 0,
730
+ wasClamped: false
731
+ };
732
+ }
733
+ // src/th/stages/electable-expense.ts
734
+ /**
735
+ * Computes expense deduction for an electable income item (standard % vs actual).
736
+ *
737
+ * Math: If actual, clamp to item income. If standard, apply percentage of item income.
738
+ * Units: THB major units.
739
+ *
740
+ * @param item Income item number or configuration object.
741
+ * @param standardRate Statutory percentage rate for standard deduction.
742
+ * @param field Diagnostic field name.
743
+ */
744
+ function calculateElectableItemExpense(item, standardRate, field) {
745
+ if (!item) return {
746
+ requested: 0,
747
+ allowed: 0,
748
+ cap: 0,
749
+ wasClamped: false
750
+ };
751
+ const amount = typeof item === "number" ? item : item.amount;
752
+ if (amount <= 0) return {
753
+ requested: 0,
754
+ allowed: 0,
755
+ cap: 0,
756
+ wasClamped: false
757
+ };
758
+ if (typeof item === "object" && item.expenseElection === "actual") {
759
+ const req = item.actualExpense ?? 0;
760
+ return require_clamp.clampCap(req, amount, field);
761
+ }
762
+ const req = Math.floor(amount * standardRate);
763
+ return {
764
+ requested: req,
765
+ allowed: req,
766
+ cap: amount,
767
+ wasClamped: false
768
+ };
769
+ }
770
+ // src/th/stages/expenses-40-5.ts
771
+ /**
772
+ * Calculates Section 40(5) rental expense deductions across property sub-categories.
773
+ *
774
+ * Math: Standard percentage per property class or actual expenses capped at income.
775
+ * Units: THB major units.
776
+ *
777
+ * @statute Thailand Royal Decree No. 11 Section 5.
778
+ */
779
+ function calculateExpenses40_5(input) {
780
+ if (!input) return {
781
+ requested: 0,
782
+ allowed: 0,
783
+ cap: 0,
784
+ wasClamped: false
785
+ };
786
+ const rates = TH_2026_PARAMETERS.expenses.section40_5;
787
+ const b = calculateElectableItemExpense(input.building, rates.building, "expense.40_5.building");
788
+ const a = calculateElectableItemExpense(input.agriculturalLand, rates.agriculturalLand, "expense.40_5.agri");
789
+ const na = calculateElectableItemExpense(input.nonAgriculturalLand, rates.nonAgriculturalLand, "expense.40_5.nonAgri");
790
+ const v = calculateElectableItemExpense(input.vehicles, rates.vehicles, "expense.40_5.vehicles");
791
+ const o = calculateElectableItemExpense(input.other, rates.other, "expense.40_5.other");
792
+ const requested = b.requested + a.requested + na.requested + v.requested + o.requested;
793
+ const allowed = b.allowed + a.allowed + na.allowed + v.allowed + o.allowed;
794
+ return {
795
+ requested,
796
+ allowed,
797
+ cap: b.cap + a.cap + na.cap + v.cap + o.cap,
798
+ wasClamped: requested > allowed
799
+ };
800
+ }
801
+ // src/th/stages/expenses-40-6.ts
802
+ /**
803
+ * Calculates Section 40(6) liberal professions expense deduction.
804
+ *
805
+ * Math: Medical 60%, law/engineering/architecture/accounting/fine arts/other 30%, or actual.
806
+ * Units: THB major units.
807
+ *
808
+ * @statute Thailand Royal Decree No. 11 Section 6.
809
+ */
810
+ function calculateExpenses40_6(input) {
811
+ if (!input) return {
812
+ requested: 0,
813
+ allowed: 0,
814
+ cap: 0,
815
+ wasClamped: false
816
+ };
817
+ const rates = TH_2026_PARAMETERS.expenses.section40_6;
818
+ const med = calculateElectableItemExpense(input.medical, rates.medical, "expense.40_6.medical");
819
+ const law = calculateElectableItemExpense(input.law, rates.other, "expense.40_6.law");
820
+ const eng = calculateElectableItemExpense(input.engineering, rates.other, "expense.40_6.eng");
821
+ const arch = calculateElectableItemExpense(input.architecture, rates.other, "expense.40_6.arch");
822
+ const acc = calculateElectableItemExpense(input.accounting, rates.other, "expense.40_6.acc");
823
+ const art = calculateElectableItemExpense(input.fineArts, rates.other, "expense.40_6.art");
824
+ const oth = calculateElectableItemExpense(input.other, rates.other, "expense.40_6.other");
825
+ const requested = med.requested + law.requested + eng.requested + arch.requested + acc.requested + art.requested + oth.requested;
826
+ const allowed = med.allowed + law.allowed + eng.allowed + arch.allowed + acc.allowed + art.allowed + oth.allowed;
827
+ return {
828
+ requested,
829
+ allowed,
830
+ cap: med.cap + law.cap + eng.cap + arch.cap + acc.cap + art.cap + oth.cap,
831
+ wasClamped: requested > allowed
832
+ };
833
+ }
834
+ // src/th/stages/expenses-40-7.ts
835
+ /**
836
+ * Calculates Section 40(7) contracting expense deduction.
837
+ *
838
+ * Math: Standard 60% or actual expenses capped at income.
839
+ * Units: THB major units.
840
+ *
841
+ * @statute Thailand Royal Decree No. 11 Section 7.
842
+ */
843
+ function calculateExpenses40_7(input) {
844
+ return calculateElectableItemExpense(input, TH_2026_PARAMETERS.expenses.section40_7.rate, "expense.40_7");
845
+ }
846
+ // src/th/stages/expenses-40-8.ts
847
+ function calculateEntertainerExpense(item) {
848
+ if (!item) return {
849
+ requested: 0,
850
+ allowed: 0,
851
+ cap: 0,
852
+ wasClamped: false
853
+ };
854
+ const amount = typeof item === "number" ? item : item.amount;
855
+ if (amount <= 0) return {
856
+ requested: 0,
857
+ allowed: 0,
858
+ cap: 0,
859
+ wasClamped: false
860
+ };
861
+ if (typeof item === "object" && item.expenseElection === "actual") return require_clamp.clampCap(item.actualExpense ?? 0, amount, "expense.40_8.entertainer");
862
+ const p = TH_2026_PARAMETERS.expenses.section40_8;
863
+ const tier1 = Math.min(amount, p.entertainerFirstTierThreshold);
864
+ const tier2 = Math.max(0, amount - p.entertainerFirstTierThreshold);
865
+ const computed = Math.floor(tier1 * p.entertainerFirstTierRate) + Math.floor(tier2 * p.entertainerSecondTierRate);
866
+ return require_clamp.clampCap(computed, p.entertainerCeiling, "expense.40_8.entertainer");
867
+ }
868
+ /**
869
+ * Calculates Section 40(8) business, commerce, and entertainer expense deduction.
870
+ *
871
+ * Math: General 60% or actual; entertainer tiered 60%/40% up to 600,000 THB or actual.
872
+ * Units: THB major units.
873
+ *
874
+ * @statute Thailand Royal Decree No. 11 Section 8.
875
+ */
876
+ function calculateExpenses40_8(input) {
877
+ if (!input) return {
878
+ requested: 0,
879
+ allowed: 0,
880
+ cap: 0,
881
+ wasClamped: false
882
+ };
883
+ if (typeof input === "number" || "amount" in input) return calculateElectableItemExpense(input, TH_2026_PARAMETERS.expenses.section40_8.generalRate, "expense.40_8.general");
884
+ const gen = calculateElectableItemExpense(input.general, TH_2026_PARAMETERS.expenses.section40_8.generalRate, "expense.40_8.general");
885
+ const ent = calculateEntertainerExpense(input.entertainer);
886
+ const requested = gen.requested + ent.requested;
887
+ const allowed = gen.allowed + ent.allowed;
888
+ return {
889
+ requested,
890
+ allowed,
891
+ cap: gen.cap + ent.cap,
892
+ wasClamped: requested > allowed
893
+ };
894
+ }
895
+ // src/th/stages/expenses.ts
896
+ /**
897
+ * Aggregates all Section 40(1)–40(8) statutory expense deductions.
898
+ *
899
+ * @statute Thailand Revenue Code Sections 42 bis, 42 ter, 43–46; Royal Decree No. 11.
900
+ */
901
+ function calculateExpenseDeductions(income = {}, assessable) {
902
+ const e1_2 = calculateExpenses40_1_2(assessable.section40_1, assessable.section40_2);
903
+ const e3 = calculateExpenses40_3(income.section40_3, assessable.section40_3);
904
+ const e4 = calculateExpenses40_4(assessable.section40_4);
905
+ const e5 = calculateExpenses40_5(income.section40_5);
906
+ const e6 = calculateExpenses40_6(income.section40_6);
907
+ const e7 = calculateExpenses40_7(income.section40_7);
908
+ const e8 = calculateExpenses40_8(income.section40_8);
909
+ return {
910
+ section40_1_2: e1_2,
911
+ section40_3: e3,
912
+ section40_4: e4,
913
+ section40_5: e5,
914
+ section40_6: e6,
915
+ section40_7: e7,
916
+ section40_8: e8,
917
+ total: e1_2.allowed + e3.allowed + e4.allowed + e5.allowed + e6.allowed + e7.allowed + e8.allowed
918
+ };
919
+ }
920
+ // src/th/stages/minimum-tax.ts
921
+ /**
922
+ * Calculates Section 48(2) Minimum Gross Tax on Section 40(2)–40(8) income.
923
+ *
924
+ * Math: 0.5% of gross Section 40(2)–(8) if gross >= 120,000 THB; payable only if the 0.5% tax exceeds 5,000 THB
925
+ * and exceeds the progressive tax.
926
+ * Units: THB major units.
927
+ *
928
+ * @statute Thailand Revenue Code Section 48(2).
929
+ */
930
+ function calculateMinimumGrossTax(grossNonEmploymentIncome, progressiveTax) {
931
+ const p = TH_2026_PARAMETERS.minimumGrossTax;
932
+ if (grossNonEmploymentIncome < p.incomeThreshold) return {
933
+ grossEligibleIncome: grossNonEmploymentIncome,
934
+ rate: p.rate,
935
+ calculatedTax: 0,
936
+ threshold: p.taxThreshold,
937
+ isApplicable: false,
938
+ wasApplied: false
939
+ };
940
+ const currency = TH_2026_PARAMETERS.currency;
941
+ const minorIncome = require_clamp.toMinorUnits(grossNonEmploymentIncome, currency, "minimumGrossTax");
942
+ const minorTax = require_clamp.applyPercentage(minorIncome, p.rate);
943
+ const calculatedTax = require_clamp.fromMinorUnits(minorTax, currency);
944
+ const isApplicable = calculatedTax > p.taxThreshold;
945
+ const wasApplied = isApplicable && calculatedTax > progressiveTax;
946
+ return {
947
+ grossEligibleIncome: grossNonEmploymentIncome,
948
+ rate: p.rate,
949
+ calculatedTax,
950
+ threshold: p.taxThreshold,
951
+ isApplicable,
952
+ wasApplied
953
+ };
954
+ }
955
+ // src/th/validate.ts
956
+ /**
957
+ * Validates a Thai corporate income tax rate used for the Section 47 bis gross-up.
958
+ *
959
+ * Purpose: Keeps `rate / (1 - rate)` finite before any stage uses the rate.
960
+ * Math: Requires `0 <= rate < 1`.
961
+ * Units: Rate ratio (0.2 = 20%).
962
+ * Throws: TaxValidationError if rate is NaN, non-finite, negative, or at least 1.
963
+ *
964
+ * @param rate Corporate income tax rate ratio.
965
+ * @statute Thailand Revenue Code Section 47 bis.
966
+ */
967
+ function validateCorpTaxRate(rate) {
968
+ if (!Number.isFinite(rate) || rate < 0 || rate >= 1) throw new require_clamp.TaxValidationError("income.section40_4.dividends.corpTaxRate", rate, "Corporate tax rate must be finite and satisfy 0 <= rate < 1");
969
+ }
970
+ /**
971
+ * Resolves an optional typed choice (election, payment type, acquisition type) at runtime.
972
+ *
973
+ * Purpose: TypeScript unions are erased at runtime, so untyped callers could otherwise pass an
974
+ * unknown string and silently get the default behaviour.
975
+ * Math: Returns `fallback` when `value` is `undefined`; otherwise requires membership in `choices`.
976
+ * Units: Dimensionless string enum.
977
+ * Throws: TaxValidationError (reported against `field`) if `value` is not one of `choices`.
978
+ *
979
+ * @param value Caller-supplied value, possibly outside the declared union.
980
+ * @param choices Typed const object whose values are the permitted choices.
981
+ * @param fallback Choice used when `value` is `undefined`.
982
+ * @param field Diagnostic field path reported on failure.
983
+ * @statute Jurisdiction-neutral input bound (AGENTS rule 8); no statutory citation.
984
+ */
985
+ function resolveChoice(value, choices, fallback, field) {
986
+ if (value === void 0) return fallback;
987
+ const match = Object.values(choices).find((choice) => choice === value);
988
+ if (match === void 0) throw new require_clamp.TaxValidationError(field, value, "Value is not a supported choice");
989
+ return match;
990
+ }
991
+ // src/th/stages/dividend-credit.ts
992
+ /**
993
+ * Calculates the Section 47 bis dividend tax credit for dividends included in regular income.
994
+ *
995
+ * Purpose: Recovers the corporate tax already paid on dividends taxed again in the regular schedule.
996
+ * Math: credit = dividend * corpTaxRate / (1 - corpTaxRate), exact in satang and rounded half-up.
997
+ * The grossed-up amount (dividend + credit) is assessable; the credit offsets tax payable.
998
+ * Units: Inputs and outputs in THB major units; arithmetic in integer satang.
999
+ * Throws: TaxValidationError if dividend is negative/non-finite or the rate is invalid.
1000
+ *
1001
+ * @param includedDividends Dividends elected `"include"`, in THB.
1002
+ * @param corpTaxRate Corporate tax rate in [0, 1); `undefined` means no credit.
1003
+ * @statute Thailand Revenue Code Section 47 bis.
1004
+ */
1005
+ function calculateDividendTaxCredit(includedDividends, corpTaxRate) {
1006
+ require_clamp.validateMonetaryAmount(includedDividends, "income.section40_4.dividends");
1007
+ if (corpTaxRate === void 0) return {
1008
+ isApplicable: false,
1009
+ dividendAmount: includedDividends,
1010
+ corpTaxRate: 0,
1011
+ credit: 0,
1012
+ grossedUpAmount: includedDividends
1013
+ };
1014
+ validateCorpTaxRate(corpTaxRate);
1015
+ const dividendSatang = toSatang(includedDividends, "income.section40_4.dividends");
1016
+ const creditSatang = require_clamp.applyGrossUp(dividendSatang, corpTaxRate, "income.section40_4.dividends");
1017
+ return {
1018
+ isApplicable: true,
1019
+ dividendAmount: includedDividends,
1020
+ corpTaxRate,
1021
+ credit: fromSatang(creditSatang, "dividendTaxCredit"),
1022
+ grossedUpAmount: fromSatang(dividendSatang + creditSatang, "dividendTaxCredit")
1023
+ };
1024
+ }
1025
+ // src/th/stages/final-withholding-source.ts
1026
+ function electionOf(input, field) {
1027
+ return resolveChoice(typeof input === "object" ? input.election : void 0, ThFinalWithholdingElection, ThFinalWithholdingElection.Include, field);
1028
+ }
1029
+ /**
1030
+ * Resolves one Section 40(4) source: validates its amount and election, withholds if final.
1031
+ *
1032
+ * Purpose: Applies the per-source final withholding election.
1033
+ * Math: tax = amount * rate (satang, half-up) when the election is `"final"`, otherwise 0.
1034
+ * Units: Input in THB major units; `satang` and `taxSatang` in integer satang.
1035
+ * Throws: TaxValidationError for a negative/non-finite amount or an unknown election value.
1036
+ *
1037
+ * @param input Bare amount, source object, or undefined.
1038
+ * @param field Diagnostic path of the amount.
1039
+ * @param electionField Diagnostic path of the election.
1040
+ * @param rate Final withholding rate (ratio).
1041
+ * @statute Thailand Revenue Code Sections 40(4), 50(2)(g), 57 ter.
1042
+ */
1043
+ function resolveSource(input, field, electionField, rate) {
1044
+ const amount = extractElectableAmount(input, field);
1045
+ const election = electionOf(input, electionField);
1046
+ const satang = toSatang(amount, field);
1047
+ const taxSatang = election === ThFinalWithholdingElection.Final ? require_clamp.applyPercentage(satang, rate) : 0;
1048
+ return {
1049
+ breakdown: {
1050
+ election,
1051
+ amount,
1052
+ rate,
1053
+ tax: fromSatang(taxSatang)
1054
+ },
1055
+ satang,
1056
+ taxSatang
1057
+ };
1058
+ }
1059
+ // src/th/stages/final-withholding.ts
1060
+ function toStructuredInput(input) {
1061
+ return typeof input === "number" ? { amount: input } : input ?? {};
1062
+ }
1063
+ function sumSatang(sources, seed = 0) {
1064
+ return sources.reduce((sum, source) => sum + source.satang, seed);
1065
+ }
1066
+ /**
1067
+ * Splits Section 40(4) income into regular-bracket income and final-withheld income.
1068
+ *
1069
+ * Purpose: Applies each source's election so only included income reaches the regular schedule.
1070
+ * Math: For each source elected `"final"`, tax = amount * rate (satang, half-up) and the income is
1071
+ * excluded from regular brackets. `"include"` (default) adds the income to Section 40(4)
1072
+ * assessable income at a 0% expense rate.
1073
+ * Units: Inputs and outputs in THB major units; arithmetic in integer satang.
1074
+ * Throws: TaxValidationError for negative/non-finite amounts or an invalid `corpTaxRate`.
1075
+ *
1076
+ * @param input Bare amount, structured sources, or undefined.
1077
+ * @statute Thailand Revenue Code Sections 40(4), 50(2)(g), 56, 57 ter.
1078
+ */
1079
+ function resolveSection40_4(input) {
1080
+ const section = toStructuredInput(input);
1081
+ const rates = TH_2026_PARAMETERS.finalWithholding;
1082
+ const base = extractElectableAmount(section.amount, "income.section40_4");
1083
+ const interest = resolveSource(section.interest, "income.section40_4.interest", "income.section40_4.interest.election", rates.interestRate);
1084
+ const dividends = resolveSource(section.dividends, "income.section40_4.dividends", "income.section40_4.dividends.election", rates.dividendRate);
1085
+ const digital = resolveSource(section.digitalAssets, "income.section40_4.digitalAssets", "income.section40_4.digitalAssets.election", rates.digitalAssetRate);
1086
+ const corpTaxRate = typeof section.dividends === "object" ? section.dividends.corpTaxRate : void 0;
1087
+ if (corpTaxRate !== void 0) validateCorpTaxRate(corpTaxRate);
1088
+ const sources = [
1089
+ interest,
1090
+ dividends,
1091
+ digital
1092
+ ];
1093
+ const isFinal = (source) => source.breakdown.election === ThFinalWithholdingElection.Final;
1094
+ const finalSources = sources.filter(isFinal);
1095
+ const includedSatang = sumSatang(sources.filter((source) => !isFinal(source)), toSatang(base, "income.section40_4"));
1096
+ const isDividendIncluded = !isFinal(dividends);
1097
+ return {
1098
+ includedAmount: fromSatang(includedSatang),
1099
+ includedDividends: isDividendIncluded ? dividends.breakdown.amount : 0,
1100
+ corpTaxRate: isDividendIncluded ? corpTaxRate : void 0,
1101
+ finalWithholding: {
1102
+ interest: interest.breakdown,
1103
+ dividends: dividends.breakdown,
1104
+ digitalAssets: digital.breakdown,
1105
+ totalAmount: fromSatang(sumSatang(finalSources)),
1106
+ totalTax: fromSatang(finalSources.reduce((sum, source) => sum + source.taxSatang, 0))
1107
+ }
1108
+ };
1109
+ }
1110
+ // src/th/stages/real-estate-expense.ts
1111
+ /**
1112
+ * Caps the holding period used by Section 48(4) at the statutory maximum.
1113
+ *
1114
+ * Purpose: More than 10 years of holding counts as 10 years.
1115
+ * Math: min(requestedYears, `realEstate.maxHoldingYears`).
1116
+ * Units: Dimensionless year count (callers round part-years up).
1117
+ * Throws: Never; `requestedYears` is validated by the caller.
1118
+ *
1119
+ * @param requestedYears Validated positive safe integer holding period.
1120
+ * @statute Thailand Revenue Code Section 48(4); Ministerial Regulation No. 126.
1121
+ */
1122
+ function effectiveHoldingYears(requestedYears) {
1123
+ return Math.min(requestedYears, TH_2026_PARAMETERS.realEstate.maxHoldingYears);
1124
+ }
1125
+ /**
1126
+ * Selects the Section 48(4) expense rate for the acquisition type.
1127
+ *
1128
+ * Purpose: Inherited or gifted property takes a flat rate; commercial or other property takes the
1129
+ * holding-year table. The two are never combined.
1130
+ * Math: Flat `inheritanceExpenseRate`, or the last `expenseTable` row with `minYears <= years`.
1131
+ * Units: Rate ratio; `years` is the effective (capped) holding period.
1132
+ * Throws: Never; the acquisition type is validated by the caller.
1133
+ *
1134
+ * @param acquisitionType Validated acquisition type.
1135
+ * @param years Effective holding years.
1136
+ * @statute Thailand Revenue Code Section 48(4); Ministerial Regulation No. 126.
1137
+ */
1138
+ function resolveRealEstateExpense(acquisitionType, years) {
1139
+ const rules = TH_2026_PARAMETERS.realEstate;
1140
+ if (acquisitionType === ThRealEstateAcquisitionType.InheritanceOrGift) return {
1141
+ basis: ThRealEstateExpenseBasis.FlatInheritance,
1142
+ rate: rules.inheritanceExpenseRate
1143
+ };
1144
+ const rate = rules.expenseTable.reduce((acc, row) => years >= row.minYears ? row.rate : acc, 0);
1145
+ return {
1146
+ basis: ThRealEstateExpenseBasis.HoldingPeriodTable,
1147
+ rate
1148
+ };
1149
+ }
1150
+ // src/th/stages/real-estate.ts
1151
+ const EMPTY_BREAKDOWN = {
1152
+ grossTransferValue: 0,
1153
+ acquisitionType: ThRealEstateAcquisitionType.Commercial,
1154
+ expenseBasis: ThRealEstateExpenseBasis.HoldingPeriodTable,
1155
+ requestedHoldingYears: 0,
1156
+ effectiveHoldingYears: 0,
1157
+ expenseRate: 0,
1158
+ expense: 0,
1159
+ netGain: 0,
1160
+ annualGain: 0,
1161
+ brackets: evaluateThBrackets(0, 0).brackets,
1162
+ annualTax: 0,
1163
+ taxBeforeCap: 0,
1164
+ taxCap: 0,
1165
+ wasClamped: false,
1166
+ tax: 0
1167
+ };
1168
+ /**
1169
+ * Scales every finite annualised-schedule bracket bound by the holding period.
1170
+ *
1171
+ * Math: `years * tax(net / years)` equals the tax on `net` against bounds multiplied by `years`
1172
+ * (each slice `rate * clip(net/years - lo, 0, width) * years = rate * clip(net - lo*years, 0, width*years)`),
1173
+ * so the annualisation needs no intermediate rounding.
1174
+ * Units: THB major units; scaling performed in integer satang.
1175
+ * Throws: TaxValidationError if a scaled bound exceeds the safe integer range (unreachable
1176
+ * for the capped holding period).
1177
+ */
1178
+ function scaleBracketsByYears(years) {
1179
+ return TH_2026_PARAMETERS.realEstate.brackets.map((bracket) => {
1180
+ if (bracket.upTo === null) return bracket;
1181
+ return {
1182
+ upTo: fromSatang(require_clamp.multiplySafeInteger(toSatang(bracket.upTo), years, "realEstateTransfer.holdingYears")),
1183
+ rate: bracket.rate
1184
+ };
1185
+ });
1186
+ }
1187
+ /**
1188
+ * Clamps real estate tax at the statutory share of the gross transfer value.
1189
+ *
1190
+ * Purpose: Applies the Section 48(4) ceiling on the separately computed tax.
1191
+ * Math: taxCap = grossMinor * `realEstate.taxCapRate` (20%, half-up); tax = min(taxBeforeCap, taxCap).
1192
+ * Units: Integer minor units (satang) in and out.
1193
+ * Throws: TaxValidationError if `grossMinor` is not a non-negative safe integer.
1194
+ * Note: Unreachable end to end (top rate 35% of at most 50% net is 17.5% < 20%); kept as the
1195
+ * statutory guard.
1196
+ *
1197
+ * @param taxBeforeCap Uncapped tax in satang.
1198
+ * @param grossMinor Gross transfer value in satang.
1199
+ * @statute Thailand Revenue Code Section 48(4).
1200
+ */
1201
+ function applyRealEstateCap(taxBeforeCap, grossMinor) {
1202
+ const taxCap = require_clamp.applyPercentage(grossMinor, TH_2026_PARAMETERS.realEstate.taxCapRate);
1203
+ return {
1204
+ taxCap,
1205
+ tax: Math.min(taxBeforeCap, taxCap),
1206
+ wasClamped: taxBeforeCap > taxCap
1207
+ };
1208
+ }
1209
+ /**
1210
+ * Calculates Section 48(4) tax on an inherited, gifted, or sold real estate transfer.
1211
+ *
1212
+ * Purpose: Taxes the gain as if earned evenly over the holding period, then scales back up.
1213
+ * `grossTransferValue` must be the official appraised value (ราคาประเมินทุนทรัพย์, Section 49 bis),
1214
+ * not the actual sale price. `holdingYears` is a positive safe integer; callers round part-years
1215
+ * up, and more than 10 years counts as 10.
1216
+ * Math: years = min(holdingYears, 10); expense = gross * 50% for inheritance/gift, otherwise
1217
+ * gross * table(years) (never combined); net = gross - expense; tax = years * bracketTax(net / years)
1218
+ * on the schedule without a 0% tier (5% from the first baht), evaluated exactly as bracketTax of
1219
+ * `net` against bounds multiplied by `years` (no per-year rounding); tax is clamped at 20% of gross
1220
+ * (which the 35% top rate on at most 50% net can never reach). `annualGain` and `annualTax` are
1221
+ * rounded for display.
1222
+ * Units: Inputs and outputs in THB major units; arithmetic in integer satang.
1223
+ * Throws: TaxValidationError for a negative/non-finite value, a non-positive/non-safe-integer
1224
+ * holding period, or an unknown `acquisitionType`.
1225
+ *
1226
+ * @statute Thailand Revenue Code Section 48(4), Section 42(5), Section 49 bis.
1227
+ */
1228
+ function calculateRealEstateTransfer(input) {
1229
+ if (input === void 0) return EMPTY_BREAKDOWN;
1230
+ require_clamp.validateMonetaryAmount(input.grossTransferValue, "realEstateTransfer.grossTransferValue");
1231
+ require_clamp.validateSafeIntegerCount(input.holdingYears, "realEstateTransfer.holdingYears", 1);
1232
+ const acquisitionType = resolveChoice(input.acquisitionType, ThRealEstateAcquisitionType, ThRealEstateAcquisitionType.Commercial, "realEstateTransfer.acquisitionType");
1233
+ const years = effectiveHoldingYears(input.holdingYears);
1234
+ const { basis, rate: expenseRate } = resolveRealEstateExpense(acquisitionType, years);
1235
+ const gross = toSatang(input.grossTransferValue, "realEstateTransfer.grossTransferValue");
1236
+ const expense = require_clamp.applyPercentage(gross, expenseRate);
1237
+ const netGain = gross - expense;
1238
+ const evaluated = evaluateThBracketSet(fromSatang(netGain), fromSatang(netGain), scaleBracketsByYears(years));
1239
+ const taxBeforeCap = toSatang(evaluated.totalTax);
1240
+ const capped = applyRealEstateCap(taxBeforeCap, gross);
1241
+ return {
1242
+ grossTransferValue: input.grossTransferValue,
1243
+ acquisitionType,
1244
+ expenseBasis: basis,
1245
+ requestedHoldingYears: input.holdingYears,
1246
+ effectiveHoldingYears: years,
1247
+ expenseRate,
1248
+ expense: fromSatang(expense),
1249
+ netGain: fromSatang(netGain),
1250
+ annualGain: fromSatang(require_clamp.roundHalfUp(netGain / years)),
1251
+ brackets: evaluated.brackets,
1252
+ annualTax: fromSatang(require_clamp.roundHalfUp(taxBeforeCap / years)),
1253
+ taxBeforeCap: fromSatang(taxBeforeCap),
1254
+ taxCap: fromSatang(capped.taxCap),
1255
+ wasClamped: capped.wasClamped,
1256
+ tax: fromSatang(capped.tax)
1257
+ };
1258
+ }
1259
+ // src/th/stages/regular-income.ts
1260
+ /**
1261
+ * Assembles the income that flows through the regular progressive schedule.
1262
+ *
1263
+ * Math: Section 40(1) = declared Section 40(1) + ineligible Section 48(5) lump sum;
1264
+ * Section 40(4) = included Section 40(4) income (final-withheld sources excluded)
1265
+ * + Section 47 bis gross-up. Other categories pass through untouched.
1266
+ * Units: THB major units; the Section 40(1) sum is performed in integer satang.
1267
+ * Throws: TaxValidationError if the declared Section 40(1) is negative or non-finite.
1268
+ *
1269
+ * @param income Declared Section 40 income.
1270
+ * @param special Lump sum, included Section 40(4) income, and dividend gross-up.
1271
+ * @statute Thailand Revenue Code Sections 40(1), 40(4), 47 bis, 48(5).
1272
+ */
1273
+ function buildRegularIncome(income, special) {
1274
+ const declared = income?.section40_1 ?? 0;
1275
+ require_clamp.validateMonetaryAmount(declared, "income.section40_1");
1276
+ const section40_1 = toSatang(declared, "income.section40_1") + toSatang(special.lumpSum, "income.section40_1");
1277
+ const section40_4 = toSatang(special.section40_4, "income.section40_4") + toSatang(special.dividendCredit, "income.section40_4");
1278
+ return {
1279
+ ...income,
1280
+ section40_1: fromSatang(section40_1),
1281
+ section40_4: fromSatang(section40_4)
1282
+ };
1283
+ }
1284
+ // src/th/stages/separate-sheet.ts
1285
+ const EMPTY_INPUT = {
1286
+ amount: 0,
1287
+ yearsOfService: 0,
1288
+ lastDailyWage: 0
1289
+ };
1290
+ /**
1291
+ * Calculates the Section 48(5) separate sheet for one-time severance/retirement lump sums.
1292
+ *
1293
+ * Purpose: Applies the severance exemption, then taxes an eligible remainder on its own schedule
1294
+ * or returns the ineligible remainder for regular treatment.
1295
+ * Math: For `paymentType` severance (the default) at ANY length of service:
1296
+ * exemption = min(amount, 400 * lastDailyWage, 600,000); `"retirement"` payments get none.
1297
+ * remainder = amount - exemption. With yearsOfService >= 5: fixed = min(7,000 * years, remainder);
1298
+ * percentage expense = 50% of (remainder - fixed), half-up; net = remainder - fixed - percentage,
1299
+ * taxed on the separate schedule (5%..35%, no 0% tier). With fewer than 5 years only the remainder
1300
+ * is returned in `addedToSection40_1` for the regular computation.
1301
+ * Units: Inputs and outputs in THB major units; arithmetic in integer satang.
1302
+ * Throws: TaxValidationError for negative/non-finite money, a non-safe-integer/negative `yearsOfService`,
1303
+ * an unknown `paymentType`, or a wage/years value whose scaled satang amount exceeds safe integers.
1304
+ *
1305
+ * @statute Thailand Revenue Code Section 48(5); Section 42(1) (severance exemption).
1306
+ */
1307
+ function calculateSeparateSheet(input = EMPTY_INPUT) {
1308
+ const rules = TH_2026_PARAMETERS.separateSheet;
1309
+ require_clamp.validateMonetaryAmount(input.amount, "separateSheet.amount");
1310
+ require_clamp.validateMonetaryAmount(input.lastDailyWage, "separateSheet.lastDailyWage");
1311
+ require_clamp.validateSafeIntegerCount(input.yearsOfService, "separateSheet.yearsOfService");
1312
+ const paymentType = resolveChoice(input.paymentType, ThSeparateSheetPaymentType, ThSeparateSheetPaymentType.Severance, "separateSheet.paymentType");
1313
+ const years = input.yearsOfService;
1314
+ const isEligible = years >= rules.minYearsOfService;
1315
+ const amountSatang = toSatang(input.amount, "separateSheet.amount");
1316
+ const wageSatang = toSatang(input.lastDailyWage, "separateSheet.lastDailyWage");
1317
+ const isSeverance = paymentType === ThSeparateSheetPaymentType.Severance;
1318
+ const wageCap = isSeverance ? require_clamp.multiplySafeInteger(wageSatang, rules.exemptionWageDays, "separateSheet.lastDailyWage") : 0;
1319
+ const exemption = isSeverance ? Math.min(amountSatang, wageCap, toSatang(rules.exemptionCeiling)) : 0;
1320
+ const remainder = amountSatang - exemption;
1321
+ const fixedCap = require_clamp.multiplySafeInteger(toSatang(rules.expensePerYearOfService), years, "separateSheet.yearsOfService");
1322
+ const fixed = isEligible ? Math.min(remainder, fixedCap) : 0;
1323
+ const percentage = isEligible ? require_clamp.applyPercentage(remainder - fixed, rules.remainderExpenseRate) : 0;
1324
+ const net = fromSatang(isEligible ? remainder - fixed - percentage : 0);
1325
+ const evaluated = evaluateThBracketSet(net, net, rules.brackets);
1326
+ return {
1327
+ amount: input.amount,
1328
+ yearsOfService: years,
1329
+ lastDailyWage: input.lastDailyWage,
1330
+ isEligible,
1331
+ addedToSection40_1: isEligible ? 0 : fromSatang(remainder),
1332
+ wageBasedCap: fromSatang(wageCap),
1333
+ exemptionCeiling: rules.exemptionCeiling,
1334
+ paymentType,
1335
+ exemption: fromSatang(exemption),
1336
+ remainder: fromSatang(remainder),
1337
+ fixedExpense: fromSatang(fixed),
1338
+ percentageExpense: fromSatang(percentage),
1339
+ netTaxable: net,
1340
+ brackets: evaluated.brackets,
1341
+ tax: evaluated.totalTax
1342
+ };
1343
+ }
1344
+ // src/th/stages/special-regimes.ts
1345
+ /**
1346
+ * Resolves Section 40(4) elections, the Section 47 bis credit, the Section 48(5) separate sheet,
1347
+ * and the Section 48(4) real estate transfer, then assembles the remaining regular income.
1348
+ *
1349
+ * Math: Regular Section 40(4) = included sources + dividend gross-up; ineligible lump sum joins
1350
+ * Section 40(1). Gross outside the regular schedule = final-elected 40(4) + eligible lump sum (or, if
1351
+ * ineligible, its severance exemption) + real estate gross transfer value.
1352
+ * Units: THB major units; sums performed in integer satang.
1353
+ * Throws: TaxValidationError for any invalid special-regime input.
1354
+ *
1355
+ * @statute Thailand Revenue Code Sections 40(4), 47 bis, 48(4), 48(5).
1356
+ */
1357
+ function resolveSpecialRegimes(input) {
1358
+ const section40_4 = resolveSection40_4(input.income?.section40_4);
1359
+ const dividendTaxCredit = calculateDividendTaxCredit(section40_4.includedDividends, section40_4.corpTaxRate);
1360
+ const separateSheet = calculateSeparateSheet(input.separateSheet);
1361
+ const realEstate = calculateRealEstateTransfer(input.realEstateTransfer);
1362
+ return {
1363
+ regularIncome: buildRegularIncome(input.income, {
1364
+ lumpSum: separateSheet.addedToSection40_1,
1365
+ section40_4: section40_4.includedAmount,
1366
+ dividendCredit: dividendTaxCredit.credit
1367
+ }),
1368
+ grossOutsideRegular: sumThAmounts([
1369
+ section40_4.finalWithholding.totalAmount,
1370
+ separateSheet.isEligible ? separateSheet.amount : separateSheet.exemption,
1371
+ realEstate.grossTransferValue
1372
+ ], "grossIncome"),
1373
+ finalWithholding: section40_4.finalWithholding,
1374
+ dividendTaxCredit,
1375
+ separateSheet,
1376
+ realEstate
1377
+ };
1378
+ }
1379
+ // src/th/stages/pipeline.ts
1380
+ function validateYear(year) {
1381
+ if (year !== void 0 && year !== TH_2026_PARAMETERS.taxYear) throw new require_clamp.TaxValidationError("taxYear", year, "Unsupported tax year; only 2026 is currently supported");
1382
+ }
1383
+ /**
1384
+ * Executes full Thailand PIT calculation pipeline for ภ.ง.ด.90/91.
1385
+ *
1386
+ * @statute Thailand Revenue Code Sections 40, 42 bis, 47, 47 bis, 48(1)–(5).
1387
+ */
1388
+ function runThTaxPipeline(input = {}) {
1389
+ validateYear(input.taxYear);
1390
+ const filingStatus = input.filingStatus ?? "single";
1391
+ const withholding = input.withholdingTax ?? 0;
1392
+ require_clamp.validateMonetaryAmount(withholding, "withholdingTax");
1393
+ const special = resolveSpecialRegimes(input);
1394
+ const income = special.regularIncome;
1395
+ const assessable = calculateAssessableIncome(income);
1396
+ const expenses = calculateExpenseDeductions(income, assessable);
1397
+ const netIncomeAfterExpenses = Math.max(0, assessable.total - expenses.total);
1398
+ const allowancesRes = calculateAllowances(input.allowances, filingStatus, assessable.total, expenses.total, assessable.section40_1);
1399
+ const evalBrackets = evaluateThBrackets(allowancesRes.netTaxableIncome, assessable.total);
1400
+ const progressiveTax = evalBrackets.totalTax;
1401
+ const minimumGrossTax = calculateMinimumGrossTax(assessable.grossNonEmploymentIncome, progressiveTax);
1402
+ const regularTax = minimumGrossTax.wasApplied ? minimumGrossTax.calculatedTax : progressiveTax;
1403
+ const combined = combineThLiability({
1404
+ regularTax,
1405
+ separateSheetTax: special.separateSheet.tax,
1406
+ realEstateTax: special.realEstate.tax,
1407
+ finalWithholdingTax: special.finalWithholding.totalTax,
1408
+ dividendTaxCredit: special.dividendTaxCredit.credit,
1409
+ withholdingTax: withholding
1410
+ });
1411
+ const grossIncome = sumThAmounts([assessable.total, special.grossOutsideRegular], "grossIncome");
1412
+ const effectiveRate = grossIncome > 0 ? combined.totalCombinedTax / grossIncome : 0;
1413
+ const breakdown = {
1414
+ assessableIncome: assessable,
1415
+ expenseDeductions: expenses,
1416
+ netIncomeAfterExpenses,
1417
+ allowances: allowancesRes.breakdown,
1418
+ netTaxableIncome: allowancesRes.netTaxableIncome,
1419
+ progressiveTax,
1420
+ progressiveBrackets: evalBrackets.brackets,
1421
+ minimumGrossTax,
1422
+ withholdingTax: withholding,
1423
+ netTaxPayable: combined.netTaxPayable,
1424
+ regularTax,
1425
+ separateSheet: special.separateSheet,
1426
+ realEstate: special.realEstate,
1427
+ finalWithholding: special.finalWithholding,
1428
+ dividendTaxCredit: special.dividendTaxCredit,
1429
+ totalCombinedTax: combined.totalCombinedTax
1430
+ };
1431
+ return {
1432
+ jurisdiction: "TH",
1433
+ taxYear: TH_2026_PARAMETERS.taxYear,
1434
+ currency: TH_2026_PARAMETERS.currency,
1435
+ grossIncome: require_clamp.thb(grossIncome, "grossIncome"),
1436
+ taxableIncome: require_clamp.thb(allowancesRes.netTaxableIncome, "taxableIncome"),
1437
+ totalTax: require_clamp.thb(combined.totalCombinedTax, "totalTax"),
1438
+ effectiveRate,
1439
+ marginalRate: evalBrackets.brackets.marginalRate,
1440
+ netTaxPayable: require_clamp.thb(combined.netTaxPayable, "netTaxPayable", { allowNegative: true }),
1441
+ breakdown
1442
+ };
1443
+ }
1444
+ // src/th/calculate.ts
1445
+ /**
1446
+ * Calculates Thailand Personal Income Tax (ภ.ง.ด.90/91) under Revenue Code.
1447
+ *
1448
+ * Defaults to tax year 2026 and single filing status.
1449
+ *
1450
+ * @param input Taxpayer assessable income, allowances, filing status, and withholding.
1451
+ * @returns Fully typed, immutable TaxResult with statutory breakdown.
1452
+ *
1453
+ * @statute Thailand Revenue Code Sections 40, 42 bis, 47, 48(1)–(2).
1454
+ */
1455
+ function calculateThTax(input = {}) {
1456
+ return runThTaxPipeline(input);
1457
+ }
1458
+ // src/th/system.ts
1459
+ /**
1460
+ * Thailand Tax System implementation for 2026.
1461
+ *
1462
+ * Implements TaxSystem contract for `@trajectonomics/tax/th`.
1463
+ */
1464
+ const th = {
1465
+ jurisdiction: "TH",
1466
+ taxYear: 2026,
1467
+ currency: "THB",
1468
+ calculate(input = {}) {
1469
+ return calculateThTax(input);
1470
+ }
1471
+ };
1472
+ exports.TH_2026_PARAMETERS = TH_2026_PARAMETERS;
1473
+ exports.ThFinalWithholdingElection = ThFinalWithholdingElection;
1474
+ exports.ThRealEstateAcquisitionType = ThRealEstateAcquisitionType;
1475
+ exports.ThRealEstateExpenseBasis = ThRealEstateExpenseBasis;
1476
+ exports.ThSeparateSheetPaymentType = ThSeparateSheetPaymentType;
1477
+ exports.calculateThTax = calculateThTax;
1478
+ exports.th = th;
1479
+
1480
+ //# sourceMappingURL=index.cjs.map