feat: simplify header — remove Book a Demo, consolidate to 4 clear na… #3865
9 changed files+2266−9
Addedapp/(platform)/data-import/page.tsx+1084−0View fileUnifiedSplit
Large file (1,084 lines). Load full file
Modifiedapp/(platform)/layout.tsx+13−0View fileUnifiedSplit
@@ -242,6 +242,18 @@ function IconSettings({ className }: { className?: string }) {
242242 );
243243}
244244
245function IconMigration({ className }: { className?: string }) {
246 return (
247 <svg viewBox="0 0 24 24" fill="none" className={className} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
248 <ellipse cx="12" cy="5" rx="9" ry="3" />
249 <path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" />
250 <path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" />
251 <path d="M15 13l2 2-2 2" />
252 <path d="M9 13l-2 2 2 2" />
253 </svg>
254 );
255}
256
245257function IconAdmin({ className }: { className?: string }) {
246258 return (
247259 <svg viewBox="0 0 24 24" fill="none" className={className} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
@@ -360,6 +372,7 @@ const NAV_GROUPS: NavGroup[] = [
360372 heading: "Settings",
361373 items: [
362374 { label: "Settings", href: "/settings", icon: IconSettings },
375 { label: "Data Migration", href: "/data-import", icon: IconMigration },
363376 { label: "Admin", href: "/admin", icon: IconAdmin, adminOnly: true },
364377 ],
365378 },
Modifiedlib/oracle/engine.ts+44−9View fileUnifiedSplit
@@ -9,6 +9,21 @@ import {
99} from "./types";
1010import { AI_DISCLAIMER } from "@/lib/constants";
1111
12/**
13 * RAG Enforcement Configuration.
14 *
15 * Strict mode: Claude must ONLY cite from retrieval context.
16 * No base-model citations allowed. Every citation must have
17 * source attribution. Unverified citations get warnings.
18 */
19const RAG_ENFORCEMENT = {
20 mode: "strict",
21 allowBaseModelCitations: false,
22 requireSourceAttribution: true,
23 unverifiedWarning: "⚠ This citation could not be verified against our authoritative sources. Do not rely on it without independent verification.",
24 noDataSourceWarning: "Marco Reid does not yet have a verified data source for this jurisdiction. All citations should be independently verified.",
25} as const;
26
1227/**
1328 * The Marco Engine \u2014 the brain of Marco Reid.
1429 *
@@ -86,28 +101,48 @@ CRITICAL RULES:
861013. NEVER fabricate a citation. If you are not certain a case exists, say so explicitly.
871024. Always specify the jurisdiction.
881035. Provide practical analysis, not just citations.
896. You assist professionals \u2014 you do not provide legal or tax advice.`;
1046. You assist professionals \u2014 you do not provide legal or tax advice.
105
106STRICT RETRIEVAL-AUGMENTED GENERATION RULES:
1071. You MUST ONLY cite cases, statutes, and regulations that you can directly attribute to a verified source in the context provided.
1082. If no relevant source material is available in the retrieval context, say "I could not find verified source material for this query. Please verify independently."
1093. NEVER generate a citation from your training data alone. Every citation MUST come from the retrieval context or be explicitly marked as UNVERIFIED.
1104. If you are unsure whether a case exists, mark it as UNVERIFIED \u2014 never guess.
1115. Prefix every citation with its source database (e.g., "[CourtListener]", "[BAILII]", "[CanLII]", "[IRS.gov]").
1126. If the user asks about a jurisdiction where we have no data source configured, say "Marco Reid does not yet have a verified data source for [jurisdiction]. Please verify all citations independently."`;
90113
91114 const jurisdictionNote = jurisdiction
92115 ? `\nFocus on ${jurisdiction} law and regulations unless the query specifies otherwise.`
93116 : "";
94117
118 const ragEnforcementBlock = `
119
120RAG ENFORCEMENT MODE: ${RAG_ENFORCEMENT.mode.toUpperCase()}
121- Base model citations allowed: ${RAG_ENFORCEMENT.allowBaseModelCitations ? "YES" : "NO"}
122- Source attribution required: ${RAG_ENFORCEMENT.requireSourceAttribution ? "YES" : "NO"}
123- Any citation without a verified source MUST include the warning: "${RAG_ENFORCEMENT.unverifiedWarning}"
124- For unsupported jurisdictions, include: "${RAG_ENFORCEMENT.noDataSourceWarning}"`;
125
95126 const domainPrompts: Record<OracleDomain, string> = {
96127 LEGAL: `${base}
97${jurisdictionNote}
98You specialise in legal research: case law, statutes, regulations, court rules, and legal analysis. You have knowledge of US federal and state law, NZ law, Australian law, and UK law. For every case you cite, provide the full citation including volume, reporter, and page number.`,
128${jurisdictionNote}${ragEnforcementBlock}
129
130You specialise in legal research: case law, statutes, regulations, court rules, and legal analysis. You have knowledge of US federal and state law, NZ law, Australian law, and UK law. For every case you cite, provide the full citation including volume, reporter, and page number. Prefix every citation with its source database. Only cite from the retrieval context provided.`,
99131
100132 ACCOUNTING: `${base}
101${jurisdictionNote}
102You specialise in accounting and tax research: IRS code sections, revenue rulings, Treasury regulations, GAAP standards, IFRS standards, state tax codes, and tax court decisions. For every tax provision, cite the specific IRC section, regulation, or ruling.`,
133${jurisdictionNote}${ragEnforcementBlock}
134
135You specialise in accounting and tax research: IRS code sections, revenue rulings, Treasury regulations, GAAP standards, IFRS standards, state tax codes, and tax court decisions. For every tax provision, cite the specific IRC section, regulation, or ruling. Prefix every citation with its source database. Only cite from the retrieval context provided.`,
103136
104137 CROSS_DOMAIN: `${base}
105${jurisdictionNote}
106You specialise in questions that span BOTH legal and accounting/tax domains simultaneously. This is your unique capability \u2014 no other research tool can answer questions that require both legal analysis and tax/accounting intelligence. Provide analysis from both perspectives and cite both legal and tax authorities.`,
138${jurisdictionNote}${ragEnforcementBlock}
139
140You specialise in questions that span BOTH legal and accounting/tax domains simultaneously. This is your unique capability \u2014 no other research tool can answer questions that require both legal analysis and tax/accounting intelligence. Provide analysis from both perspectives and cite both legal and tax authorities. Prefix every citation with its source database. Only cite from the retrieval context provided.`,
107141
108142 IP: `${base}
109${jurisdictionNote}
110You specialise in intellectual property law: patents, trademarks, copyright, trade secrets, and licensing. You can search USPTO records, analyse patent claims, assess trademark likelihood of confusion, and research IP case law. For patents, cite the patent number and key claims. For trademarks, cite the registration number and class.`,
143${jurisdictionNote}${ragEnforcementBlock}
144
145You specialise in intellectual property law: patents, trademarks, copyright, trade secrets, and licensing. You can search USPTO records, analyse patent claims, assess trademark likelihood of confusion, and research IP case law. For patents, cite the patent number and key claims. For trademarks, cite the registration number and class. Prefix every citation with its source database. Only cite from the retrieval context provided.`,
111146 };
112147
113148 return domainPrompts[domain];
Addedlib/tax/au.ts+165−0View fileUnifiedSplit
@@ -0,0 +1,165 @@
1// Australia Tax & Payroll Module
2// Isolated per-country module — no imports from other country files.
3// Income tax brackets, Medicare levy, Superannuation 11.5%, GST 10%.
4
5export const LAST_UPDATED = "2025-01-01";
6export const TAX_YEAR = "2024-2025";
7export const JURISDICTION = "AU";
8export const CURRENCY = { code: "AUD", symbol: "$" };
9
10// ---------------------------------------------------------------------------
11// Types
12// ---------------------------------------------------------------------------
13
14export interface AUPayrollResult {
15 grossPay: number;
16 incomeTax: number;
17 employeeDeductions: { name: string; amount: number }[];
18 employerCosts: { name: string; amount: number }[];
19 netPay: number;
20 totalEmployerCost: number;
21}
22
23export type PayFrequency = "weekly" | "fortnightly" | "monthly";
24
25// ---------------------------------------------------------------------------
26// Rate Constants
27// ---------------------------------------------------------------------------
28
29export const TAX_BRACKETS = [
30 { min: 0, max: 18_200, rate: 0 },
31 { min: 18_200, max: 45_000, rate: 0.16 },
32 { min: 45_000, max: 135_000, rate: 0.30 },
33 { min: 135_000, max: 190_000, rate: 0.37 },
34 { min: 190_000, max: Infinity, rate: 0.45 },
35] as const;
36
37export const MEDICARE_LEVY = 0.02;
38export const SUPER_RATE = 0.115; // 11.5% for 2024-25
39export const GST_RATE = 0.10;
40
41export const CORPORATE_TAX_RATE = 0.30;
42export const CORPORATE_TAX_RATE_SMALL_BUSINESS = 0.25; // Base rate entity: turnover < $50M
43
44export const INCOME_TAX_BRACKETS = [
45 { min: 0, max: 18_200, rate: 0, label: "$0 - $18,200 (tax-free)" },
46 { min: 18_200, max: 45_000, rate: 0.16, label: "$18,201 - $45,000" },
47 { min: 45_000, max: 135_000, rate: 0.30, label: "$45,001 - $135,000" },
48 { min: 135_000, max: 190_000, rate: 0.37, label: "$135,001 - $190,000" },
49 { min: 190_000, max: Infinity, rate: 0.45, label: "$190,001+" },
50] as const;
51
52// ---------------------------------------------------------------------------
53// Helpers (self-contained — no shared imports)
54// ---------------------------------------------------------------------------
55
56function periodsPerYear(freq: PayFrequency): number {
57 switch (freq) {
58 case "weekly":
59 return 52;
60 case "fortnightly":
61 return 26;
62 case "monthly":
63 return 12;
64 }
65}
66
67function applyBrackets(
68 annual: number,
69 brackets: ReadonlyArray<{ min: number; max: number; rate: number }>,
70): number {
71 let tax = 0;
72 for (const b of brackets) {
73 if (annual <= b.min) break;
74 const taxable = Math.min(annual, b.max) - b.min;
75 tax += taxable * b.rate;
76 }
77 return tax;
78}
79
80function round2(n: number): number {
81 return Math.round(n * 100) / 100;
82}
83
84// ---------------------------------------------------------------------------
85// Payroll Calculator
86// ---------------------------------------------------------------------------
87
88export function calculatePayroll(
89 annualSalary: number,
90 frequency: PayFrequency,
91): AUPayrollResult {
92 const periods = periodsPerYear(frequency);
93
94 const annualIncomeTax = applyBrackets(annualSalary, TAX_BRACKETS);
95 const annualMedicare = annualSalary * MEDICARE_LEVY;
96 const annualSuper = annualSalary * SUPER_RATE;
97
98 const grossPay = round2(annualSalary / periods);
99 const incomeTax = round2(annualIncomeTax / periods);
100
101 const employeeDeductions = [
102 { name: "Income tax", amount: round2(annualIncomeTax / periods) },
103 { name: "Medicare levy (2%)", amount: round2(annualMedicare / periods) },
104 ];
105
106 const employerCosts = [
107 { name: "Superannuation (11.5%)", amount: round2(annualSuper / periods) },
108 ];
109
110 const totalDeductions = employeeDeductions.reduce((s, d) => s + d.amount, 0);
111 const netPay = round2(grossPay - totalDeductions);
112 const totalEmployerCost = round2(grossPay + employerCosts.reduce((s, c) => s + c.amount, 0));
113
114 return { grossPay, incomeTax, employeeDeductions, employerCosts, netPay, totalEmployerCost };
115}
116
117// ---------------------------------------------------------------------------
118// GST Calculator
119// ---------------------------------------------------------------------------
120
121export function calculateGST(
122 amount: number,
123 inclusive: boolean,
124): { gross: number; tax: number; net: number; rate: number } {
125 if (inclusive) {
126 const gross = round2(amount);
127 const tax = round2(amount - amount / (1 + GST_RATE));
128 const net = round2(amount - tax);
129 return { gross, tax, net, rate: GST_RATE };
130 }
131 const net = round2(amount);
132 const tax = round2(amount * GST_RATE);
133 const gross = round2(amount + tax);
134 return { gross, tax, net, rate: GST_RATE };
135}
136
137// ---------------------------------------------------------------------------
138// Income Tax Calculator
139// ---------------------------------------------------------------------------
140
141export function calculateIncomeTax(
142 income: number,
143): { tax: number; effectiveRate: number; breakdown: { bracket: string; rate: number; tax: number }[] } {
144 const breakdown: { bracket: string; rate: number; tax: number }[] = [];
145 let totalTax = 0;
146
147 for (const b of INCOME_TAX_BRACKETS) {
148 if (income <= b.min) break;
149 const taxable = Math.min(income, b.max) - b.min;
150 const tax = round2(taxable * b.rate);
151 totalTax += tax;
152 if (tax > 0 || b.rate === 0) {
153 breakdown.push({ bracket: b.label, rate: b.rate, tax });
154 }
155 }
156
157 // Medicare levy on top
158 const medicareTax = round2(income * MEDICARE_LEVY);
159 totalTax = round2(totalTax + medicareTax);
160 breakdown.push({ bracket: "Medicare levy (2%)", rate: MEDICARE_LEVY, tax: medicareTax });
161
162 const effectiveRate = income > 0 ? round2((totalTax / income) * 10000) / 10000 : 0;
163
164 return { tax: totalTax, effectiveRate, breakdown };
165}
Addedlib/tax/ca.ts+221−0View fileUnifiedSplit
@@ -0,0 +1,221 @@
1// Canada Tax & Payroll Module
2// Isolated per-country module — no imports from other country files.
3// Federal brackets, CPP, EI, provincial GST/HST/PST rates.
4
5export const LAST_UPDATED = "2025-01-01";
6export const TAX_YEAR = "2024-2025";
7export const JURISDICTION = "CA";
8export const CURRENCY = { code: "CAD", symbol: "$" };
9
10// ---------------------------------------------------------------------------
11// Types
12// ---------------------------------------------------------------------------
13
14export interface CAPayrollResult {
15 grossPay: number;
16 incomeTax: number;
17 employeeDeductions: { name: string; amount: number }[];
18 employerCosts: { name: string; amount: number }[];
19 netPay: number;
20 totalEmployerCost: number;
21}
22
23export type PayFrequency = "weekly" | "fortnightly" | "monthly";
24
25// ---------------------------------------------------------------------------
26// Rate Constants
27// ---------------------------------------------------------------------------
28
29export const FEDERAL_BRACKETS = [
30 { min: 0, max: 55_867, rate: 0.15 },
31 { min: 55_867, max: 111_733, rate: 0.205 },
32 { min: 111_733, max: 154_906, rate: 0.26 },
33 { min: 154_906, max: 220_000, rate: 0.29 },
34 { min: 220_000, max: Infinity, rate: 0.33 },
35] as const;
36
37export const BASIC_PERSONAL_AMOUNT = 15_705; // 2024 federal BPA
38export const CPP_RATE = 0.0595;
39export const CPP_BASIC_EXEMPTION = 3_500;
40export const CPP_MAX_PENSIONABLE = 68_500;
41export const EI_EMPLOYEE_RATE = 0.0166;
42export const EI_MAX_INSURABLE = 63_200;
43export const EI_EMPLOYER_MULTIPLIER = 1.4;
44
45export const FEDERAL_GST_RATE = 0.05;
46
47// Corporate tax
48export const CORPORATE_TAX_FEDERAL_GENERAL = 0.15;
49export const CORPORATE_TAX_FEDERAL_SMALL_BUSINESS = 0.09;
50export const CORPORATE_TAX_AVG_PROVINCIAL_GENERAL = 0.115;
51export const CORPORATE_TAX_AVG_PROVINCIAL_SMALL = 0.025;
52
53export const INCOME_TAX_BRACKETS = [
54 { min: 0, max: 55_867, rate: 0.15, label: "$0 - $55,867" },
55 { min: 55_867, max: 111_733, rate: 0.205, label: "$55,868 - $111,733" },
56 { min: 111_733, max: 154_906, rate: 0.26, label: "$111,734 - $154,906" },
57 { min: 154_906, max: 220_000, rate: 0.29, label: "$154,907 - $220,000" },
58 { min: 220_000, max: Infinity, rate: 0.33, label: "$220,001+" },
59] as const;
60
61// Provincial tax table (GST/HST/PST)
62export const PROVINCIAL_TAX: Record<
63 string,
64 { province: string; gstRate: number; pstRate: number; combinedRate: number; type: "HST" | "GST+PST" | "GST+QST" | "GST only" }
65> = {
66 AB: { province: "Alberta", gstRate: 5, pstRate: 0, combinedRate: 5, type: "GST only" },
67 BC: { province: "British Columbia", gstRate: 5, pstRate: 7, combinedRate: 12, type: "GST+PST" },
68 MB: { province: "Manitoba", gstRate: 5, pstRate: 7, combinedRate: 12, type: "GST+PST" },
69 NB: { province: "New Brunswick", gstRate: 5, pstRate: 10, combinedRate: 15, type: "HST" },
70 NL: { province: "Newfoundland and Labrador", gstRate: 5, pstRate: 10, combinedRate: 15, type: "HST" },
71 NS: { province: "Nova Scotia", gstRate: 5, pstRate: 10, combinedRate: 15, type: "HST" },
72 NT: { province: "Northwest Territories", gstRate: 5, pstRate: 0, combinedRate: 5, type: "GST only" },
73 NU: { province: "Nunavut", gstRate: 5, pstRate: 0, combinedRate: 5, type: "GST only" },
74 ON: { province: "Ontario", gstRate: 5, pstRate: 8, combinedRate: 13, type: "HST" },
75 PE: { province: "Prince Edward Island", gstRate: 5, pstRate: 10, combinedRate: 15, type: "HST" },
76 QC: { province: "Quebec", gstRate: 5, pstRate: 9.975, combinedRate: 14.975, type: "GST+QST" },
77 SK: { province: "Saskatchewan", gstRate: 5, pstRate: 6, combinedRate: 11, type: "GST+PST" },
78 YT: { province: "Yukon", gstRate: 5, pstRate: 0, combinedRate: 5, type: "GST only" },
79};
80
81// ---------------------------------------------------------------------------
82// Helpers (self-contained — no shared imports)
83// ---------------------------------------------------------------------------
84
85function periodsPerYear(freq: PayFrequency): number {
86 switch (freq) {
87 case "weekly":
88 return 52;
89 case "fortnightly":
90 return 26;
91 case "monthly":
92 return 12;
93 }
94}
95
96function applyBrackets(
97 annual: number,
98 brackets: ReadonlyArray<{ min: number; max: number; rate: number }>,
99): number {
100 let tax = 0;
101 for (const b of brackets) {
102 if (annual <= b.min) break;
103 const taxable = Math.min(annual, b.max) - b.min;
104 tax += taxable * b.rate;
105 }
106 return tax;
107}
108
109function round2(n: number): number {
110 return Math.round(n * 100) / 100;
111}
112
113// ---------------------------------------------------------------------------
114// Payroll Calculator
115// ---------------------------------------------------------------------------
116
117export function calculatePayroll(
118 annualSalary: number,
119 frequency: PayFrequency,
120): CAPayrollResult {
121 const periods = periodsPerYear(frequency);
122
123 // Federal income tax (with basic personal amount credit)
124 const annualFederalTax = Math.max(
125 0,
126 applyBrackets(annualSalary, FEDERAL_BRACKETS) - BASIC_PERSONAL_AMOUNT * 0.15,
127 );
128
129 // CPP (employee)
130 const cppEarnings = Math.min(annualSalary, CPP_MAX_PENSIONABLE) - CPP_BASIC_EXEMPTION;
131 const annualCPPEmployee = Math.max(0, cppEarnings) * CPP_RATE;
132
133 // CPP (employer) - same rate
134 const annualCPPEmployer = annualCPPEmployee;
135
136 // EI (employee)
137 const eiEarnings = Math.min(annualSalary, EI_MAX_INSURABLE);
138 const annualEIEmployee = eiEarnings * EI_EMPLOYEE_RATE;
139
140 // EI (employer) = 1.4x employee
141 const annualEIEmployer = annualEIEmployee * EI_EMPLOYER_MULTIPLIER;
142
143 const grossPay = round2(annualSalary / periods);
144 const incomeTax = round2(annualFederalTax / periods);
145
146 const employeeDeductions = [
147 { name: "Federal income tax", amount: round2(annualFederalTax / periods) },
148 { name: "CPP (5.95%)", amount: round2(annualCPPEmployee / periods) },
149 { name: "EI (1.66%)", amount: round2(annualEIEmployee / periods) },
150 ];
151
152 const employerCosts = [
153 { name: "CPP employer (5.95%)", amount: round2(annualCPPEmployer / periods) },
154 { name: "EI employer (2.324%)", amount: round2(annualEIEmployer / periods) },
155 ];
156
157 const totalDeductions = employeeDeductions.reduce((s, d) => s + d.amount, 0);
158 const netPay = round2(grossPay - totalDeductions);
159 const totalEmployerCost = round2(grossPay + employerCosts.reduce((s, c) => s + c.amount, 0));
160
161 return { grossPay, incomeTax, employeeDeductions, employerCosts, netPay, totalEmployerCost };
162}
163
164// ---------------------------------------------------------------------------
165// GST Calculator (federal GST or provincial combined rate)
166// ---------------------------------------------------------------------------
167
168export function calculateGST(
169 amount: number,
170 inclusive: boolean,
171 provinceCode?: string,
172): { gross: number; tax: number; net: number; rate: number } {
173 const rate = provinceCode
174 ? (PROVINCIAL_TAX[provinceCode.toUpperCase()]?.combinedRate ?? FEDERAL_GST_RATE * 100) / 100
175 : FEDERAL_GST_RATE;
176
177 if (inclusive) {
178 const gross = round2(amount);
179 const tax = round2(amount - amount / (1 + rate));
180 const net = round2(amount - tax);
181 return { gross, tax, net, rate };
182 }
183 const net = round2(amount);
184 const tax = round2(amount * rate);
185 const gross = round2(amount + tax);
186 return { gross, tax, net, rate };
187}
188
189// ---------------------------------------------------------------------------
190// Income Tax Calculator
191// ---------------------------------------------------------------------------
192
193export function calculateIncomeTax(
194 income: number,
195): { tax: number; effectiveRate: number; breakdown: { bracket: string; rate: number; tax: number }[] } {
196 const breakdown: { bracket: string; rate: number; tax: number }[] = [];
197 let totalTax = 0;
198
199 for (const b of INCOME_TAX_BRACKETS) {
200 if (income <= b.min) break;
201 const taxable = Math.min(income, b.max) - b.min;
202 const tax = round2(taxable * b.rate);
203 totalTax += tax;
204 if (tax > 0) {
205 breakdown.push({ bracket: b.label, rate: b.rate, tax });
206 }
207 }
208
209 // Basic Personal Amount credit
210 const bpaCredit = round2(BASIC_PERSONAL_AMOUNT * 0.15);
211 totalTax = round2(Math.max(0, totalTax - bpaCredit));
212 breakdown.push({
213 bracket: `Basic Personal Amount credit (-$${bpaCredit.toLocaleString()})`,
214 rate: -0.15,
215 tax: -bpaCredit,
216 });
217
218 const effectiveRate = income > 0 ? round2((totalTax / income) * 10000) / 10000 : 0;
219
220 return { tax: totalTax, effectiveRate, breakdown };
221}
Addedlib/tax/index.ts+40−0View fileUnifiedSplit
@@ -0,0 +1,40 @@
1// Tax Module Registry
2// Central index for all per-country tax modules.
3// Each country module is completely isolated — no cross-imports between countries.
4
5import * as nz from "./nz";
6import * as au from "./au";
7import * as uk from "./uk";
8import * as us from "./us";
9import * as ca from "./ca";
10
11export const TAX_MODULES = { nz, au, uk, us, ca } as const;
12
13export type CountryCode = keyof typeof TAX_MODULES;
14
15/**
16 * Get the tax module for a specific country.
17 * Returns undefined if the country code is not supported.
18 */
19export function getCountryTax(code: string) {
20 return TAX_MODULES[code.toLowerCase() as keyof typeof TAX_MODULES];
21}
22
23/**
24 * All supported country codes.
25 */
26export const SUPPORTED_COUNTRIES: CountryCode[] = ["nz", "au", "uk", "us", "ca"];
27
28/**
29 * Country display metadata for UI rendering.
30 */
31export const COUNTRY_INFO: Record<CountryCode, { name: string; currency: string; symbol: string; taxLabel: string }> = {
32 nz: { name: "New Zealand", currency: "NZD", symbol: "$", taxLabel: "GST" },
33 au: { name: "Australia", currency: "AUD", symbol: "$", taxLabel: "GST" },
34 uk: { name: "United Kingdom", currency: "GBP", symbol: "£", taxLabel: "VAT" },
35 us: { name: "United States", currency: "USD", symbol: "$", taxLabel: "Sales Tax" },
36 ca: { name: "Canada", currency: "CAD", symbol: "$", taxLabel: "GST/HST" },
37};
38
39// Re-export individual modules for direct access
40export { nz, au, uk, us, ca };
Addedlib/tax/nz.ts+194−0View fileUnifiedSplit
@@ -0,0 +1,194 @@
1// New Zealand Tax & Payroll Module
2// Isolated per-country module — no imports from other country files.
3// PAYE, ACC levy, KiwiSaver, GST 15%.
4
5export const LAST_UPDATED = "2025-01-01";
6export const TAX_YEAR = "2024-2025";
7export const JURISDICTION = "NZ";
8export const CURRENCY = { code: "NZD", symbol: "$" };
9
10// ---------------------------------------------------------------------------
11// Types
12// ---------------------------------------------------------------------------
13
14export interface NZPayrollResult {
15 grossPay: number;
16 incomeTax: number;
17 employeeDeductions: { name: string; amount: number }[];
18 employerCosts: { name: string; amount: number }[];
19 netPay: number;
20 totalEmployerCost: number;
21}
22
23export type PayFrequency = "weekly" | "fortnightly" | "monthly";
24
25export interface NZPayrollOptions {
26 kiwiSaverRate?: number; // 3 | 4 | 6 | 8 | 10
27}
28
29// ---------------------------------------------------------------------------
30// Rate Constants
31// ---------------------------------------------------------------------------
32
33export const PAYE_BRACKETS = [
34 { min: 0, max: 14_000, rate: 0.105 },
35 { min: 14_000, max: 48_000, rate: 0.175 },
36 { min: 48_000, max: 70_000, rate: 0.30 },
37 { min: 70_000, max: 180_000, rate: 0.33 },
38 { min: 180_000, max: Infinity, rate: 0.39 },
39] as const;
40
41export const ACC_LEVY_RATE = 0.016;
42export const ACC_LEVY_CAP = 142_283;
43export const KIWISAVER_EMPLOYER_MIN = 0.03;
44export const KIWISAVER_RATES = [3, 4, 6, 8, 10] as const;
45export const GST_RATE = 0.15;
46export const CORPORATE_TAX_RATE = 0.28;
47
48export const INCOME_TAX_BRACKETS = [
49 { min: 0, max: 14_000, rate: 0.105, label: "$0 - $14,000" },
50 { min: 14_000, max: 48_000, rate: 0.175, label: "$14,001 - $48,000" },
51 { min: 48_000, max: 70_000, rate: 0.30, label: "$48,001 - $70,000" },
52 { min: 70_000, max: 180_000, rate: 0.33, label: "$70,001 - $180,000" },
53 { min: 180_000, max: Infinity, rate: 0.39, label: "$180,001+" },
54] as const;
55
56// ---------------------------------------------------------------------------
57// Helpers (self-contained — no shared imports)
58// ---------------------------------------------------------------------------
59
60function periodsPerYear(freq: PayFrequency): number {
61 switch (freq) {
62 case "weekly":
63 return 52;
64 case "fortnightly":
65 return 26;
66 case "monthly":
67 return 12;
68 }
69}
70
71function applyBrackets(
72 annual: number,
73 brackets: ReadonlyArray<{ min: number; max: number; rate: number }>,
74): number {
75 let tax = 0;
76 for (const b of brackets) {
77 if (annual <= b.min) break;
78 const taxable = Math.min(annual, b.max) - b.min;
79 tax += taxable * b.rate;
80 }
81 return tax;
82}
83
84function round2(n: number): number {
85 return Math.round(n * 100) / 100;
86}
87
88// ---------------------------------------------------------------------------
89// ESCT rate (Employer Superannuation Contribution Tax)
90// ---------------------------------------------------------------------------
91
92function esctRate(annualSalary: number): number {
93 if (annualSalary <= 16_800) return 0.105;
94 if (annualSalary <= 57_600) return 0.175;
95 if (annualSalary <= 84_000) return 0.30;
96 if (annualSalary <= 216_000) return 0.33;
97 return 0.39;
98}
99
100// ---------------------------------------------------------------------------
101// Payroll Calculator
102// ---------------------------------------------------------------------------
103
104export function calculatePayroll(
105 annualSalary: number,
106 frequency: PayFrequency,
107 options?: NZPayrollOptions,
108): NZPayrollResult {
109 const periods = periodsPerYear(frequency);
110 const kiwiSaverRate = ((options?.kiwiSaverRate ?? 3) / 100);
111
112 // PAYE
113 const annualPAYE = applyBrackets(annualSalary, PAYE_BRACKETS);
114
115 // ACC earner levy
116 const accEarnings = Math.min(annualSalary, ACC_LEVY_CAP);
117 const annualACC = accEarnings * ACC_LEVY_RATE;
118
119 // KiwiSaver employee
120 const annualKSEmployee = annualSalary * kiwiSaverRate;
121
122 // KiwiSaver employer
123 const annualKSEmployer = annualSalary * KIWISAVER_EMPLOYER_MIN;
124
125 // ESCT on employer KiwiSaver contribution
126 const esct = esctRate(annualSalary);
127 const annualESCT = annualKSEmployer * esct;
128
129 const grossPay = round2(annualSalary / periods);
130 const incomeTax = round2(annualPAYE / periods);
131
132 const employeeDeductions = [
133 { name: "PAYE", amount: round2(annualPAYE / periods) },
134 { name: "ACC earner levy", amount: round2(annualACC / periods) },
135 { name: `KiwiSaver (${options?.kiwiSaverRate ?? 3}%)`, amount: round2(annualKSEmployee / periods) },
136 ];
137
138 const employerCosts = [
139 { name: "KiwiSaver employer (3%)", amount: round2(annualKSEmployer / periods) },
140 { name: `ESCT (${(esct * 100).toFixed(1)}%)`, amount: round2(annualESCT / periods) },
141 ];
142
143 const totalDeductions = employeeDeductions.reduce((s, d) => s + d.amount, 0);
144 const netPay = round2(grossPay - totalDeductions);
145 const totalEmployerCost = round2(grossPay + employerCosts.reduce((s, c) => s + c.amount, 0));
146
147 return { grossPay, incomeTax, employeeDeductions, employerCosts, netPay, totalEmployerCost };
148}
149
150// ---------------------------------------------------------------------------
151// GST Calculator
152// ---------------------------------------------------------------------------
153
154export function calculateGST(
155 amount: number,
156 inclusive: boolean,
157): { gross: number; tax: number; net: number; rate: number } {
158 if (inclusive) {
159 const gross = round2(amount);
160 const tax = round2(amount - amount / (1 + GST_RATE));
161 const net = round2(amount - tax);
162 return { gross, tax, net, rate: GST_RATE };
163 }
164 const net = round2(amount);
165 const tax = round2(amount * GST_RATE);
166 const gross = round2(amount + tax);
167 return { gross, tax, net, rate: GST_RATE };
168}
169
170// ---------------------------------------------------------------------------
171// Income Tax Calculator
172// ---------------------------------------------------------------------------
173
174export function calculateIncomeTax(
175 income: number,
176): { tax: number; effectiveRate: number; breakdown: { bracket: string; rate: number; tax: number }[] } {
177 const breakdown: { bracket: string; rate: number; tax: number }[] = [];
178 let totalTax = 0;
179
180 for (const b of INCOME_TAX_BRACKETS) {
181 if (income <= b.min) break;
182 const taxable = Math.min(income, b.max) - b.min;
183 const tax = round2(taxable * b.rate);
184 totalTax += tax;
185 if (tax > 0) {
186 breakdown.push({ bracket: b.label, rate: b.rate, tax });
187 }
188 }
189
190 totalTax = round2(totalTax);
191 const effectiveRate = income > 0 ? round2((totalTax / income) * 10000) / 10000 : 0;
192
193 return { tax: totalTax, effectiveRate, breakdown };
194}
Addedlib/tax/uk.ts+189−0View fileUnifiedSplit
@@ -0,0 +1,189 @@
1// United Kingdom Tax & Payroll Module
2// Isolated per-country module — no imports from other country files.
3// Income tax + personal allowance, Employee NI, Employer NI, pension, VAT 20%.
4
5export const LAST_UPDATED = "2025-01-01";
6export const TAX_YEAR = "2024-2025";
7export const JURISDICTION = "UK";
8export const CURRENCY = { code: "GBP", symbol: "£" };
9
10// ---------------------------------------------------------------------------
11// Types
12// ---------------------------------------------------------------------------
13
14export interface UKPayrollResult {
15 grossPay: number;
16 incomeTax: number;
17 employeeDeductions: { name: string; amount: number }[];
18 employerCosts: { name: string; amount: number }[];
19 netPay: number;
20 totalEmployerCost: number;
21}
22
23export type PayFrequency = "weekly" | "fortnightly" | "monthly";
24
25// ---------------------------------------------------------------------------
26// Rate Constants
27// ---------------------------------------------------------------------------
28
29export const INCOME_TAX_BRACKETS = [
30 { min: 0, max: 12_570, rate: 0, label: "Personal allowance (0 - 12,570)" },
31 { min: 12_570, max: 50_270, rate: 0.20, label: "Basic rate (12,571 - 50,270)" },
32 { min: 50_270, max: 125_140, rate: 0.40, label: "Higher rate (50,271 - 125,140)" },
33 { min: 125_140, max: Infinity, rate: 0.45, label: "Additional rate (125,141+)" },
34] as const;
35
36// Employee NI thresholds (Class 1)
37export const NI_PRIMARY_THRESHOLD = 12_570;
38export const NI_UPPER_EARNINGS_LIMIT = 50_270;
39export const NI_MAIN_RATE = 0.08;
40export const NI_UPPER_RATE = 0.02;
41
42// Employer NI
43export const NI_SECONDARY_THRESHOLD = 9_100;
44export const EMPLOYER_NI_RATE = 0.138;
45
46// Auto-enrolment pension
47export const PENSION_EMPLOYEE_RATE = 0.05;
48export const PENSION_EMPLOYER_RATE = 0.03;
49
50// VAT
51export const VAT_STANDARD_RATE = 0.20;
52export const VAT_REDUCED_RATE = 0.05;
53
54// Corporation Tax
55export const CORPORATE_TAX_SMALL_PROFITS_RATE = 0.19; // profits <= 50,000
56export const CORPORATE_TAX_MAIN_RATE = 0.25; // profits > 250,000
57
58// ---------------------------------------------------------------------------
59// Helpers (self-contained — no shared imports)
60// ---------------------------------------------------------------------------
61
62function periodsPerYear(freq: PayFrequency): number {
63 switch (freq) {
64 case "weekly":
65 return 52;
66 case "fortnightly":
67 return 26;
68 case "monthly":
69 return 12;
70 }
71}
72
73function applyBrackets(
74 annual: number,
75 brackets: ReadonlyArray<{ min: number; max: number; rate: number }>,
76): number {
77 let tax = 0;
78 for (const b of brackets) {
79 if (annual <= b.min) break;
80 const taxable = Math.min(annual, b.max) - b.min;
81 tax += taxable * b.rate;
82 }
83 return tax;
84}
85
86function round2(n: number): number {
87 return Math.round(n * 100) / 100;
88}
89
90// ---------------------------------------------------------------------------
91// Payroll Calculator
92// ---------------------------------------------------------------------------
93
94export function calculatePayroll(
95 annualSalary: number,
96 frequency: PayFrequency,
97): UKPayrollResult {
98 const periods = periodsPerYear(frequency);
99
100 // Income tax (note: personal allowance tapers above 100k but simplified here)
101 const annualIncomeTax = applyBrackets(annualSalary, INCOME_TAX_BRACKETS);
102
103 // Employee NI
104 let annualEmployeeNI = 0;
105 if (annualSalary > NI_PRIMARY_THRESHOLD) {
106 const mainBand = Math.min(annualSalary, NI_UPPER_EARNINGS_LIMIT) - NI_PRIMARY_THRESHOLD;
107 annualEmployeeNI += Math.max(0, mainBand) * NI_MAIN_RATE;
108 if (annualSalary > NI_UPPER_EARNINGS_LIMIT) {
109 annualEmployeeNI += (annualSalary - NI_UPPER_EARNINGS_LIMIT) * NI_UPPER_RATE;
110 }
111 }
112
113 // Employer NI
114 let annualEmployerNI = 0;
115 if (annualSalary > NI_SECONDARY_THRESHOLD) {
116 annualEmployerNI = (annualSalary - NI_SECONDARY_THRESHOLD) * EMPLOYER_NI_RATE;
117 }
118
119 // Pension
120 const annualPensionEmployee = annualSalary * PENSION_EMPLOYEE_RATE;
121 const annualPensionEmployer = annualSalary * PENSION_EMPLOYER_RATE;
122
123 const grossPay = round2(annualSalary / periods);
124 const incomeTax = round2(annualIncomeTax / periods);
125
126 const employeeDeductions = [
127 { name: "Income tax", amount: round2(annualIncomeTax / periods) },
128 { name: "Employee NI", amount: round2(annualEmployeeNI / periods) },
129 { name: "Pension (5%)", amount: round2(annualPensionEmployee / periods) },
130 ];
131
132 const employerCosts = [
133 { name: "Employer NI (13.8%)", amount: round2(annualEmployerNI / periods) },
134 { name: "Employer pension (3%)", amount: round2(annualPensionEmployer / periods) },
135 ];
136
137 const totalDeductions = employeeDeductions.reduce((s, d) => s + d.amount, 0);
138 const netPay = round2(grossPay - totalDeductions);
139 const totalEmployerCost = round2(grossPay + employerCosts.reduce((s, c) => s + c.amount, 0));
140
141 return { grossPay, incomeTax, employeeDeductions, employerCosts, netPay, totalEmployerCost };
142}
143
144// ---------------------------------------------------------------------------
145// VAT Calculator
146// ---------------------------------------------------------------------------
147
148export function calculateGST(
149 amount: number,
150 inclusive: boolean,
151): { gross: number; tax: number; net: number; rate: number } {
152 const rate = VAT_STANDARD_RATE;
153 if (inclusive) {
154 const gross = round2(amount);
155 const tax = round2(amount - amount / (1 + rate));
156 const net = round2(amount - tax);
157 return { gross, tax, net, rate };
158 }
159 const net = round2(amount);
160 const tax = round2(amount * rate);
161 const gross = round2(amount + tax);
162 return { gross, tax, net, rate };
163}
164
165// ---------------------------------------------------------------------------
166// Income Tax Calculator
167// ---------------------------------------------------------------------------
168
169export function calculateIncomeTax(
170 income: number,
171): { tax: number; effectiveRate: number; breakdown: { bracket: string; rate: number; tax: number }[] } {
172 const breakdown: { bracket: string; rate: number; tax: number }[] = [];
173 let totalTax = 0;
174
175 for (const b of INCOME_TAX_BRACKETS) {
176 if (income <= b.min) break;
177 const taxable = Math.min(income, b.max) - b.min;
178 const tax = round2(taxable * b.rate);
179 totalTax += tax;
180 if (tax > 0 || b.rate === 0) {
181 breakdown.push({ bracket: b.label, rate: b.rate, tax });
182 }
183 }
184
185 totalTax = round2(totalTax);
186 const effectiveRate = income > 0 ? round2((totalTax / income) * 10000) / 10000 : 0;
187
188 return { tax: totalTax, effectiveRate, breakdown };
189}
Addedlib/tax/us.ts+316−0View fileUnifiedSplit
@@ -0,0 +1,316 @@
1// United States Tax & Payroll Module
2// Isolated per-country module — no imports from other country files.
3// Federal brackets, Social Security, Medicare, FUTA, state sales tax table.
4
5export const LAST_UPDATED = "2025-01-01";
6export const TAX_YEAR = "2024-2025";
7export const JURISDICTION = "US";
8export const CURRENCY = { code: "USD", symbol: "$" };
9
10// ---------------------------------------------------------------------------
11// Types
12// ---------------------------------------------------------------------------
13
14export interface USPayrollResult {
15 grossPay: number;
16 incomeTax: number;
17 employeeDeductions: { name: string; amount: number }[];
18 employerCosts: { name: string; amount: number }[];
19 netPay: number;
20 totalEmployerCost: number;
21}
22
23export type PayFrequency = "weekly" | "fortnightly" | "monthly";
24
25export interface USPayrollOptions {
26 filingStatus?: "single" | "married";
27}
28
29// ---------------------------------------------------------------------------
30// Rate Constants
31// ---------------------------------------------------------------------------
32
33export const FEDERAL_BRACKETS_SINGLE = [
34 { min: 0, max: 11_600, rate: 0.10 },
35 { min: 11_600, max: 47_150, rate: 0.12 },
36 { min: 47_150, max: 100_525, rate: 0.22 },
37 { min: 100_525, max: 191_950, rate: 0.24 },
38 { min: 191_950, max: 243_725, rate: 0.32 },
39 { min: 243_725, max: 609_350, rate: 0.35 },
40 { min: 609_350, max: Infinity, rate: 0.37 },
41] as const;
42
43export const FEDERAL_BRACKETS_MARRIED = [
44 { min: 0, max: 23_200, rate: 0.10 },
45 { min: 23_200, max: 94_300, rate: 0.12 },
46 { min: 94_300, max: 201_050, rate: 0.22 },
47 { min: 201_050, max: 383_900, rate: 0.24 },
48 { min: 383_900, max: 487_450, rate: 0.32 },
49 { min: 487_450, max: 731_200, rate: 0.35 },
50 { min: 731_200, max: Infinity, rate: 0.37 },
51] as const;
52
53export const STANDARD_DEDUCTION_SINGLE = 14_600;
54export const STANDARD_DEDUCTION_MARRIED = 29_200;
55
56export const SS_RATE = 0.062;
57export const SS_WAGE_CAP = 168_600;
58export const MEDICARE_RATE = 0.0145;
59export const MEDICARE_ADDITIONAL_THRESHOLD = 200_000;
60export const MEDICARE_ADDITIONAL_RATE = 0.009;
61export const FUTA_RATE = 0.006; // Effective rate after state credit
62export const FUTA_WAGE_CAP = 7_000;
63
64export const CORPORATE_TAX_RATE = 0.21; // Federal flat rate
65
66export const INCOME_TAX_BRACKETS_SINGLE = [
67 { min: 0, max: 11_600, rate: 0.10, label: "$0 - $11,600" },
68 { min: 11_600, max: 47_150, rate: 0.12, label: "$11,601 - $47,150" },
69 { min: 47_150, max: 100_525, rate: 0.22, label: "$47,151 - $100,525" },
70 { min: 100_525, max: 191_950, rate: 0.24, label: "$100,526 - $191,950" },
71 { min: 191_950, max: 243_725, rate: 0.32, label: "$191,951 - $243,725" },
72 { min: 243_725, max: 609_350, rate: 0.35, label: "$243,726 - $609,350" },
73 { min: 609_350, max: Infinity, rate: 0.37, label: "$609,351+" },
74] as const;
75
76export const INCOME_TAX_BRACKETS_MARRIED = [
77 { min: 0, max: 23_200, rate: 0.10, label: "$0 - $23,200" },
78 { min: 23_200, max: 94_300, rate: 0.12, label: "$23,201 - $94,300" },
79 { min: 94_300, max: 201_050, rate: 0.22, label: "$94,301 - $201,050" },
80 { min: 201_050, max: 383_900, rate: 0.24, label: "$201,051 - $383,900" },
81 { min: 383_900, max: 487_450, rate: 0.32, label: "$383,901 - $487,450" },
82 { min: 487_450, max: 731_200, rate: 0.35, label: "$487,451 - $731,200" },
83 { min: 731_200, max: Infinity, rate: 0.37, label: "$731,201+" },
84] as const;
85
86// State sales tax table (all 50 states + DC)
87export const STATE_SALES_TAX: Record<
88 string,
89 { state: string; rate: number; avgLocalRate: number; combinedRate: number }
90> = {
91 AL: { state: "Alabama", rate: 4.0, avgLocalRate: 5.24, combinedRate: 9.24 },
92 AK: { state: "Alaska", rate: 0, avgLocalRate: 1.76, combinedRate: 1.76 },
93 AZ: { state: "Arizona", rate: 5.6, avgLocalRate: 2.8, combinedRate: 8.4 },
94 AR: { state: "Arkansas", rate: 6.5, avgLocalRate: 2.97, combinedRate: 9.47 },
95 CA: { state: "California", rate: 7.25, avgLocalRate: 1.57, combinedRate: 8.82 },
96 CO: { state: "Colorado", rate: 2.9, avgLocalRate: 4.89, combinedRate: 7.79 },
97 CT: { state: "Connecticut", rate: 6.35, avgLocalRate: 0, combinedRate: 6.35 },
98 DE: { state: "Delaware", rate: 0, avgLocalRate: 0, combinedRate: 0 },
99 FL: { state: "Florida", rate: 6.0, avgLocalRate: 1.02, combinedRate: 7.02 },
100 GA: { state: "Georgia", rate: 4.0, avgLocalRate: 3.38, combinedRate: 7.38 },
101 HI: { state: "Hawaii", rate: 4.0, avgLocalRate: 0.44, combinedRate: 4.44 },
102 ID: { state: "Idaho", rate: 6.0, avgLocalRate: 0.02, combinedRate: 6.02 },
103 IL: { state: "Illinois", rate: 6.25, avgLocalRate: 2.58, combinedRate: 8.83 },
104 IN: { state: "Indiana", rate: 7.0, avgLocalRate: 0, combinedRate: 7.0 },
105 IA: { state: "Iowa", rate: 6.0, avgLocalRate: 0.94, combinedRate: 6.94 },
106 KS: { state: "Kansas", rate: 6.5, avgLocalRate: 2.19, combinedRate: 8.69 },
107 KY: { state: "Kentucky", rate: 6.0, avgLocalRate: 0, combinedRate: 6.0 },
108 LA: { state: "Louisiana", rate: 4.45, avgLocalRate: 5.1, combinedRate: 9.55 },
109 ME: { state: "Maine", rate: 5.5, avgLocalRate: 0, combinedRate: 5.5 },
110 MD: { state: "Maryland", rate: 6.0, avgLocalRate: 0, combinedRate: 6.0 },
111 MA: { state: "Massachusetts", rate: 6.25, avgLocalRate: 0, combinedRate: 6.25 },
112 MI: { state: "Michigan", rate: 6.0, avgLocalRate: 0, combinedRate: 6.0 },
113 MN: { state: "Minnesota", rate: 6.875, avgLocalRate: 0.66, combinedRate: 7.535 },
114 MS: { state: "Mississippi", rate: 7.0, avgLocalRate: 0.07, combinedRate: 7.07 },
115 MO: { state: "Missouri", rate: 4.225, avgLocalRate: 4.06, combinedRate: 8.285 },
116 MT: { state: "Montana", rate: 0, avgLocalRate: 0, combinedRate: 0 },
117 NE: { state: "Nebraska", rate: 5.5, avgLocalRate: 1.44, combinedRate: 6.94 },
118 NV: { state: "Nevada", rate: 6.85, avgLocalRate: 1.38, combinedRate: 8.23 },
119 NH: { state: "New Hampshire", rate: 0, avgLocalRate: 0, combinedRate: 0 },
120 NJ: { state: "New Jersey", rate: 6.625, avgLocalRate: -0.03, combinedRate: 6.6 },
121 NM: { state: "New Mexico", rate: 4.875, avgLocalRate: 2.72, combinedRate: 7.595 },
122 NY: { state: "New York", rate: 4.0, avgLocalRate: 4.53, combinedRate: 8.53 },
123 NC: { state: "North Carolina", rate: 4.75, avgLocalRate: 2.23, combinedRate: 6.98 },
124 ND: { state: "North Dakota", rate: 5.0, avgLocalRate: 1.96, combinedRate: 6.96 },
125 OH: { state: "Ohio", rate: 5.75, avgLocalRate: 1.49, combinedRate: 7.24 },
126 OK: { state: "Oklahoma", rate: 4.5, avgLocalRate: 4.47, combinedRate: 8.97 },
127 OR: { state: "Oregon", rate: 0, avgLocalRate: 0, combinedRate: 0 },
128 PA: { state: "Pennsylvania", rate: 6.0, avgLocalRate: 0.34, combinedRate: 6.34 },
129 RI: { state: "Rhode Island", rate: 7.0, avgLocalRate: 0, combinedRate: 7.0 },
130 SC: { state: "South Carolina", rate: 6.0, avgLocalRate: 1.44, combinedRate: 7.44 },
131 SD: { state: "South Dakota", rate: 4.2, avgLocalRate: 1.9, combinedRate: 6.1 },
132 TN: { state: "Tennessee", rate: 7.0, avgLocalRate: 2.55, combinedRate: 9.55 },
133 TX: { state: "Texas", rate: 6.25, avgLocalRate: 1.95, combinedRate: 8.2 },
134 UT: { state: "Utah", rate: 6.1, avgLocalRate: 1.09, combinedRate: 7.19 },
135 VT: { state: "Vermont", rate: 6.0, avgLocalRate: 0.24, combinedRate: 6.24 },
136 VA: { state: "Virginia", rate: 5.3, avgLocalRate: 0.45, combinedRate: 5.75 },
137 WA: { state: "Washington", rate: 6.5, avgLocalRate: 3.83, combinedRate: 10.33 },
138 WV: { state: "West Virginia", rate: 6.0, avgLocalRate: 0.56, combinedRate: 6.56 },
139 WI: { state: "Wisconsin", rate: 5.0, avgLocalRate: 0.44, combinedRate: 5.44 },
140 WY: { state: "Wyoming", rate: 4.0, avgLocalRate: 1.36, combinedRate: 5.36 },
141 DC: { state: "District of Columbia", rate: 6.0, avgLocalRate: 0, combinedRate: 6.0 },
142};
143
144// Average combined sales tax rate used for generic calculations
145export const AVG_SALES_TAX_RATE = 0.085;
146
147// ---------------------------------------------------------------------------
148// Helpers (self-contained — no shared imports)
149// ---------------------------------------------------------------------------
150
151function periodsPerYear(freq: PayFrequency): number {
152 switch (freq) {
153 case "weekly":
154 return 52;
155 case "fortnightly":
156 return 26;
157 case "monthly":
158 return 12;
159 }
160}
161
162function applyBrackets(
163 annual: number,
164 brackets: ReadonlyArray<{ min: number; max: number; rate: number }>,
165): number {
166 let tax = 0;
167 for (const b of brackets) {
168 if (annual <= b.min) break;
169 const taxable = Math.min(annual, b.max) - b.min;
170 tax += taxable * b.rate;
171 }
172 return tax;
173}
174
175function round2(n: number): number {
176 return Math.round(n * 100) / 100;
177}
178
179// ---------------------------------------------------------------------------
180// Payroll Calculator
181// ---------------------------------------------------------------------------
182
183export function calculatePayroll(
184 annualSalary: number,
185 frequency: PayFrequency,
186 options?: USPayrollOptions,
187): USPayrollResult {
188 const periods = periodsPerYear(frequency);
189 const filingStatus = options?.filingStatus ?? "single";
190
191 // Federal income tax (standard deduction applied)
192 const standardDeduction = filingStatus === "married"
193 ? STANDARD_DEDUCTION_MARRIED
194 : STANDARD_DEDUCTION_SINGLE;
195 const taxableIncome = Math.max(0, annualSalary - standardDeduction);
196 const brackets = filingStatus === "married"
197 ? FEDERAL_BRACKETS_MARRIED
198 : FEDERAL_BRACKETS_SINGLE;
199 const annualFederalTax = applyBrackets(taxableIncome, brackets);
200
201 // Social Security (employee)
202 const ssWages = Math.min(annualSalary, SS_WAGE_CAP);
203 const annualSSEmployee = ssWages * SS_RATE;
204
205 // Medicare (employee)
206 let annualMedicareEmployee = annualSalary * MEDICARE_RATE;
207 if (annualSalary > MEDICARE_ADDITIONAL_THRESHOLD) {
208 annualMedicareEmployee += (annualSalary - MEDICARE_ADDITIONAL_THRESHOLD) * MEDICARE_ADDITIONAL_RATE;
209 }
210
211 // Social Security (employer) - same as employee
212 const annualSSEmployer = ssWages * SS_RATE;
213
214 // Medicare (employer) - no additional surtax
215 const annualMedicareEmployer = annualSalary * MEDICARE_RATE;
216
217 // FUTA (employer only)
218 const futaWages = Math.min(annualSalary, FUTA_WAGE_CAP);
219 const annualFUTA = futaWages * FUTA_RATE;
220
221 const grossPay = round2(annualSalary / periods);
222 const incomeTax = round2(annualFederalTax / periods);
223
224 const employeeDeductions: { name: string; amount: number }[] = [
225 { name: "Federal income tax", amount: round2(annualFederalTax / periods) },
226 { name: "Social Security (6.2%)", amount: round2(annualSSEmployee / periods) },
227 ];
228
229 // Split Medicare line if additional Medicare applies
230 if (annualSalary > MEDICARE_ADDITIONAL_THRESHOLD) {
231 const baseMedicare = annualSalary * MEDICARE_RATE;
232 const additionalMedicare = (annualSalary - MEDICARE_ADDITIONAL_THRESHOLD) * MEDICARE_ADDITIONAL_RATE;
233 employeeDeductions.push(
234 { name: "Medicare (1.45%)", amount: round2(baseMedicare / periods) },
235 { name: "Additional Medicare (0.9%)", amount: round2(additionalMedicare / periods) },
236 );
237 } else {
238 employeeDeductions.push(
239 { name: "Medicare (1.45%)", amount: round2(annualMedicareEmployee / periods) },
240 );
241 }
242
243 const employerCosts = [
244 { name: "Social Security (6.2%)", amount: round2(annualSSEmployer / periods) },
245 { name: "Medicare (1.45%)", amount: round2(annualMedicareEmployer / periods) },
246 { name: "FUTA (0.6%)", amount: round2(annualFUTA / periods) },
247 ];
248
249 const totalDeductions = employeeDeductions.reduce((s, d) => s + d.amount, 0);
250 const netPay = round2(grossPay - totalDeductions);
251 const totalEmployerCost = round2(grossPay + employerCosts.reduce((s, c) => s + c.amount, 0));
252
253 return { grossPay, incomeTax, employeeDeductions, employerCosts, netPay, totalEmployerCost };
254}
255
256// ---------------------------------------------------------------------------
257// Sales Tax Calculator (equivalent to GST/VAT in other countries)
258// ---------------------------------------------------------------------------
259
260export function calculateGST(
261 amount: number,
262 inclusive: boolean,
263 stateCode?: string,
264): { gross: number; tax: number; net: number; rate: number } {
265 const rate = stateCode
266 ? (STATE_SALES_TAX[stateCode.toUpperCase()]?.combinedRate ?? AVG_SALES_TAX_RATE * 100) / 100
267 : AVG_SALES_TAX_RATE;
268
269 if (inclusive) {
270 const gross = round2(amount);
271 const tax = round2(amount - amount / (1 + rate));
272 const net = round2(amount - tax);
273 return { gross, tax, net, rate };
274 }
275 const net = round2(amount);
276 const tax = round2(amount * rate);
277 const gross = round2(amount + tax);
278 return { gross, tax, net, rate };
279}
280
281// ---------------------------------------------------------------------------
282// Income Tax Calculator
283// ---------------------------------------------------------------------------
284
285export function calculateIncomeTax(
286 income: number,
287 filingStatus: "single" | "married" = "single",
288): { tax: number; effectiveRate: number; breakdown: { bracket: string; rate: number; tax: number }[] } {
289 const breakdown: { bracket: string; rate: number; tax: number }[] = [];
290
291 const stdDed = filingStatus === "married" ? STANDARD_DEDUCTION_MARRIED : STANDARD_DEDUCTION_SINGLE;
292 const taxableIncome = Math.max(0, income - stdDed);
293 const brackets = filingStatus === "married" ? INCOME_TAX_BRACKETS_MARRIED : INCOME_TAX_BRACKETS_SINGLE;
294
295 breakdown.push({
296 bracket: `Standard deduction: -$${stdDed.toLocaleString()} (${filingStatus})`,
297 rate: 0,
298 tax: 0,
299 });
300
301 let totalTax = 0;
302 for (const b of brackets) {
303 if (taxableIncome <= b.min) break;
304 const taxable = Math.min(taxableIncome, b.max) - b.min;
305 const tax = round2(taxable * b.rate);
306 totalTax += tax;
307 if (tax > 0) {
308 breakdown.push({ bracket: b.label, rate: b.rate, tax });
309 }
310 }
311
312 totalTax = round2(totalTax);
313 const effectiveRate = income > 0 ? round2((totalTax / income) * 10000) / 10000 : 0;
314
315 return { tax: totalTax, effectiveRate, breakdown };
316}
0317
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts