feat(accounts): add tax treatment classification (Taxable / Tax Deferred / Tax Free)#840
feat(accounts): add tax treatment classification (Taxable / Tax Deferred / Tax Free)#840Lunchtime0614 wants to merge 2 commits intoafadil:mainfrom
Conversation
Adds a per-account tax treatment classification (Taxable, Tax Deferred, Tax Free) so users can categorize accounts by how their gains are taxed and see portfolio breakdowns by tax status. Backend (Rust): - New Diesel migration adds non-null tax_treatment TEXT column on accounts (defaults to 'TAXABLE' so existing rows are preserved). - Adds TaxTreatment enum to wealthfolio-core accounts model with SCREAMING_SNAKE_CASE serde rename, serialized on Account, NewAccount (with serde default) and AccountUpdate (optional for partial updates). - Wires the new field through the sqlite repository (including update-path preservation when the caller omits it), the HTTP models in wealthfolio-server, and the connect broker service. Frontend (React): - Account form exposes a tax treatment select (Taxable / Tax Deferred / Tax Free). Edit modal, archive, and hide flows preserve the value. - Zod schema and adapter layer carry taxTreatment through the update_account IPC call. - New TaxTreatmentDonutChart on the Holdings Insights page shows portfolio value grouped by account tax treatment. - AccountsSummary dashboard card renders a per-group tax treatment breakdown inside expanded account groups. This feature was implemented entirely with Claude Code.
|
Thanks for putting this together — the UI (dashboard breakdown + insights donut) is a nice addition. I've been working on a larger change that's also focused on tax (entity modeling, jurisdictional rule groups, transaction-level withholding), so I hope you don't mind some feedback from a fellow contributor. A few things jumped out that might be worth looking at before this would be merged — none are blockers for the concept, just things I noticed: 1. pub fn from_str(s: &str) -> Self {
match s {
"TAX_FREE" => TaxTreatment::TaxFree,
"TAX_DEFERRED" => TaxTreatment::TaxDeferred,
_ => TaxTreatment::Taxable,
}
}If the column ever drifts (manual edit, sync from a newer device, future variant) this silently rewrites the value to 2. Two conflicting defaults on update (
// adapters/shared/accounts.ts
const { taxTreatment, ...updatePayload } = payload;
const finalPayload = taxTreatment ? { ...updatePayload, taxTreatment } : updatePayload;Three layers working around the same issue. Could be simpler if the DB model field were 3. Archive/hide handlers can overwrite updateAccountMutation.mutate({
...account,
taxTreatment: (account.taxTreatment || "TAXABLE") as ...,
isArchived: archive,
});If 4. Sync replay may reject the new column on older devices The sync payload layer ( 5.
6. New-account form marks
Thanks again for contributing this! |
Cleans up the tax treatment feature in response to PR review feedback: - Make `TaxTreatment::from_str` strict via `FromStr` (returns a validation error on unknown input instead of silently falling back to `Taxable`). The DB layer and the server HTTP model still default to `Taxable` for forward-compat with unexpected values, but now log a warning on the fallback path so drift is visible. - Drop the `"TAXABLE"` default in `From<AccountUpdate>` and the TS adapter's strip-and-re-add workaround. The repository's `tax_treatment_provided` flag is now the single source of truth for preservation on update. When the caller omits `taxTreatment`, JSON.stringify drops the undefined key, serde resolves to `None`, and the repository copies the existing DB value. - Remove the `taxTreatment: (... || "TAXABLE")` fallback from the archive/hide handlers so toggling those flags can't accidentally flip a legacy row's tax treatment to `TAXABLE`. - Remove the unused `groupingMode` state from `SettingsProvider` and `SettingsContextType` (leftover scaffolding with no readers). - Add missing `tax_treatment` field to test fixtures in `net_worth_service_tests.rs` and `snapshot_service_tests.rs` that the original feature commit missed.
|
Thanks for the review — I am still very new to all of this and with the help of Claude AI again, I reviewed your comments and had Claude make some changes. I agree with your comments - I was coming at this from a self hosted perspective. I've just pushed a follow-up commit addressing points 1, 2, 3, and 5, with 4 and 6 left as discussion points below. 1. 2. Conflicting defaults on update — You were right that this was three layers patching the same hole. I dropped the 3. Archive/hide handlers overwriting 4. Sync payload rejection on older devices — Good catch, and honestly the trickiest one to address retroactively since the older devices are already out there. I didn't change anything here yet — my read is that the cleanest path forward is to document in the release notes that syncing devices need to upgrade together for this release. An 5. Unused 6. New-account form required vs. backend default — I left this as-is for now because the current combo (UI requires, backend defaults to Thanks again for taking the time. Looking forward to seeing your larger tax PR — sounds like there'd be useful overlap. |
Summary
Adds a per-account tax treatment classification so users can categorize
accounts by how their gains are taxed (Taxable / Tax Deferred / Tax Free)
and view portfolio breakdowns by tax status.
Features
Backend (Rust)
2026-04-01-000001_add_tax_treatmentadds a non-nulltax_treatment TEXTcolumn onaccounts, defaulting toTAXABLEsoexisting rows are preserved on upgrade.
TaxTreatmentenum inwealthfolio-coreaccounts model withSCREAMING_SNAKE_CASEserde rename. Variants:Taxable(default),TaxDeferred,TaxFree.Account,NewAccount(with#[serde(default)]for backwards compat), and
AccountUpdate(optional for partial updates).wealthfolio-storage-sqliterepository, includingupdate-path preservation — if the caller omits
tax_treatmentin anAccountUpdate, the existing value is kept rather than clobbered.wealthfolio-serverand the connect broker serviceboth carry the new field.
Frontend (React / TypeScript)
options. Preserved across edit, archive, and hide flows.
taxTreatmentthrough theupdate_accountIPC call.
TaxTreatmentDonutChartwidget showingportfolio value grouped by the account's tax treatment.
per-group tax treatment breakdown (Taxable / Tax Deferred / Tax Free
subtotals) inside the accordion.
Test plan
cargo check -p wealthfolio-core -p wealthfolio-storage-sqlite -p wealthfolio-server -p wealthfolio-connectpassestsc --noEmitclean on every modified frontend fileNOT NULL DEFAULT 'TAXABLE'so existing rows upgrade cleanlyBackwards compatibility
TAXABLE— no data loss, no manual steps.NewAccount::tax_treatmentuses#[serde(default)]so older HTTP clients still work.AccountUpdate::tax_treatmentisOption<...>, so partial updates that don't touch the field leave it untouched.Notes
afadil/wealthfolio@mainat v3.2.1, so no merge conflicts expected.