Blog: Silent Data Loss in Onboarding — Taxpayer Type Fix
Bug Post-Mortem: Onboarding Taxpayer Type Was Silently Discarded
Release: v1.0.462
Severity: High — incorrect user classification, no visible error
Status: Fixed ✅
What Happened
During onboarding, users are asked to identify themselves as either a Landlord or Self-Employed taxpayer. This selection drives which MTD ITSA workflow is presented — property income submissions versus self-employment income submissions. Getting this right from the start is critical.
A bug in TaxpayerTypeStep.handleSubmit inside onboarding-client.tsx meant that this selection was never actually saved.
The Root Cause
The handler passed taxpayerType to the org.update mutation using an unsafe type cast:
updateOrg.mutateAsync({
orgId,
...({ taxpayerType: selected } as Record<string, unknown>)
} as Parameters<typeof updateOrg.mutateAsync>[0])
This pattern uses two successive type casts to force an unrecognised field past TypeScript's compile-time checks. At the boundary of the mutation, the payload is validated against a Zod schema — and that schema did not include taxpayerType as a declared field.
Zod v3 applies .strip() to object schemas by default, which means any key not listed in the schema is quietly removed before processing continues. The mutation succeeded, the database write succeeded, but taxpayerType was gone before it ever reached a database column.
Why It Was Hard to Spot
- No error was thrown. Zod stripping is silent by design — it is the default "safe" behaviour for accepting partial updates without crashing.
- The mutation returned success. From the client's perspective, the save worked.
- The UI moved forward. The onboarding step completed normally, giving users no reason to suspect anything was wrong.
- The symptom appeared later. Users only noticed something was off when the wrong workflow appeared — and by then, the connection to the onboarding step was non-obvious.
Impact
Any user who completed onboarding and selected Self-Employed as their taxpayer type after this code path was introduced would have their taxpayerType remain as null or 'landlord'. They would be shown the Landlord property-income workflow rather than the Self-Employed workflow, meaning:
- Incorrect income/expense categories presented
- Potentially wrong quarterly submission structure
- MTD ITSA compliance risk if submissions were made under the wrong classification
The Fix
The fix has two parts:
- Schema update:
taxpayerTypeis now an explicitly declared, typed, and validated field in theorg.updateZod schema. - Handler update: The unsafe type cast in
TaxpayerTypeStep.handleSubmithas been removed. The field is passed directly in the correctly typed payload with no casting required.
This ensures that TypeScript enforces the shape at compile time, Zod validates it at runtime, and the value is preserved through to the database write.
What You Should Do
If you set up your account during the affected period and chose Self-Employed:
- Check your account settings to confirm your taxpayer type is correctly recorded.
- If it shows Landlord and you are self-employed, update it in your account settings or contact support.
- Review any quarterly submissions made under the incorrect classification with your accountant or tax adviser.
We apologise for this issue. Silent data loss of this kind — where the UI signals success but the data is not saved — is one of the harder failure modes to detect and one we take seriously. We are reviewing other mutation call sites to ensure no similar patterns exist elsewhere in the codebase.
Fixed in v1.0.462 · src/app/onboarding/onboarding-client.tsx