Critical fix: Self-employed taxpayer type now correctly saved during onboarding
Critical fix: Self-employed taxpayer type now correctly saved during onboarding
Release: v1.0.461
Affected users: Anyone who completed onboarding as self-employed or "both" (self-employed + landlord)
What happened
During the onboarding flow, users select their taxpayer type — Landlord, Self-employed, or Both. This choice controls which parts of the MTD platform are shown: landlord users see property income workflows, self-employed users see SE transactions and SE quarterly submissions, and "both" users see everything.
A bug introduced by an unsafe TypeScript type cast meant that the taxpayer type selection was silently discarded before being written to the database. The TaxpayerTypeStep and SeProfileStep components were routing this data through the generic org.update mutation, whose input schema does not include taxpayer-type fields. Zod's default .strip() behaviour removed the unrecognised fields without raising an error, so the mutation appeared to succeed while the data was quietly lost.
The practical effect: every self-employed or "both" user who completed onboarding had taxpayerType = "landlord" stored, regardless of what they selected. SE-specific UI — SE transactions, SE quarterly summaries, SE reports — was permanently hidden for them.
What changed
Two new dedicated tRPC mutations have been introduced specifically for the onboarding flow:
onboarding.setTaxpayerType
Accepts { orgId, taxpayerType } where taxpayerType is strictly typed as "landlord" | "self_employed" | "both". Writes directly to the organizations table with a tenant-isolation check.
onboarding.setSeProfile
Accepts { orgId, seNatureOfTrade, seTradeName? } and writes self-employment trade details directly to the organizations table, also with a tenant-isolation guard.
The onboarding UI (onboarding-client.tsx) now calls these mutations directly instead of attempting to smuggle extra fields through the generic org.update mutation.
Who is affected
- New users completing onboarding from v1.0.461 onwards are not affected — the fix is in place.
- Existing users who completed onboarding as self-employed or "both" before this release likely have
taxpayerType = "landlord"stored incorrectly and will need to correct it.
Remediation steps for affected users
If you selected Self-employed or Both during onboarding but the SE MTD features (SE transactions, SE quarterly summaries, SE reports) are not visible in your account:
- Navigate to Settings → Tax Profile.
- Re-select your correct taxpayer type.
- Save the change.
Your SE MTD workflows will be unlocked immediately after saving.
Technical detail
The root cause was a pattern of using TypeScript's type system to work around Zod's runtime validation:
// The unsafe pattern — Zod strips unknown fields before the DB write
await updateOrg.mutateAsync({
orgId,
...({ taxpayerType: selected } as Record<string, unknown>)
} as Parameters<typeof updateOrg.mutateAsync>[0]);
Zod's .strip() is the correct default for API boundaries — it prevents unexpected data from reaching the database. The fix is to define schemas that explicitly include the fields you intend to write, rather than routing data through an unrelated mutation and casting away type errors.
// The correct pattern — dedicated mutation with a matching schema
await setTaxpayerType.mutateAsync({ orgId, taxpayerType: selected });
Both new mutations also enforce orgId === ctx.orgId server-side, ensuring that one tenant cannot overwrite another tenant's taxpayer profile.