Add tax features: summary report, deductible expenses, invoice tax fix, CSV export

- Add taxDeductible boolean to expenses schema + migration 0002
- Update expenses router, form, and list to support tax-deductible flag
- Fix invoice-view tax calculation (was hardcoded $0.00; now uses taxRate)
- New Tax Summary tab in Reports: year selector, income/deductions breakdown,
  SE tax + federal income estimates, quarterly bar chart
- CSV export for accountant with income + expense rows and tax summary

https://claude.ai/code/session_012sqEgNQpx676isepeoX4Mi
This commit is contained in:
Claude
2026-04-05 03:21:08 +00:00
parent 1f76cf38a7
commit 74f9696023
8 changed files with 409 additions and 175 deletions
+1
View File
@@ -15,6 +15,7 @@ const createExpenseSchema = z.object({
category: z.string().optional().or(z.literal("")),
billable: z.boolean().default(false),
reimbursable: z.boolean().default(false),
taxDeductible: z.boolean().default(false),
notes: z.string().optional().or(z.literal("")),
clientId: z.string().optional().or(z.literal("")),
businessId: z.string().optional().or(z.literal("")),
+10
View File
@@ -160,6 +160,16 @@ async function isMigrationApplied(client: Pool, tag: string): Promise<boolean> {
`);
return parseInt(rows[0]?.count ?? "0") > 0;
}
if (tag === "0002_tax_deductible") {
// 0002 adds taxDeductible to beenvoice_expense
const { rows } = await client.query<{ count: string }>(`
SELECT COUNT(*)::text AS count FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'beenvoice_expense'
AND column_name = 'taxDeductible'
`);
return parseInt(rows[0]?.count ?? "0") > 0;
}
// Unknown migration — assume not applied so it runs
return false;
}
+1
View File
@@ -334,6 +334,7 @@ export const expenses = createTable(
category: d.varchar({ length: 100 }),
billable: d.boolean().default(false).notNull(),
reimbursable: d.boolean().default(false).notNull(),
taxDeductible: d.boolean().default(false).notNull(),
notes: d.varchar({ length: 500 }),
createdById: d
.varchar({ length: 255 })