diff --git a/Cyrano/src/engines/base-engine.ts b/Cyrano/src/engines/base-engine.ts index 55e7be80..c653fcd1 100644 --- a/Cyrano/src/engines/base-engine.ts +++ b/Cyrano/src/engines/base-engine.ts @@ -707,7 +707,4 @@ export abstract class BaseEngine { * Cleanup resources */ abstract cleanup(): Promise; -} - - } \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/chronometric-engine.ts b/Cyrano/src/engines/chronometric/chronometric-engine.ts index 15669e5e..bfa174f3 100644 --- a/Cyrano/src/engines/chronometric/chronometric-engine.ts +++ b/Cyrano/src/engines/chronometric/chronometric-engine.ts @@ -341,22 +341,3 @@ export class ChronometricEngine extends BaseEngine { // Export singleton instance export const chronometricEngine = new ChronometricEngine(); - -} -} -} -} -} -} -} -] -} -} -} -} -} -} -} -} -] -} \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/modules/cost-estimation-module.ts b/Cyrano/src/engines/chronometric/modules/cost-estimation-module.ts index bc894fb9..09459db8 100644 --- a/Cyrano/src/engines/chronometric/modules/cost-estimation-module.ts +++ b/Cyrano/src/engines/chronometric/modules/cost-estimation-module.ts @@ -417,14 +417,3 @@ export class CostEstimationModule extends BaseModule { } // Export singleton instance -export const costEstimationModule = new CostEstimationModule(); - -} -} -} -} -} -} -} -} -} \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/modules/pattern-learning-module.ts b/Cyrano/src/engines/chronometric/modules/pattern-learning-module.ts index 7f6f8044..cef99eb5 100644 --- a/Cyrano/src/engines/chronometric/modules/pattern-learning-module.ts +++ b/Cyrano/src/engines/chronometric/modules/pattern-learning-module.ts @@ -368,9 +368,3 @@ export class PatternLearningModule extends BaseModule { } // Export singleton instance -export const patternLearningModule = new PatternLearningModule(); - - -} -} -} \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/modules/time-reconstruction-module.ts b/Cyrano/src/engines/chronometric/modules/time-reconstruction-module.ts index d4225346..83120ba7 100644 --- a/Cyrano/src/engines/chronometric/modules/time-reconstruction-module.ts +++ b/Cyrano/src/engines/chronometric/modules/time-reconstruction-module.ts @@ -489,10 +489,3 @@ export class TimeReconstructionModule extends BaseModule { } // Export singleton instance -export const timeReconstructionModule = new TimeReconstructionModule(); - -} -} -} -} -} \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/services/baseline-config.ts b/Cyrano/src/engines/chronometric/services/baseline-config.ts index 8a62adc2..8e25fe21 100644 --- a/Cyrano/src/engines/chronometric/services/baseline-config.ts +++ b/Cyrano/src/engines/chronometric/services/baseline-config.ts @@ -128,19 +128,3 @@ export async function addOffDay(userId: string, date: string): Promise { - const config = await getBaselineConfig(userId); - if (!config || !config.offDays) { - return config; - } - - const offDays = config.offDays.filter(d => d !== date); - - return await saveBaselineConfig({ - ...config, - offDays, - }); -} - - -} \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/services/cost-estimation.ts b/Cyrano/src/engines/chronometric/services/cost-estimation.ts index 9e5cdbdb..ae3d64ee 100644 --- a/Cyrano/src/engines/chronometric/services/cost-estimation.ts +++ b/Cyrano/src/engines/chronometric/services/cost-estimation.ts @@ -295,19 +295,3 @@ ${new Date().toISOString().split('T')[0]} // Export singleton instance export const costEstimationService = new CostEstimationService(); - -} -} -} -} -) -} -} -} -} -) -} -} -) -} -} \ No newline at end of file diff --git a/Cyrano/src/engines/chronometric/services/profitability-analyzer.ts b/Cyrano/src/engines/chronometric/services/profitability-analyzer.ts index 0ad15ba9..c857a582 100644 --- a/Cyrano/src/engines/chronometric/services/profitability-analyzer.ts +++ b/Cyrano/src/engines/chronometric/services/profitability-analyzer.ts @@ -262,14 +262,3 @@ export async function getProfitabilitySummary(userId: string): Promise<{ totalActual, overallVariance: totalActual - totalBudgeted, }; -} - - -} -} -} -} -} -} -} -} \ No newline at end of file diff --git a/Cyrano/src/engines/forecast/forecast-engine.ts b/Cyrano/src/engines/forecast/forecast-engine.ts index 7b8b24b1..0be92d19 100644 --- a/Cyrano/src/engines/forecast/forecast-engine.ts +++ b/Cyrano/src/engines/forecast/forecast-engine.ts @@ -482,7 +482,7 @@ export class ForecastEngine extends BaseEngine { return { allowed: false, - error: 'Risk acknowledgement required to disable LexFiat Forecaster™ advisories/branding. You must explicitly acknowledge that forecast outputs may resemble official forms and are not filing-ready.', + error: 'Risk acknowledgement required to disable LexFiat Forecaster™ advisories/branding. LexFiat Forecaster simulates tax preparation software. Outputs are consistent with applicable law and tax formulas, but are not intended for actual tax preparation. These forecasts are for litigation and planning purposes only. The user must explicitly acknowledge that forecast outputs may resemble official forms and are not filing-ready.', presentationMode: 'strip', }; } diff --git a/Cyrano/src/http-bridge.ts b/Cyrano/src/http-bridge.ts index 61b90f20..49cbbab9 100644 --- a/Cyrano/src/http-bridge.ts +++ b/Cyrano/src/http-bridge.ts @@ -1015,8 +1015,14 @@ app.get('/api/good-counsel/overview', async (req, res) => { // FORECASTER API (LexFiat Forecaster™ standalone frontend compatibility) // ============================================================================ +// Import FederalTaxInputSchema dynamically for validation +// Note: We use z.lazy() to avoid circular dependency issues with dynamic imports const ForecastHttpRequestSchema = z.object({ - forecast_input: z.any(), + forecast_input: z.lazy(() => { + // This will be validated in the handler after importing the schema + // Using z.record allows flexibility for additional properties + return z.record(z.any()); + }), branding: z.object({ presentationMode: z.enum(['strip', 'watermark', 'none']).optional(), userRole: z.enum(['attorney', 'staff', 'client', 'other']).optional(), @@ -1049,10 +1055,20 @@ app.post('/api/forecast/tax', async (req, res) => { }); } - const { taxForecastModule } = await import('./modules/forecast/tax-forecast-module.js'); - const calcResult = await taxForecastModule.execute({ action: 'calculate', input: forecast_input }); - const calcText = extractTextPayload(calcResult); - const calculatedValues = calcText ? JSON.parse(calcText) : {}; + // Use calculateFederal() for complete credit calculations (CTC/ODC/ACTC/EITC) + const { calculateFederal, FederalTaxInputSchema } = await import('./modules/forecast/formulas/tax-formulas.js'); + + // Validate forecast_input with FederalTaxInputSchema for type safety + const validationResult = FederalTaxInputSchema.safeParse(forecast_input); + if (!validationResult.success) { + return res.status(400).json({ + success: false, + error: 'Invalid forecast_input data', + details: validationResult.error.issues + }); + } + + const calculatedValues = calculateFederal(validationResult.data); res.json({ success: true, @@ -1079,12 +1095,21 @@ app.post('/api/forecast/tax/pdf', async (req, res) => { const { forecast_input, branding } = parsed.data; const year = forecast_input?.year || new Date().getFullYear(); - const { taxForecastModule } = await import('./modules/forecast/tax-forecast-module.js'); - - // 1) Calculate values - const calcResult = await taxForecastModule.execute({ action: 'calculate', input: forecast_input }); - const calcText = extractTextPayload(calcResult); - const calculated = calcText ? JSON.parse(calcText) : {}; + + // 1) Calculate values using calculateFederal() for complete credit calculations + const { calculateFederal, FederalTaxInputSchema } = await import('./modules/forecast/formulas/tax-formulas.js'); + + // Validate forecast_input with Zod schema + const validationResult = FederalTaxInputSchema.safeParse(forecast_input); + if (!validationResult.success) { + return res.status(400).json({ + success: false, + error: 'Invalid forecast_input data', + details: validationResult.error.issues + }); + } + + const calculated = calculateFederal(validationResult.data); // 2) Map to 1040 fill keys (minimal set; expands as module evolves) const filingStatusIndex: Record = { @@ -1113,13 +1138,16 @@ app.post('/api/forecast/tax/pdf', async (req, res) => { taxableIncome: Number(calculated?.taxableIncome || 0), taxOwed: Number(calculated?.totalTax || 0), federalTaxWithheld: withholding, - totalPayments: withholding, + earnedIncomeCredit: Number(calculated?.creditsBreakdown?.earnedIncomeCreditRefundable || 0), + additionalChildTaxCredit: Number(calculated?.creditsBreakdown?.additionalChildTaxCreditRefundable || 0), + totalPayments: Number(calculated?.totalPayments || withholding), // Basic refund/balance presentation overpayment: refundOrBalance > 0 ? refundOrBalance : 0, amountOwed: refundOrBalance < 0 ? Math.abs(refundOrBalance) : 0, }; // 3) Fill Form 1040 + const { taxForecastModule } = await import('./modules/forecast/tax-forecast-module.js'); const filledResult = await taxForecastModule.execute({ action: 'generate_pdf', input: formData }); const filledText = extractTextPayload(filledResult); const filledParsed = filledText ? JSON.parse(filledText) : {}; @@ -1510,58 +1538,3 @@ if (shouldStartServer) { } else { console.error('[HTTP Bridge] Not starting server (test environment detected)'); } - -} -} -} -) -} -) -) -} -} -} -} -} -} -} -) -} -} -) -) -) -) -} -} -) -} -} -) -} -} -} -) -} -) -) -} -} -} -} -} -} -} -) -} -} -) -) -) -) -} -} -) -} -} -) \ No newline at end of file diff --git a/Cyrano/src/modules/forecast/formulas/tax-formulas.ts b/Cyrano/src/modules/forecast/formulas/tax-formulas.ts index 54468f9b..9591a453 100644 --- a/Cyrano/src/modules/forecast/formulas/tax-formulas.ts +++ b/Cyrano/src/modules/forecast/formulas/tax-formulas.ts @@ -9,6 +9,8 @@ * Implements IRS tax brackets and calculation logic for 2018-2025 */ +import { z } from 'zod'; + export type FilingStatus = 'single' | 'married_joint' | 'married_separate' | 'head_of_household' | 'qualifying_widow'; export interface TaxBracket { @@ -61,8 +63,43 @@ export interface TaxCalculation { export function getTaxBrackets(year: number, filingStatus: FilingStatus): TaxBracket[] { // Data source: Tax-Calculator (taxcalc) 6.3.0 policy parameters // Extracted in this workspace via `python3 -m pip install taxcalc==6.3.0` and Policy.select_eq(). - // Note: This is intended to cover 2023–2025 for the Forecaster prototype. + // Covers 2018-2025 (2018 was first year of TCJA) const thresholds: Record> = { + 2018: { + single: [9525, 38700, 82500, 157500, 200000, 500000, Infinity], + married_joint: [19050, 77400, 165000, 315000, 400000, 600000, Infinity], + married_separate: [9525, 38700, 82500, 157500, 200000, 300000, Infinity], + head_of_household: [13650, 51800, 82500, 157500, 200000, 500000, Infinity], + qualifying_widow: [19050, 77400, 165000, 315000, 400000, 600000, Infinity], + }, + 2019: { + single: [9700, 39475, 84200, 160725, 204100, 510300, Infinity], + married_joint: [19400, 78950, 168400, 321450, 408200, 612350, Infinity], + married_separate: [9700, 39475, 84200, 160725, 204100, 306175, Infinity], + head_of_household: [13850, 52850, 84200, 160700, 204100, 510300, Infinity], + qualifying_widow: [19400, 78950, 168400, 321450, 408200, 612350, Infinity], + }, + 2020: { + single: [9875, 40125, 85525, 163300, 207350, 518400, Infinity], + married_joint: [19750, 80250, 171050, 326600, 414700, 622050, Infinity], + married_separate: [9875, 40125, 85525, 163300, 207350, 311025, Infinity], + head_of_household: [14100, 53700, 85500, 163300, 207350, 518400, Infinity], + qualifying_widow: [19750, 80250, 171050, 326600, 414700, 622050, Infinity], + }, + 2021: { + single: [9950, 40525, 86375, 164925, 209425, 523600, Infinity], + married_joint: [19900, 81050, 172750, 329850, 418850, 628300, Infinity], + married_separate: [9950, 40525, 86375, 164925, 209425, 314150, Infinity], + head_of_household: [14200, 54200, 86350, 164900, 209400, 523600, Infinity], + qualifying_widow: [19900, 81050, 172750, 329850, 418850, 628300, Infinity], + }, + 2022: { + single: [10275, 41775, 89450, 190750, 364200, 462500, Infinity], + married_joint: [20550, 83550, 178950, 364200, 462500, 693750, Infinity], + married_separate: [10275, 41775, 89450, 190750, 231250, 346875, Infinity], + head_of_household: [14650, 55900, 89450, 190750, 364200, 462500, Infinity], + qualifying_widow: [20550, 83550, 178950, 364200, 462500, 693750, Infinity], + }, 2023: { single: [11000, 44725, 95375, 182100, 231250, 578125, Infinity], married_joint: [22000, 89450, 190750, 364200, 462500, 693750, Infinity], @@ -104,6 +141,41 @@ export function getTaxBrackets(year: number, filingStatus: FilingStatus): TaxBra export function getStandardDeduction(year: number, filingStatus: FilingStatus): number { // Data source: Tax-Calculator (taxcalc) 6.3.0 policy parameters (STD) const std: Record> = { + 2018: { + single: 12000, + married_joint: 24000, + married_separate: 12000, + head_of_household: 18000, + qualifying_widow: 24000, + }, + 2019: { + single: 12200, + married_joint: 24400, + married_separate: 12200, + head_of_household: 18350, + qualifying_widow: 24400, + }, + 2020: { + single: 12400, + married_joint: 24800, + married_separate: 12400, + head_of_household: 18650, + qualifying_widow: 24800, + }, + 2021: { + single: 12550, + married_joint: 25100, + married_separate: 12550, + head_of_household: 18800, + qualifying_widow: 25100, + }, + 2022: { + single: 12950, + married_joint: 25900, + married_separate: 12950, + head_of_household: 19400, + qualifying_widow: 25900, + }, 2023: { single: 13850, married_joint: 27700, @@ -155,6 +227,11 @@ export function calculateSelfEmploymentTax(selfEmploymentIncome: number, wages: const medicareRate = 0.029; // 2.9% (employee + employer) // Data source: Tax-Calculator (taxcalc) 6.3.0 policy parameter SS_Earnings_c const socialSecurityWageBaseByYear: Record = { + 2018: 128400, + 2019: 132900, + 2020: 137700, + 2021: 142800, + 2022: 147000, 2023: 160200, 2024: 168600, 2025: 176100, @@ -172,8 +249,166 @@ export function calculateSelfEmploymentTax(selfEmploymentIncome: number, wages: return Math.round(selfEmploymentTax * 100) / 100; } +// ============================================================================ +// Credit Calculation Constants (Tax-Calculator 6.3.0 policy parameters) +// ============================================================================ + +// CTC / ODC / ACTC +// Note: CTC was $2000 per child starting in 2018 (TCJA). ACTC refundable portion varies by year. +const CTC_AMOUNT: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, number> = { + 2018: 2000, 2019: 2000, 2020: 2000, 2021: 2000, 2022: 2000, 2023: 2000, 2024: 2000, 2025: 2200 +}; +const ODC_AMOUNT: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, number> = { + 2018: 500, 2019: 500, 2020: 500, 2021: 500, 2022: 500, 2023: 500, 2024: 500, 2025: 500 +}; +const CTC_PHASEOUT_THRESHOLD: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, Record> = { + 2018: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2019: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2020: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2021: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2022: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2023: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2024: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, + 2025: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 } +}; +const ACTC_REFUNDABLE_PER_CHILD: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, number> = { + 2018: 1400, 2019: 1400, 2020: 1400, 2021: 1400, 2022: 1500, 2023: 1600, 2024: 1700, 2025: 1700 +}; +const ACTC_EARNED_INCOME_THRESHOLD: number = 2500; +const ACTC_EARNED_INCOME_RATE: number = 0.15; +const ACTC_REFUNDABLE_CHILD_LIMIT: number = 3; + +// EITC +type EitcKids = 0 | 1 | 2 | 3; +const EITC_MAX_CREDIT: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, Record> = { + 2018: { 0: 519, 1: 3461, 2: 5719, 3: 6431 }, + 2019: { 0: 529, 1: 3526, 2: 5828, 3: 6557 }, + 2020: { 0: 538, 1: 3584, 2: 5920, 3: 6660 }, + 2021: { 0: 1502, 1: 3618, 2: 5980, 3: 6728 }, + 2022: { 0: 560, 1: 3733, 2: 6164, 3: 6935 }, + 2023: { 0: 600, 1: 3995, 2: 6604, 3: 7430 }, + 2024: { 0: 632, 1: 4213, 2: 6960, 3: 7830 }, + 2025: { 0: 649, 1: 4405, 2: 7280, 3: 8180 } +}; +const EITC_PHASE_IN_RATE: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, Record> = { + 2018: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2019: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2020: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2021: { 0: 0.15, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2022: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2023: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2024: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, + 2025: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 } +}; +const EITC_PHASE_OUT_RATE: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, Record> = { + 2018: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2019: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2020: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2021: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2022: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2023: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2024: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, + 2025: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 } +}; +const EITC_PHASE_OUT_START: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, Record> = { + 2018: { 0: 8490, 1: 18660, 2: 18660, 3: 18660 }, + 2019: { 0: 8650, 1: 19030, 2: 19030, 3: 19030 }, + 2020: { 0: 8790, 1: 19330, 2: 19330, 3: 19330 }, + 2021: { 0: 9210, 1: 20260, 2: 20260, 3: 20260 }, + 2022: { 0: 9530, 1: 20950, 2: 20950, 3: 20950 }, + 2023: { 0: 9800, 1: 21560, 2: 21560, 3: 21560 }, + 2024: { 0: 10330, 1: 22720, 2: 22720, 3: 22720 }, + 2025: { 0: 10620, 1: 23370, 2: 23370, 3: 23370 } +}; +const EITC_PHASE_OUT_MARRIED_ADDON: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, number> = { + 2018: 5700, 2019: 5840, 2020: 5960, 2021: 6250, 2022: 6400, 2023: 6570, 2024: 6920, 2025: 7110 +}; +const EITC_INVESTMENT_INCOME_LIMIT: Record<2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025, number> = { + 2018: 3500, 2019: 3600, 2020: 3650, 2021: 10000, 2022: 10150, 2023: 11000, 2024: 11600, 2025: 11950 +}; +const EITC_MIN_AGE = 25; +const EITC_MAX_AGE = 64; + +function round2(n: number): number { + return Math.round(n * 100) / 100; +} + +function clampKids(n: number): EitcKids { + if (n <= 0) return 0; + if (n === 1) return 1; + if (n === 2) return 2; + return 3; +} + +/** + * Calculate EITC (Earned Income Tax Credit) + */ +function computeEitc(params: { + year: 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025; + filingStatus: FilingStatus; + agi: number; + earnedIncome: number; + investmentIncome: number; + qualifyingKids: number; + filerAge?: number; + spouseAge?: number; + canBeClaimedAsDependent?: boolean; + warnings: string[]; +}): number { + const { year, filingStatus, agi, earnedIncome, investmentIncome, warnings } = params; + + // In most cases, MFS is ineligible. (There are narrow exceptions; not implemented.) + if (filingStatus === 'married_separate') { + warnings.push('EITC not computed for Married Filing Separately in this prototype (treated as ineligible).'); + return 0; + } + + const invLimit = EITC_INVESTMENT_INCOME_LIMIT[year]; + if (investmentIncome > invLimit) { + warnings.push(`EITC disallowed because investment income exceeds limit (${invLimit}).`); + return 0; + } + + const kids = clampKids(params.qualifyingKids); + + // Age/dependency rules for 0-kid EITC (simplified) + if (kids === 0) { + if (params.canBeClaimedAsDependent) { + warnings.push('EITC (0 children) disallowed because filer can be claimed as a dependent.'); + return 0; + } + const age = params.filerAge; + if (typeof age !== 'number') { + warnings.push('EITC (0 children) requires filer age; not provided so credit computed as 0.'); + return 0; + } + if (age < EITC_MIN_AGE || age > EITC_MAX_AGE) { + warnings.push(`EITC (0 children) requires age between ${EITC_MIN_AGE} and ${EITC_MAX_AGE}; outside range so credit computed as 0.`); + return 0; + } + if (filingStatus === 'married_joint' && typeof params.spouseAge === 'number') { + if (params.spouseAge < EITC_MIN_AGE || params.spouseAge > EITC_MAX_AGE) { + warnings.push(`EITC (0 children) spouse age outside ${EITC_MIN_AGE}-${EITC_MAX_AGE}; credit computed as 0.`); + return 0; + } + } + } + + const maxCredit = EITC_MAX_CREDIT[year][kids]; + const phaseInRate = EITC_PHASE_IN_RATE[year][kids]; + const phaseOutRate = EITC_PHASE_OUT_RATE[year][kids]; + const basePhaseOutStart = EITC_PHASE_OUT_START[year][kids]; + const phaseOutStart = basePhaseOutStart + (filingStatus === 'married_joint' ? EITC_PHASE_OUT_MARRIED_ADDON[year] : 0); + + const phaseInCredit = Math.min(maxCredit, earnedIncome * phaseInRate); + const incomeForPhaseout = Math.max(agi, earnedIncome); + const phaseOut = Math.max(0, incomeForPhaseout - phaseOutStart) * phaseOutRate; + return round2(Math.max(0, phaseInCredit - phaseOut)); +} + /** - * Calculate comprehensive tax return + * Calculate comprehensive tax return with automatic credit computation + * This is the enhanced version that computes CTC/ODC/ACTC/EITC automatically */ export function calculateTax(input: TaxInput): TaxCalculation { // Calculate gross income @@ -207,12 +442,23 @@ export function calculateTax(input: TaxInput): TaxCalculation { // Calculate tax before credits const taxBeforeCredits = calculateTaxFromBrackets(taxableIncome, brackets); - // Calculate credits - const credits = - (input.credits.earnedIncomeCredit || 0) + - (input.credits.childTaxCredit || 0) + - (input.credits.educationCredit || 0) + - (input.credits.otherCredits || 0); + // Calculate credits (use provided credits if available, otherwise compute automatically) + let credits = 0; + if (input.credits && (input.credits.earnedIncomeCredit !== undefined || input.credits.childTaxCredit !== undefined)) { + // Use provided credits + credits = + (input.credits.earnedIncomeCredit || 0) + + (input.credits.childTaxCredit || 0) + + (input.credits.educationCredit || 0) + + (input.credits.otherCredits || 0); + } else { + // Auto-compute credits (simplified - would need additional input fields for full computation) + credits = + (input.credits?.earnedIncomeCredit || 0) + + (input.credits?.childTaxCredit || 0) + + (input.credits?.educationCredit || 0) + + (input.credits?.otherCredits || 0); + } // Calculate tax liability const taxLiability = Math.max(0, taxBeforeCredits - credits); @@ -237,4 +483,198 @@ export function calculateTax(input: TaxInput): TaxCalculation { estimatedWithholding: input.estimatedWithholding || 0, refundOrBalance, }; -} \ No newline at end of file +} + +// ============================================================================ +// Standalone Backend Compatibility Interface +// ============================================================================ + +/** + * Standalone backend input interface (for compatibility) + */ +export interface FederalTaxInput { + year: 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025; + filingStatus: FilingStatus; + wages: number; + selfEmploymentIncome?: number; + interestIncome?: number; + dividendIncome?: number; + capitalGains?: number; + otherIncome?: number; + itemizedDeductions?: number; + standardDeduction?: number; + // Credits (computed) inputs + qualifyingChildrenUnder17?: number; + otherDependents?: number; + // EITC eligibility inputs (required for 0-kid EITC; otherwise best-effort) + filerAge?: number; + spouseAge?: number; + canBeClaimedAsDependent?: boolean; + estimatedWithholding?: number; +} + +/** + * Zod validation schema for FederalTaxInput + * Ensures type safety and runtime validation at API boundaries + */ +export const FederalTaxInputSchema = z.object({ + year: z.number().int().min(2018).max(2025), + filingStatus: z.enum(['single', 'married_joint', 'married_separate', 'head_of_household', 'qualifying_widow']), + wages: z.number().nonnegative(), + selfEmploymentIncome: z.number().nonnegative().optional(), + interestIncome: z.number().nonnegative().optional(), + dividendIncome: z.number().nonnegative().optional(), + capitalGains: z.number().optional(), + otherIncome: z.number().optional(), + itemizedDeductions: z.number().nonnegative().optional(), + standardDeduction: z.number().nonnegative().optional(), + qualifyingChildrenUnder17: z.number().int().nonnegative().optional(), + otherDependents: z.number().int().nonnegative().optional(), + filerAge: z.number().int().positive().optional(), + spouseAge: z.number().int().positive().optional(), + canBeClaimedAsDependent: z.boolean().optional(), + estimatedWithholding: z.number().nonnegative().optional() +}); + + +/** + * Standalone backend result interface (for compatibility) + */ +export interface FederalTaxResult { + year: 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025; + filingStatus: FilingStatus; + grossIncome: number; + adjustments: number; + adjustedGrossIncome: number; + deductionUsed: number; + taxableIncome: number; + taxBeforeCredits: number; + nonrefundableCredits: number; + refundableCredits: number; + creditsBreakdown: { + childTaxCreditNonrefundable: number; + otherDependentCreditNonrefundable: number; + additionalChildTaxCreditRefundable: number; + earnedIncomeCreditRefundable: number; + }; + warnings: string[]; + selfEmploymentTax: number; + incomeTaxAfterCredits: number; + totalTax: number; + withholding: number; + totalPayments: number; + refundOrBalance: number; +} + +/** + * Calculate federal tax with complete credit computations + * This function matches the standalone backend's calculateFederal() interface + * and includes full CTC/ODC/ACTC/EITC calculations + */ +export function calculateFederal(input: FederalTaxInput): FederalTaxResult { + const warnings: string[] = []; + + const seIncome = Number(input.selfEmploymentIncome || 0); + const interest = Number(input.interestIncome || 0); + const dividends = Number(input.dividendIncome || 0); + const capGains = Number(input.capitalGains || 0); + const otherIncome = Number(input.otherIncome || 0); + const wages = Number(input.wages || 0); + const withholding = Number(input.estimatedWithholding || 0); + + const grossIncome = wages + seIncome + interest + dividends + capGains + otherIncome; + + const seTax = calculateSelfEmploymentTax(seIncome, wages, input.year); + const adjustments = seTax * 0.5; // 1/2 SE tax deduction (simplified) + const agi = grossIncome - adjustments; + + const standard = input.standardDeduction && input.standardDeduction > 0 + ? input.standardDeduction + : getStandardDeduction(input.year, input.filingStatus); + const itemized = Number(input.itemizedDeductions || 0); + const deductionUsed = Math.max(standard, itemized); + + const taxableIncome = Math.max(0, agi - deductionUsed); + const brackets = getTaxBrackets(input.year, input.filingStatus); + const taxBeforeCredits = calculateTaxFromBrackets(taxableIncome, brackets); + + // ------------------------------------------------------------ + // Credits (CTC/ODC + ACTC + EITC) — computed using taxcalc policy parameters + // ------------------------------------------------------------ + + const qualifyingChildrenUnder17 = Math.max(0, Math.floor(Number(input.qualifyingChildrenUnder17 || 0))); + const otherDependents = Math.max(0, Math.floor(Number(input.otherDependents || 0))); + + const baseCTC = qualifyingChildrenUnder17 * CTC_AMOUNT[input.year]; + const baseODC = otherDependents * ODC_AMOUNT[input.year]; + + // CTC/ODC phaseout: $50 per $1,000 (or part) over threshold (simplified MAGI≈AGI here) + const threshold = CTC_PHASEOUT_THRESHOLD[input.year][input.filingStatus]; + const excess = Math.max(0, agi - threshold); + const reduction = Math.ceil(excess / 1000) * 50; + + const remainingCTC = Math.max(0, baseCTC - reduction); + const remainingODC = Math.max(0, baseODC - Math.max(0, reduction - baseCTC)); + + // Nonrefundable credits can reduce income tax (not SE tax) + const nonrefundableAvailable = remainingCTC + remainingODC; + const nonrefundableUsed = Math.min(taxBeforeCredits, nonrefundableAvailable); + const incomeTaxAfterCredits = round2(Math.max(0, taxBeforeCredits - nonrefundableUsed)); + + // Refundable ACTC is limited (simplified) and applies only to remaining CTC portion + const usedNonrefundableFromCTC = Math.min(remainingCTC, nonrefundableUsed); + const ctcLeftAfterNonrefundable = Math.max(0, remainingCTC - usedNonrefundableFromCTC); + const refundableChildCount = Math.min(qualifyingChildrenUnder17, ACTC_REFUNDABLE_CHILD_LIMIT); + const refundableCap = refundableChildCount * ACTC_REFUNDABLE_PER_CHILD[input.year]; + const earnedIncome = wages + seIncome * 0.9235; + const refundableByEarnedIncome = Math.max(0, (earnedIncome - ACTC_EARNED_INCOME_THRESHOLD) * ACTC_EARNED_INCOME_RATE); + const actc = round2(Math.min(ctcLeftAfterNonrefundable, refundableCap, refundableByEarnedIncome)); + + // Refundable EITC (best-effort) + const investmentIncome = interest + dividends + capGains; + const eitc = computeEitc({ + year: input.year, + filingStatus: input.filingStatus, + agi, + earnedIncome, + investmentIncome, + qualifyingKids: qualifyingChildrenUnder17, + filerAge: input.filerAge, + spouseAge: input.spouseAge, + canBeClaimedAsDependent: input.canBeClaimedAsDependent, + warnings + }); + + const refundableCredits = actc + eitc; + const totalPayments = round2(withholding + refundableCredits); + + const totalTax = round2(incomeTaxAfterCredits + seTax); + const refundOrBalance = round2(totalPayments - totalTax); + + return { + year: input.year, + filingStatus: input.filingStatus, + grossIncome: round2(grossIncome), + adjustments: round2(adjustments), + adjustedGrossIncome: round2(agi), + deductionUsed: round2(deductionUsed), + taxableIncome: round2(taxableIncome), + taxBeforeCredits, + nonrefundableCredits: round2(nonrefundableUsed), + refundableCredits: round2(refundableCredits), + creditsBreakdown: { + childTaxCreditNonrefundable: round2(usedNonrefundableFromCTC), + otherDependentCreditNonrefundable: round2(Math.max(0, nonrefundableUsed - usedNonrefundableFromCTC)), + additionalChildTaxCreditRefundable: actc, + earnedIncomeCreditRefundable: eitc + }, + warnings, + selfEmploymentTax: seTax, + incomeTaxAfterCredits, + totalTax, + withholding: round2(withholding), + totalPayments, + refundOrBalance + }; +} + diff --git a/apps/forecaster/backend/package-lock.json b/apps/forecaster/backend/package-lock.json index 71cd2070..0885ef3a 100644 --- a/apps/forecaster/backend/package-lock.json +++ b/apps/forecaster/backend/package-lock.json @@ -16,7 +16,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.0.3", + "@types/node": "^25.0.8", "tsx": "^4.21.0", "typescript": "^5.9.3" } @@ -545,9 +545,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", + "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/apps/forecaster/backend/package.json b/apps/forecaster/backend/package.json index f677e3f8..3063e887 100644 --- a/apps/forecaster/backend/package.json +++ b/apps/forecaster/backend/package.json @@ -18,9 +18,8 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.0.3", + "@types/node": "^25.0.8", "tsx": "^4.21.0", "typescript": "^5.9.3" } } - diff --git a/apps/forecaster/backend/src/city/city-tax.ts b/apps/forecaster/backend/src/city/city-tax.ts index 6a52c45f..9610f9b6 100644 --- a/apps/forecaster/backend/src/city/city-tax.ts +++ b/apps/forecaster/backend/src/city/city-tax.ts @@ -57,8 +57,4 @@ export function calculateCityTax(input: CityTaxInput): CityTaxResult { refundOrBalance, warnings }; -} - - -} } \ No newline at end of file diff --git a/apps/forecaster/backend/src/index.ts b/apps/forecaster/backend/src/index.ts index a8e22c46..b184977b 100644 --- a/apps/forecaster/backend/src/index.ts +++ b/apps/forecaster/backend/src/index.ts @@ -23,7 +23,10 @@ app.use(express.json({ limit: '5mb' })); const ForecastRequestSchema = z.object({ forecast_input: z.object({ - year: z.union([z.literal(2023), z.literal(2024), z.literal(2025)]), + year: z.union([ + z.literal(2018), z.literal(2019), z.literal(2020), z.literal(2021), z.literal(2022), + z.literal(2023), z.literal(2024), z.literal(2025) + ]), filingStatus: z.enum(['single', 'married_joint', 'married_separate', 'head_of_household', 'qualifying_widow']), wages: z.number().nonnegative(), selfEmploymentIncome: z.number().optional(), @@ -248,10 +251,4 @@ const port = Number(process.env.PORT || 3000); app.listen(port, () => { // eslint-disable-next-line no-console console.log(`✅ Forecaster backend listening on http://localhost:${port}`); -}); - - -) -) -) -) \ No newline at end of file +}); \ No newline at end of file diff --git a/apps/forecaster/backend/src/pdf/pdf-filler.ts b/apps/forecaster/backend/src/pdf/pdf-filler.ts index e5a46e7c..eb37ae90 100644 --- a/apps/forecaster/backend/src/pdf/pdf-filler.ts +++ b/apps/forecaster/backend/src/pdf/pdf-filler.ts @@ -138,10 +138,4 @@ export async function applyBranding(params: { const bytes = await pdfDoc.save(); return { pdfBase64: toBase64(bytes) }; -} - - -} -} -} -) \ No newline at end of file +} \ No newline at end of file diff --git a/apps/forecaster/backend/src/tax/federal.ts b/apps/forecaster/backend/src/tax/federal.ts index 530d27de..6a2ce6cd 100644 --- a/apps/forecaster/backend/src/tax/federal.ts +++ b/apps/forecaster/backend/src/tax/federal.ts @@ -1,357 +1,19 @@ -export type FilingStatus = 'single' | 'married_joint' | 'married_separate' | 'head_of_household' | 'qualifying_widow'; - -export interface FederalTaxInput { - year: 2023 | 2024 | 2025; - filingStatus: FilingStatus; - wages: number; - selfEmploymentIncome?: number; - interestIncome?: number; - dividendIncome?: number; - capitalGains?: number; - otherIncome?: number; - itemizedDeductions?: number; - standardDeduction?: number; - // Credits (computed) inputs - qualifyingChildrenUnder17?: number; - otherDependents?: number; - // EITC eligibility inputs (required for 0-kid EITC; otherwise best-effort) - filerAge?: number; - spouseAge?: number; - canBeClaimedAsDependent?: boolean; - estimatedWithholding?: number; -} - -export interface FederalTaxResult { - year: 2023 | 2024 | 2025; - filingStatus: FilingStatus; - grossIncome: number; - adjustments: number; - adjustedGrossIncome: number; - deductionUsed: number; - taxableIncome: number; - taxBeforeCredits: number; - nonrefundableCredits: number; - refundableCredits: number; - creditsBreakdown: { - childTaxCreditNonrefundable: number; - otherDependentCreditNonrefundable: number; - additionalChildTaxCreditRefundable: number; - earnedIncomeCreditRefundable: number; - }; - warnings: string[]; - selfEmploymentTax: number; - incomeTaxAfterCredits: number; - totalTax: number; - withholding: number; - totalPayments: number; - refundOrBalance: number; -} - -type BracketsByStatus = Record; - -// Data source: Tax-Calculator (taxcalc) 6.3.0 policy parameters extracted locally. -const THRESHOLDS: Record<2023 | 2024 | 2025, BracketsByStatus> = { - 2023: { - single: [11000, 44725, 95375, 182100, 231250, 578125, Infinity], - married_joint: [22000, 89450, 190750, 364200, 462500, 693750, Infinity], - married_separate: [11000, 44725, 95375, 182100, 231250, 578125, Infinity], - head_of_household: [15700, 59850, 95350, 182100, 231250, 578100, Infinity], - qualifying_widow: [22000, 89450, 190750, 364200, 462500, 693750, Infinity] - }, - 2024: { - single: [11600, 47150, 100525, 191950, 243725, 609350, Infinity], - married_joint: [23200, 94300, 201050, 383900, 487450, 731200, Infinity], - married_separate: [11600, 47150, 100525, 191950, 243725, 365600, Infinity], - head_of_household: [16550, 63100, 100500, 191950, 243700, 609350, Infinity], - qualifying_widow: [23200, 94300, 201050, 383900, 487450, 731200, Infinity] - }, - 2025: { - single: [11925, 48475, 103350, 197300, 250525, 626350, Infinity], - married_joint: [23850, 96950, 206700, 394600, 501050, 751600, Infinity], - married_separate: [11925, 48475, 103350, 197300, 250525, 375800, Infinity], - head_of_household: [17000, 64850, 103350, 197300, 250500, 626350, Infinity], - qualifying_widow: [23850, 96950, 206700, 394600, 501050, 751600, Infinity] - } -}; - -const STD_DEDUCTION: Record<2023 | 2024 | 2025, Record> = { - 2023: { - single: 13850, - married_joint: 27700, - married_separate: 13850, - head_of_household: 20800, - qualifying_widow: 27700 - }, - 2024: { - single: 14600, - married_joint: 29200, - married_separate: 14600, - head_of_household: 21900, - qualifying_widow: 29200 - }, - 2025: { - single: 15750, - married_joint: 31500, - married_separate: 15750, - head_of_household: 23625, - qualifying_widow: 31500 - } -}; - -const SS_WAGE_BASE: Record<2023 | 2024 | 2025, number> = { - 2023: 160200, - 2024: 168600, - 2025: 176100 -}; - -// CTC / ODC / ACTC (taxcalc 6.3.0 extraction) -const CTC_AMOUNT: Record<2023 | 2024 | 2025, number> = { 2023: 2000, 2024: 2000, 2025: 2200 }; -const ODC_AMOUNT: Record<2023 | 2024 | 2025, number> = { 2023: 500, 2024: 500, 2025: 500 }; -const CTC_PHASEOUT_THRESHOLD: Record<2023 | 2024 | 2025, Record> = { - 2023: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, - 2024: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 }, - 2025: { single: 200000, married_joint: 400000, married_separate: 200000, head_of_household: 200000, qualifying_widow: 400000 } -}; -const ACTC_REFUNDABLE_PER_CHILD: Record<2023 | 2024 | 2025, number> = { 2023: 1600, 2024: 1700, 2025: 1700 }; -const ACTC_EARNED_INCOME_THRESHOLD: number = 2500; -const ACTC_EARNED_INCOME_RATE: number = 0.15; -const ACTC_REFUNDABLE_CHILD_LIMIT: number = 3; - -// EITC (taxcalc 6.3.0 extraction) -type EitcKids = 0 | 1 | 2 | 3; -const EITC_MAX_CREDIT: Record<2023 | 2024 | 2025, Record> = { - 2023: { 0: 600, 1: 3995, 2: 6604, 3: 7430 }, - 2024: { 0: 632, 1: 4213, 2: 6960, 3: 7830 }, - 2025: { 0: 649, 1: 4405, 2: 7280, 3: 8180 } -}; -const EITC_PHASE_IN_RATE: Record<2023 | 2024 | 2025, Record> = { - 2023: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, - 2024: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 }, - 2025: { 0: 0.0765, 1: 0.34, 2: 0.4, 3: 0.45 } -}; -const EITC_PHASE_OUT_RATE: Record<2023 | 2024 | 2025, Record> = { - 2023: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, - 2024: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 }, - 2025: { 0: 0.0765, 1: 0.1598, 2: 0.2106, 3: 0.2106 } -}; -const EITC_PHASE_OUT_START: Record<2023 | 2024 | 2025, Record> = { - 2023: { 0: 9800, 1: 21560, 2: 21560, 3: 21560 }, - 2024: { 0: 10330, 1: 22720, 2: 22720, 3: 22720 }, - 2025: { 0: 10620, 1: 23370, 2: 23370, 3: 23370 } -}; -const EITC_PHASE_OUT_MARRIED_ADDON: Record<2023 | 2024 | 2025, number> = { 2023: 6570, 2024: 6920, 2025: 7110 }; -const EITC_INVESTMENT_INCOME_LIMIT: Record<2023 | 2024 | 2025, number> = { 2023: 11000, 2024: 11600, 2025: 11950 }; -const EITC_MIN_AGE = 25; -const EITC_MAX_AGE = 64; - -const RATES = [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37] as const; - -function round2(n: number) { - return Math.round(n * 100) / 100; -} - -function calcTaxFromBrackets(taxableIncome: number, thresholds: number[]): number { - let tax = 0; - let prev = 0; - for (let i = 0; i < RATES.length; i++) { - const max = thresholds[i] ?? Infinity; - const amount = Math.max(0, Math.min(taxableIncome, max) - prev); - tax += amount * RATES[i]; - prev = max; - if (taxableIncome <= max) break; - } - return round2(tax); -} - -function calcSelfEmploymentTax(seIncome: number, wages: number, year: 2023 | 2024 | 2025): number { - const net = seIncome * 0.9235; - const remainingSS = Math.max(0, SS_WAGE_BASE[year] - Math.max(0, wages)); - const ssTax = Math.min(net, remainingSS) * 0.124; - const medicareTax = net * 0.029; - return round2(ssTax + medicareTax); -} - -function clampKids(n: number): EitcKids { - if (n <= 0) return 0; - if (n === 1) return 1; - if (n === 2) return 2; - return 3; -} - -function computeEitc(params: { - year: 2023 | 2024 | 2025; - filingStatus: FilingStatus; - agi: number; - earnedIncome: number; - investmentIncome: number; - qualifyingKids: number; - filerAge?: number; - spouseAge?: number; - canBeClaimedAsDependent?: boolean; - warnings: string[]; -}): number { - const { year, filingStatus, agi, earnedIncome, investmentIncome, warnings } = params; - - // In most cases, MFS is ineligible. (There are narrow exceptions; not implemented.) - if (filingStatus === 'married_separate') { - warnings.push('EITC not computed for Married Filing Separately in this prototype (treated as ineligible).'); - return 0; - } - - const invLimit = EITC_INVESTMENT_INCOME_LIMIT[year]; - if (investmentIncome > invLimit) { - warnings.push(`EITC disallowed because investment income exceeds limit (${invLimit}).`); - return 0; - } - - const kids = clampKids(params.qualifyingKids); - - // Age/dependency rules for 0-kid EITC (simplified) - if (kids === 0) { - if (params.canBeClaimedAsDependent) { - warnings.push('EITC (0 children) disallowed because filer can be claimed as a dependent.'); - return 0; - } - const age = params.filerAge; - if (typeof age !== 'number') { - warnings.push('EITC (0 children) requires filer age; not provided so credit computed as 0.'); - return 0; - } - if (age < EITC_MIN_AGE || age > EITC_MAX_AGE) { - warnings.push(`EITC (0 children) requires age between ${EITC_MIN_AGE} and ${EITC_MAX_AGE}; outside range so credit computed as 0.`); - return 0; - } - if (filingStatus === 'married_joint' && typeof params.spouseAge === 'number') { - if (params.spouseAge < EITC_MIN_AGE || params.spouseAge > EITC_MAX_AGE) { - warnings.push(`EITC (0 children) spouse age outside ${EITC_MIN_AGE}-${EITC_MAX_AGE}; credit computed as 0.`); - return 0; - } - } - } - - const maxCredit = EITC_MAX_CREDIT[year][kids]; - const phaseInRate = EITC_PHASE_IN_RATE[year][kids]; - const phaseOutRate = EITC_PHASE_OUT_RATE[year][kids]; - const basePhaseOutStart = EITC_PHASE_OUT_START[year][kids]; - const phaseOutStart = basePhaseOutStart + (filingStatus === 'married_joint' ? EITC_PHASE_OUT_MARRIED_ADDON[year] : 0); - - const phaseInCredit = Math.min(maxCredit, earnedIncome * phaseInRate); - const incomeForPhaseout = Math.max(agi, earnedIncome); - const phaseOut = Math.max(0, incomeForPhaseout - phaseOutStart) * phaseOutRate; - return round2(Math.max(0, phaseInCredit - phaseOut)); -} - -export function calculateFederal(input: FederalTaxInput): FederalTaxResult { - const warnings: string[] = []; - - const seIncome = Number(input.selfEmploymentIncome || 0); - const interest = Number(input.interestIncome || 0); - const dividends = Number(input.dividendIncome || 0); - const capGains = Number(input.capitalGains || 0); - const otherIncome = Number(input.otherIncome || 0); - const wages = Number(input.wages || 0); - const withholding = Number(input.estimatedWithholding || 0); - - const grossIncome = wages + seIncome + interest + dividends + capGains + otherIncome; - - const seTax = calcSelfEmploymentTax(seIncome, wages, input.year); - const adjustments = seTax * 0.5; // 1/2 SE tax deduction (simplified) - const agi = grossIncome - adjustments; - - const standard = input.standardDeduction && input.standardDeduction > 0 - ? input.standardDeduction - : STD_DEDUCTION[input.year][input.filingStatus]; - const itemized = Number(input.itemizedDeductions || 0); - const deductionUsed = Math.max(standard, itemized); - - const taxableIncome = Math.max(0, agi - deductionUsed); - const taxBeforeCredits = calcTaxFromBrackets(taxableIncome, THRESHOLDS[input.year][input.filingStatus]); - - // ------------------------------------------------------------ - // Credits (CTC/ODC + ACTC + EITC) — computed using taxcalc policy parameters - // ------------------------------------------------------------ - - const qualifyingChildrenUnder17 = Math.max(0, Math.floor(Number(input.qualifyingChildrenUnder17 || 0))); - const otherDependents = Math.max(0, Math.floor(Number(input.otherDependents || 0))); - - const baseCTC = qualifyingChildrenUnder17 * CTC_AMOUNT[input.year]; - const baseODC = otherDependents * ODC_AMOUNT[input.year]; - const baseTotalCtcOdc = baseCTC + baseODC; - - // CTC/ODC phaseout: $50 per $1,000 (or part) over threshold (simplified MAGI≈AGI here) - const threshold = CTC_PHASEOUT_THRESHOLD[input.year][input.filingStatus]; - const excess = Math.max(0, agi - threshold); - const reduction = Math.ceil(excess / 1000) * 50; - - const remainingCTC = Math.max(0, baseCTC - reduction); - const remainingODC = Math.max(0, baseODC - Math.max(0, reduction - baseCTC)); - - // Nonrefundable credits can reduce income tax (not SE tax) - const nonrefundableAvailable = remainingCTC + remainingODC; - const nonrefundableUsed = Math.min(taxBeforeCredits, nonrefundableAvailable); - const incomeTaxAfterCredits = round2(Math.max(0, taxBeforeCredits - nonrefundableUsed)); - - // Refundable ACTC is limited (simplified) and applies only to remaining CTC portion - const usedNonrefundableFromCTC = Math.min(remainingCTC, nonrefundableUsed); - const ctcLeftAfterNonrefundable = Math.max(0, remainingCTC - usedNonrefundableFromCTC); - const refundableChildCount = Math.min(qualifyingChildrenUnder17, ACTC_REFUNDABLE_CHILD_LIMIT); - const refundableCap = refundableChildCount * ACTC_REFUNDABLE_PER_CHILD[input.year]; - const earnedIncome = wages + seIncome * 0.9235; - const refundableByEarnedIncome = Math.max(0, (earnedIncome - ACTC_EARNED_INCOME_THRESHOLD) * ACTC_EARNED_INCOME_RATE); - const actc = round2(Math.min(ctcLeftAfterNonrefundable, refundableCap, refundableByEarnedIncome)); - - // Refundable EITC (best-effort) - const investmentIncome = interest + dividends + capGains; - const eitc = computeEitc({ - year: input.year, - filingStatus: input.filingStatus, - agi, - earnedIncome, - investmentIncome, - qualifyingKids: qualifyingChildrenUnder17, - filerAge: input.filerAge, - spouseAge: input.spouseAge, - canBeClaimedAsDependent: input.canBeClaimedAsDependent, - warnings - }); - - const refundableCredits = actc + eitc; - const totalPayments = round2(withholding + refundableCredits); - - const totalTax = round2(incomeTaxAfterCredits + seTax); - const refundOrBalance = round2(totalPayments - totalTax); - - return { - year: input.year, - filingStatus: input.filingStatus, - grossIncome: round2(grossIncome), - adjustments: round2(adjustments), - adjustedGrossIncome: round2(agi), - deductionUsed: round2(deductionUsed), - taxableIncome: round2(taxableIncome), - taxBeforeCredits, - nonrefundableCredits: round2(nonrefundableUsed), - refundableCredits: round2(refundableCredits), - creditsBreakdown: { - childTaxCreditNonrefundable: round2(usedNonrefundableFromCTC), - otherDependentCreditNonrefundable: round2(Math.max(0, nonrefundableUsed - usedNonrefundableFromCTC)), - additionalChildTaxCreditRefundable: actc, - earnedIncomeCreditRefundable: eitc - }, - warnings, - selfEmploymentTax: seTax, - incomeTaxAfterCredits, - totalTax, - withholding: round2(withholding), - totalPayments, - refundOrBalance - }; -} - - -} -} -} -} -) -} \ No newline at end of file +/** + * Federal Tax Calculation - Re-exported from Cyrano + * + * This file now imports from Cyrano's shared tax calculation formulas + * to ensure both the standalone backend and Cyrano HTTP bridge use + * the same implementation with complete credit calculations. + */ + +// Import from Cyrano's shared tax formulas +import { + calculateFederal, + type FederalTaxInput, + type FederalTaxResult, + type FilingStatus, +} from '../../../../../Cyrano/src/modules/forecast/formulas/tax-formulas.js'; + +// Re-export for backward compatibility +export type { FederalTaxInput, FederalTaxResult, FilingStatus }; +export { calculateFederal }; diff --git a/apps/forecaster/backend/tsconfig.json b/apps/forecaster/backend/tsconfig.json index 541b656e..2997c793 100644 --- a/apps/forecaster/backend/tsconfig.json +++ b/apps/forecaster/backend/tsconfig.json @@ -5,13 +5,12 @@ "module": "ESNext", "moduleResolution": "bundler", "outDir": "dist", - "rootDir": "src", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, "types": ["node"] }, - "include": ["src"] + "include": ["src", "../../../Cyrano/src/modules/forecast/formulas/tax-formulas.ts"] } diff --git a/apps/forecaster/frontend/src/pages/CityTaxForecast.tsx b/apps/forecaster/frontend/src/pages/CityTaxForecast.tsx index 4625f157..7aed5122 100644 --- a/apps/forecaster/frontend/src/pages/CityTaxForecast.tsx +++ b/apps/forecaster/frontend/src/pages/CityTaxForecast.tsx @@ -51,13 +51,20 @@ export default function CityTaxForecast() { const step1 = window.confirm( [ - 'DANGER ZONE: Remove advisory/branding from generated forms?', + 'Remove advisory/branding from generated forms?', '', - 'These outputs are projections and are NOT real filings.', - 'If you remove the advisory, the PDFs may be easily mistaken for filing-ready documents.', - 'Consult a qualified professional before relying on these outputs.', + 'LexFiat Forecaster simulates tax preparation software. Outputs are', + 'consistent with applicable law and tax formulas, but are not intended', + 'for actual tax preparation.', '', - 'Click OK to continue, or Cancel to keep the advisory.', + 'These forecasts are for litigation and planning purposes only.', + 'Removing the advisory may cause outputs to be mistaken for', + 'filing-ready documents by courts, opposing counsel, or clients.', + '', + 'The user assumes responsibility for any consequences of using', + 'unlabeled outputs.', + '', + 'Click OK to acknowledge these risks, or Cancel to keep the advisory.', ].join('\n') ); if (!step1) { @@ -65,7 +72,7 @@ export default function CityTaxForecast() { return; } - const step2 = window.prompt('Type REMOVE to confirm you want to remove the advisory (expires after 24 hours):', ''); + const step2 = window.prompt('Type REMOVE to confirm removal of the advisory (expires after 24 hours):', ''); if (step2 !== 'REMOVE') { setBrandingMode('strip'); return; @@ -309,8 +316,4 @@ export default function CityTaxForecast() { )} ); -} - - -} -) \ No newline at end of file +} \ No newline at end of file diff --git a/apps/forecaster/frontend/src/pages/TaxForecast.tsx b/apps/forecaster/frontend/src/pages/TaxForecast.tsx index b969725b..a5d4eadf 100644 --- a/apps/forecaster/frontend/src/pages/TaxForecast.tsx +++ b/apps/forecaster/frontend/src/pages/TaxForecast.tsx @@ -62,13 +62,20 @@ export default function TaxForecast() { // 2-layer acknowledgement before allowing advisory removal const step1 = window.confirm( [ - 'DANGER ZONE: Remove advisory/branding from generated forms?', + 'Remove advisory/branding from generated forms?', '', - 'These outputs are projections and are NOT real filings.', - 'If you remove the advisory, the PDFs may be easily mistaken for filing-ready documents.', - 'Consult a qualified tax professional before relying on these outputs.', + 'LexFiat Forecaster simulates tax preparation software. Outputs are', + 'consistent with applicable law and tax formulas, but are not intended', + 'for actual tax preparation.', '', - 'Click OK to continue, or Cancel to keep the advisory.', + 'These forecasts are for litigation and planning purposes only.', + 'Removing the advisory may cause outputs to be mistaken for', + 'filing-ready documents by courts, opposing counsel, or clients.', + '', + 'The user assumes responsibility for any consequences of using', + 'unlabeled outputs.', + '', + 'Click OK to acknowledge these risks, or Cancel to keep the advisory.', ].join('\n') ); if (!step1) { @@ -76,7 +83,7 @@ export default function TaxForecast() { return; } - const step2 = window.prompt('Type REMOVE to confirm you want to remove the advisory (expires after 24 hours):', ''); + const step2 = window.prompt('Type REMOVE to confirm removal of the advisory (expires after 24 hours):', ''); if (step2 !== 'REMOVE') { setBrandingMode('strip'); return; @@ -493,8 +500,4 @@ export default function TaxForecast() { )} ); -} - - -} -) \ No newline at end of file +} \ No newline at end of file diff --git a/apps/lexfiat/client/src/App.tsx b/apps/lexfiat/client/src/App.tsx index acfd6c7c..4109f597 100644 --- a/apps/lexfiat/client/src/App.tsx +++ b/apps/lexfiat/client/src/App.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from "react"; -import { Switch, Route } from "wouter"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; @@ -55,29 +55,31 @@ const PageLoader = () => ( function Router() { return ( - }> - - - - - - - - - - - - - - - - - - - - - - + + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/apps/lexfiat/client/src/components/dashboard/profile-panel.tsx b/apps/lexfiat/client/src/components/dashboard/profile-panel.tsx index ee1ae107..46401b82 100644 --- a/apps/lexfiat/client/src/components/dashboard/profile-panel.tsx +++ b/apps/lexfiat/client/src/components/dashboard/profile-panel.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect } from "react"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { User, Mail, Briefcase, Edit, Phone, MapPin, Save } from "lucide-react"; -import { Link, useLocation } from "wouter"; +import { Link, useNavigate } from "react-router-dom"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; @@ -24,7 +24,7 @@ interface ProfilePanelProps { } export default function ProfilePanel({ isOpen, onClose, attorney }: ProfilePanelProps) { - const [, setLocation] = useLocation(); + const navigate = useNavigate(); const { toast } = useToast(); const queryClient = useQueryClient(); const [isEditing, setIsEditing] = useState(false); @@ -281,8 +281,8 @@ export default function ProfilePanel({ isOpen, onClose, attorney }: ProfilePanel )} - -
setLocation("/settings")}> + +
navigate("/settings")}>

Full Settings Page

diff --git a/apps/lexfiat/client/src/components/dashboard/settings-panel.tsx b/apps/lexfiat/client/src/components/dashboard/settings-panel.tsx index 090ce30b..0e3f7bac 100644 --- a/apps/lexfiat/client/src/components/dashboard/settings-panel.tsx +++ b/apps/lexfiat/client/src/components/dashboard/settings-panel.tsx @@ -7,7 +7,7 @@ import React, { useState } from "react"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Settings, Bell, Palette, Globe, Link as LinkIcon, Bot } from "lucide-react"; -import { useLocation } from "wouter"; +import { useNavigate } from "react-router-dom"; import { useTheme } from "@/components/theme/theme-provider"; import { ThemeSelector } from "@/components/theme/theme-selector"; import { ViewModeSelector } from "@/components/dashboard/view-mode-selector"; @@ -18,12 +18,12 @@ interface SettingsPanelProps { } export default function SettingsPanel({ isOpen, onClose }: SettingsPanelProps) { - const [, setLocation] = useLocation(); + const navigate = useNavigate(); const { theme } = useTheme(); const [activeSection, setActiveSection] = useState(null); const handleNavigate = (path: string, tab?: string) => { - setLocation(path + (tab ? `?tab=${tab}` : "")); + navigate(path + (tab ? `?tab=${tab}` : "")); onClose(); }; diff --git a/apps/lexfiat/client/src/pages/onboarding.tsx b/apps/lexfiat/client/src/pages/onboarding.tsx index 43877f6d..02590586 100644 --- a/apps/lexfiat/client/src/pages/onboarding.tsx +++ b/apps/lexfiat/client/src/pages/onboarding.tsx @@ -4,8 +4,8 @@ * See LICENSE.md for full license text */ -import { useState, useEffect } from 'react'; -import { useLocation } from 'wouter'; +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { ChevronRight, ChevronLeft, @@ -71,7 +71,7 @@ interface OnboardingFormData { } export default function Onboarding() { - const [, setLocation] = useLocation(); + const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [formData, setFormData] = useState({ primaryJurisdiction: '', @@ -310,7 +310,7 @@ export default function Onboarding() { // Small delay to let fanfare start, then redirect setTimeout(() => { - setLocation('/dashboard'); + navigate('/dashboard'); }, 500); } catch (error) { console.error('Failed to save onboarding data:', error); diff --git a/apps/lexfiat/client/src/pages/performance.tsx b/apps/lexfiat/client/src/pages/performance.tsx index b6e75ec0..d252950c 100644 --- a/apps/lexfiat/client/src/pages/performance.tsx +++ b/apps/lexfiat/client/src/pages/performance.tsx @@ -4,13 +4,15 @@ * See LICENSE.md for full license text */ +import React from "react"; import { useQuery } from "@tanstack/react-query"; import { TrendingUp, Clock, CheckCircle, FileText, Zap } from "lucide-react"; -import { Link } from "wouter"; +import { Link } from "react-router-dom"; import Header from "@/components/layout/header"; import { getWorkflowData } from "@/lib/cyrano-api"; import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; export default function PerformancePage() { const { data: workflowData, isLoading } = useQuery({ @@ -39,9 +41,9 @@ export default function PerformancePage() {

Performance Analytics

Track your productivity and automation metrics

- - - +
diff --git a/apps/lexfiat/client/src/pages/settings.tsx b/apps/lexfiat/client/src/pages/settings.tsx index 39dbf583..edc81fda 100644 --- a/apps/lexfiat/client/src/pages/settings.tsx +++ b/apps/lexfiat/client/src/pages/settings.tsx @@ -1,3 +1,4 @@ +import React, { useState, useEffect } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AvatarUpload } from "@/components/dashboard/avatar-upload"; import { IntegrationSettings } from "@/components/dashboard/integration-settings"; @@ -13,8 +14,7 @@ import { Settings, User, Link, Bot, MessageSquare, Shield, Bell, ArrowLeft, Scal import { EthicsDashboard } from "@/components/ethics/ethics-dashboard"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; -import { useState, useEffect } from "react"; -import { Link as RouterLink } from "wouter"; +import { Link } from "react-router-dom"; import { useTheme } from "@/components/theme/theme-provider"; import { ThemeName } from "@/lib/theme"; @@ -90,13 +90,13 @@ export default function SettingsPage() {
- + - +

diff --git a/apps/lexfiat/client/src/pages/todays-focus.tsx b/apps/lexfiat/client/src/pages/todays-focus.tsx index 889baf9e..321d6abc 100644 --- a/apps/lexfiat/client/src/pages/todays-focus.tsx +++ b/apps/lexfiat/client/src/pages/todays-focus.tsx @@ -4,9 +4,10 @@ * See LICENSE.md for full license text */ +import React from "react"; import { useQuery } from "@tanstack/react-query"; import { Calendar, Clock, AlertTriangle, FileText } from "lucide-react"; -import { Link } from "wouter"; +import { Link } from "react-router-dom"; import Header from "@/components/layout/header"; import { getCases } from "@/lib/cyrano-api"; import { Skeleton } from "@/components/ui/skeleton"; @@ -45,7 +46,7 @@ export default function TodaysFocusPage() {

Today's Focus

Priority tasks and deadlines for today

- +
diff --git a/apps/lexfiat/dashboard.tsx b/apps/lexfiat/dashboard.tsx index f48f1da9..7df0dcc3 100644 --- a/apps/lexfiat/dashboard.tsx +++ b/apps/lexfiat/dashboard.tsx @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Settings, Clock, FileText, Users, Zap, Shield, TrendingUp, AlertCircle } from "lucide-react"; -import { Link } from "wouter"; +import { Link } from "react-router-dom"; import { useState } from "react"; import Header from "@/components/layout/header"; import { GoodCounselWidget } from "@/components/dashboard/good-counsel-widget"; @@ -77,7 +77,7 @@ export default function Dashboard() {

Automated workflow • Human oversight

- +