[PM-27010] fix: Resolve tax ID type from country and value, with format guidance - #22388
[PM-27010] fix: Resolve tax ID type from country and value, with format guidance#22388amorask-bitwarden wants to merge 8 commits into
Conversation
The web client resolved a tax ID's Stripe type from the billing country alone, so multi-format countries like Canada always resolved to the first tax-impacting type and a bare value (e.g. a Canadian business number) was tagged with the wrong type. Add an optional per-type format regex, ported verbatim from the server's TaxService, and make getTaxIdTypeForCountry match the entered value against it, mirroring the server's resolution. First piece of PM-27010; the submit-path wiring and format-guidance UX follow in later commits.
The format hint was an observable whose async pipe subscribes lazily inside the field's visibility @if, so it missed the country change that revealed the field and fell back to a stale value, leaking the US EIN example into Canada. Replace it with a getter that reads the live form values, and exclude the US. Reword the hint to "Recognized format: {type}" for multi-format countries (driven by the type description) since it only appears once the entered value matches a format; clean up terse and markdown type names that now surface.
🤖 Bitwarden Claude Code ReviewOverall Assessment: REQUEST CHANGES Reviewed the value-aware tax ID type resolution in Code Review Details
|
| this.group.controls.taxId.disable(); | ||
| // getRawValue() includes disabled controls, so clear the value rather than just disabling. | ||
| this.group.controls.taxId.reset(); | ||
| } |
There was a problem hiding this comment.
Details and fix
supportsTaxId$ is false for three different reasons: the country has no tax ID types, the country is US, or scenario.supportsTaxId is false (subscriber tier). Because of startWith(...), the subscription emits once during ngOnInit — after patchValue(scenario.existing) — so for an existing record whose country is US, or an org whose tier no longer supports tax IDs, the reset clears the patched value immediately.
Trace for EditBillingAddressDialogComponent with an existing us_ein on file:
- Dialog constructor +
ngOnInitpatchcountry: "US",taxId: "12-3456789". supportsTaxId$emitsfalseonstartWith("US").disable()+reset()→ control value is nownull.- User edits only the postal code and saves.
getBillingAddressFromFormusesgetRawValue()→taxId: nullis PUT to/address.
Before this PR step 3 only disabled the control, so getRawValue() round-tripped the existing value. The same applies to the async patchValue in trial-payment-dialog.component.ts:172 and organization-plans.component.ts:1227, which land after this subscription is live.
Scoping the reset to an actual country change keeps the stale-value protection without touching the initial state:
this.supportsTaxId$
.pipe(pairwise(), takeUntil(this.destroy$))
.subscribe(([previouslySupported, supportsTaxId]) => {
if (supportsTaxId) {
this.group.controls.taxId.enable();
} else {
this.group.controls.taxId.disable();
// getRawValue() includes disabled controls, so clear the value rather than just disabling.
if (previouslySupported) {
this.group.controls.taxId.reset();
}
}
});(pairwise needs the initial enable()/disable() handled separately, or use startWith on the pair — either shape works as long as the first emission does not reset.)
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-27010
📔 Objective
The web client resolved a tax ID's Stripe type from the billing country alone, so it submitted the wrong type for countries with more than one format — e.g. a bare Canadian Business Number (
987654321) was taggedca_gst_hst, and UK VAT numbers were taggedeu_vatinstead ofgb_vat. It also offered the customer no format guidance.This PR fixes it on the client:
getTaxIdTypeForCountrynow derives the type from the country and the entered value, matching against per-type format regexes ported verbatim from the server'sTaxServicefor the 19 multi-format countries. The submit path passes the entered value through.For reviewers: the save-blocking symptom was already fixed server-side in #8122 (PM-40694) — please don't redo that server work. This PR is the client-side correctness + UX. The client's no-match fallback intentionally relies on that server-side re-derivation to stay correct, so don't disable it.
Testing
gb_vatincl. bare,eu_vatfor XI), Spain, a single-format country (France), the switch-to-US reset, and an upgrade/checkout flow.Follow-ups (out of scope)
clients,server, andbilling-pricing(the root cause behind both PM-27010 and PM-40694).*-type.spec.tsJest ignore pattern silently hides*-type.spec.tsspecs repo-wide.📸 Screenshots