Tax Development Skill
Implement multi-country tax engines in ERP systems using Claude Code.
Development-time skill for implementing multi-country tax engines (SST, GST, VAT) in ERP systems using Claude Code.
Version: 1.0.0
Status: ✅ Production Ready (Malaysia, Singapore), 🟡 Beta (Thailand)
License: MIT
CRITICAL: This is a development-time skill only — it helps Claude write correct tax code. It does NOT include runtime code that ships to production.
When to Use This Skill
Auto-trigger this skill when:
- Working on files in
src/lib/tax/orsrc/app/api/tax*/ - User mentions: "tax engine", "SST", "GST", "VAT", "withholding tax", "tax calculation"
- Implementing bill/invoice tax calculations
- Adding support for a new country (see guides/adding-sweden-vat.md)
Scope: Transactional Taxes Only
This skill covers sales-related taxes (SST, GST, VAT) calculated on invoices and bills.
In Scope: Malaysia SST, Singapore GST, Thailand VAT, Sweden MOMS, etc.
Out of Scope: Income tax, payroll tax, corporate tax - see @scope-income-tax.md for why.
Real-World Example: Adding Sweden VAT
"I need to add Sweden VAT support"
- Skill provides
@thailand-vat.mdas research template - Copy
@malaysia-engine.tspattern for Sweden engine - Use
@currency-rounding.md(SEK = half-up like MYR) - Cross-validate with node-sales-tax:
SalesTax.getSalesTax('SE')// 25% - Add tests using existing test pattern
- Generate confidence report
Time saved: ~5 hours (skill) vs ~7 hours (from scratch)
See guides/adding-sweden-vat.md for complete walkthrough.
Confidence Levels
Each reference file includes confidence ratings (🟢 A - Verified to ⚪ X - Unknown):
- 🟢 A - Verified: Multiple authoritative sources agree
- 🟡 B - High: Authoritative source, no contradictions
- 🟠 C - Medium: Single authoritative source or incomplete
- 🔴 D - Low: Conflicting or unofficial sources
- ⚪ X - Unknown: Placeholder, requires research
See @confidence-framework.md for detailed methodology.
Domain Overview
Tax Types by Country
| Country | Tax Type | Rate | Nature | Input Tax Recovery |
|---|---|---|---|---|
| Malaysia | SST (Service Tax) | 8% | Single-stage | No |
| Malaysia | SST (Sales Tax) | 5-10% | Single-stage | No |
| Malaysia | WHT | 0-15% | Withholding | N/A |
| Singapore | GST | 9% | Multi-stage VAT | Yes |
| Thailand | VAT | 7% | Multi-stage VAT | Yes |
Key Distinctions
SST (Malaysia) - Single-stage tax, charged once in supply chain:
- Service Tax: Tax on services provided by registered businesses
- Sales Tax: Tax on manufactured/imported goods
- No input tax recovery (tax is cost, not credit)
GST/VAT (Singapore, Thailand) - Multi-stage tax with credit mechanism:
- Tax charged at each stage of supply chain
- Businesses can recover input tax (tax paid on purchases)
- Net tax = Output tax - Input tax
WHT (Withholding Tax) - Tax deducted at source:
- Applied to payments to non-residents
- Rates vary by payment type (0-15%)
- See @malaysia-sst.md for detailed WHT rules
Core Patterns
TaxEngine Interface (Async Only)
All tax engines MUST implement this interface:
export interface TaxEngine {
countryCode: string;
// ALWAYS async — DB lookups for tax configs are inherently async
calculate(params: TaxCalculationParams): Promise<TaxCalculation[]>;
validateRegistration(taxId: string): boolean;
getApplicableTaxes(context: TaxContext): Promise<TaxConfig[]>;
// Optional: Generate tax filing summary
generateFiling?(periodId: string): Promise<{
outputTax: number;
inputTax: number;
netPayable: number;
}>;
}
CRITICAL: The calculate() method is ALWAYS async. Never create a sync variant. Tax configuration lookups from the database are inherently asynchronous.
TaxCalculationParams
export interface TaxCalculationParams {
amount: number; // Amount in local currency
taxType: string; // 'SST_SERVICE', 'GST', 'VAT', etc.
isTaxInclusive: boolean; // Is amount inclusive of tax?
transactionDate: Date; // CRITICAL: For effective date lookups
vendorId?: string; // For WHT determination
customerId?: string; // For reverse charge checks
itemCategory?: string; // For exempt/zero-rated checks
}
TaxCalculation Result
export interface TaxCalculation {
taxConfigId: string; // Reference to TaxConfig
taxType: string; // 'SST_SERVICE', 'GST', etc.
taxRate: number; // Rate at transaction time (snapshot)
taxableAmount: number; // Amount subject to tax
taxAmount: number; // Calculated tax
direction: 'INPUT' | 'OUTPUT'; // INPUT = tax paid, OUTPUT = tax collected
roundingAdjustment?: number; // Track rounding differences
}
Calculation Rules
Tax-Inclusive vs Tax-Exclusive
// Tax Exclusive: Amount does NOT include tax
// Formula: tax = amount × rate
// Total = amount + tax
// Tax Inclusive: Amount INCLUDES tax
// Formula: tax = amount - (amount / (1 + rate))
// Taxable = amount / (1 + rate)
function calculateTax(amount: number, rate: number, isInclusive: boolean): {
taxableAmount: number;
taxAmount: number;
} {
if (isInclusive) {
const taxableAmount = amount / (1 + rate);
const taxAmount = amount - taxableAmount;
return { taxableAmount, taxAmount };
} else {
const taxAmount = amount * rate;
return { taxableAmount: amount, taxAmount };
}
}
Effective Date Handling (CRITICAL)
Example: Malaysia SST rate changed from 6% to 8% in March 2024.
// ALWAYS filter by effective date
const taxConfig = await prisma.taxConfig.findFirst({
where: {
countryCode: 'MY',
taxType: 'SST_SERVICE',
isActive: true,
effectiveFrom: { lte: transactionDate },
OR: [
{ effectiveTo: null },
{ effectiveTo: { gt: transactionDate } }
]
},
orderBy: { effectiveFrom: 'desc' }
});
// Result:
// - Transaction Jan 2024: Rate = 6%
// - Transaction March 2024: Rate = 8%
Why this matters: Audits will check historical transactions used correct rates.
Rounding Rules
Rounding varies by country — see @currency-rounding.md:
- Malaysia (MYR): Round half-up to 2 decimal places
- Singapore (SGD): Round half-up to 2 decimal places
- Thailand (THB): Truncate to 2 decimal places
function roundTax(amount: number, countryCode: string): number {
switch (countryCode) {
case 'MY':
case 'SG':
return Math.round(amount * 100) / 100;
case 'TH':
return Math.trunc(amount * 100) / 100;
default:
return Math.round(amount * 100) / 100;
}
}
Safety Rules (CRITICAL)
-
NEVER hardcode rates
- ❌ Bad:
const tax = amount * 0.08 - ✅ Good: Fetch from TaxConfig with effective date filter
- ❌ Bad:
-
ALWAYS store snapshots
- TaxLine must store
taxRateat transaction time - Future rate changes must not affect historical transactions
- TaxLine must store
-
Effective dates matter
- Example: Malaysia SST was 6% pre-March 2024, 8% after
- Always filter by
transactionDatewhen looking up rates
-
Use async/await
- TaxEngine.calculate() always returns Promise
- No sync variants allowed
-
Rounding varies by country
- See @currency-rounding.md
- Wrong rounding = wrong tax amounts = compliance issues
-
Validate tax IDs
- Use country-specific validation in
validateRegistration() - Malaysia: SS-YYMM-NNNNNNNN format
- Singapore: Valid GST registration number
- Use country-specific validation in
Adding a New Country
Step-by-step guide for adding tax support for a new country:
Step 1: Research Tax Framework
Create a reference file at references/{country}-{tax}.md:
# {Country} {Tax}
## Framework
- Legal basis
- Tax authority
- Current rate(s)
## Registration
- Threshold
- Tax ID format
- Validation rules
## Filing
- Form name
- Frequency
- Due dates
## Special Rules
- Exemptions
- Zero-rated items
- Reverse charge
- WHT if applicable
See @thailand-vat.md for a skeleton template.
Step 2: Define TaxConfig Records
Add seed data for the country:
await prisma.taxConfig.create({
data: {
countryCode: 'TH',
taxType: 'VAT',
rate: 0.07,
name: 'Thailand VAT',
effectiveFrom: new Date('1992-01-01'),
isActive: true,
// GL account mappings
outputTaxAccountId: '...',
inputTaxAccountId: '...',
}
});
Step 3: Implement TaxEngine
Create engine following the pattern in examples/malaysia-engine.ts:
export class ThailandVATEngine implements TaxEngine {
countryCode = 'TH';
async calculate(params: TaxCalculationParams): Promise<TaxCalculation[]> {
// 1. Fetch applicable tax config with effective date
// 2. Validate if transaction is taxable
// 3. Calculate tax with correct rounding
// 4. Return TaxCalculation array
}
validateRegistration(taxId: string): boolean {
// Validate Thai VAT ID format
}
async getApplicableTaxes(context: TaxContext): Promise<TaxConfig[]> {
// Return active VAT configs for Thailand
}
}
Step 4: Register the Engine
Add to the tax engine registry:
import { ThailandVATEngine } from './engines/thailand-vat';
taxRegistry.register(new ThailandVATEngine());
Step 5: Add Test Cases
Add validation test cases to scripts/validate-tax-calc.ts:
{
name: 'Thailand VAT 7% on 1000 THB',
country: 'TH',
taxType: 'VAT',
amount: 1000,
expectedTax: 70,
expectedTotal: 1070,
}
Step 6: Test End-to-End
- Create a test bill/invoice with the new country's tax
- Verify TaxLine is created with correct amounts
- Verify GL entries are correct
- Run validation script
Testing Tax Calculations
Using the Validation Script
# Run all test cases
npx ts-node .claude/skills/tax-development/scripts/validate-tax-calc.ts
# Run specific test case
npx ts-node .claude/skills/tax-development/scripts/validate-tax-calc.ts --case="Malaysia SST 8%"
# Test custom scenario
npx ts-node .claude/skills/tax-development/scripts/validate-tax-calc.ts \
--country=MY \
--taxType=SST_SERVICE \
--amount=1000 \
--rate=0.08
Unit Test Patterns
describe('MalaysiaSSTEngine', () => {
it('should calculate 8% SST on service amount', async () => {
const engine = new MalaysiaSSTEngine();
const result = await engine.calculate({
amount: 1000,
taxType: 'SST_SERVICE',
isTaxInclusive: false,
transactionDate: new Date('2024-06-01'), // Post-rate change
});
expect(result[0].taxRate).toBe(0.08);
expect(result[0].taxAmount).toBe(80);
expect(result[0].taxableAmount).toBe(1000);
});
it('should use 6% rate for pre-March 2024 transactions', async () => {
const result = await engine.calculate({
amount: 1000,
taxType: 'SST_SERVICE',
isTaxInclusive: false,
transactionDate: new Date('2024-01-15'), // Pre-rate change
});
expect(result[0].taxRate).toBe(0.06);
expect(result[0].taxAmount).toBe(60);
});
});
References
- @malaysia-sst.md — Malaysia SST detailed rules (🟢 A - Verified)
- @singapore-gst.md — Singapore GST detailed rules (🟢 A - Verified)
- @thailand-vat.md — Thailand VAT skeleton (🟠 C - Incomplete)
- @tax-engine-patterns.md — Architecture patterns
- @currency-rounding.md — Rounding rules by country
- @confidence-framework.md — Source trustworthiness & confidence ratings
- @third-party-apis.md — External tax API services (Avalara, Stripe Tax, etc.)
- @free-tax-apis.md — Free and low-cost alternatives ($0-$9/month options)
- @github-tax-repositories.md — Open-source libraries (node-sales-tax, etc.)
- @scope-income-tax.md — Why income tax is out of scope
Guides
- guides/adding-sweden-vat.md — Example: Adding a new country (Sweden VAT)
Examples
- examples/types.ts — TaxEngine interface definitions
- examples/registry.ts — Tax engine registry pattern
- examples/malaysia-engine.ts — Complete Malaysia implementation
Scripts
- scripts/validate-tax-calc.ts — Tax calculation validation tool