-
Notifications
You must be signed in to change notification settings - Fork 2k
[PM-27010] fix: Resolve tax ID type from country and value, with format guidance #22388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
amorask-bitwarden
wants to merge
10
commits into
main
Choose a base branch
from
billing/PM-27010/users-unable-add-vat-id
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
790a56b
[PM-27010] fix: Add value-aware tax ID type resolution
amorask-bitwarden 75be6ab
Pass the entered tax ID value to the resolver on submit
amorask-bitwarden 7477e79
Show value-reactive tax ID format guidance for supported countries
amorask-bitwarden 7db4835
Reset tax ID when the billing country stops supporting it
amorask-bitwarden afdd9f4
Apply review feedback: resolution guards, shared match helper, and tests
amorask-bitwarden 27c69aa
Fix stale tax ID hint and reword it as a recognized-format confirmation
amorask-bitwarden 550ad18
Compute tax ID warning state live instead of caching it
amorask-bitwarden a956392
Merge branch 'main' into billing/PM-27010/users-unable-add-vat-id
amorask-bitwarden 182f8c2
Merge branch 'main' into billing/PM-27010/users-unable-add-vat-id
amorask-bitwarden 6050c1b
Scope tax ID reset to a country change so it can't clear an existing β¦
amorask-bitwarden File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
253 changes: 253 additions & 0 deletions
253
apps/web/src/app/billing/payment/components/enter-billing-address.component.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| import { mock } from "jest-mock-extended"; | ||
|
|
||
| import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; | ||
| import { TaxIdWarningTypes } from "@bitwarden/web-vault/app/billing/warnings/types"; | ||
|
|
||
| import { | ||
| BillingAddressControls, | ||
| EnterBillingAddressComponent, | ||
| getBillingAddressFromControls, | ||
| } from "./enter-billing-address.component"; | ||
|
|
||
| describe("getBillingAddressFromControls", () => { | ||
| const buildControls = ( | ||
| overrides: Partial<BillingAddressControls> = {}, | ||
| ): BillingAddressControls => ({ | ||
| country: "US", | ||
| postalCode: "10001", | ||
| line1: "123 Main St", | ||
| line2: "Apt 4B", | ||
| city: "New York", | ||
| state: "NY", | ||
| taxId: null, | ||
| ...overrides, | ||
| }); | ||
|
|
||
| it("resolves a Canadian value against the entered value rather than the country default", () => { | ||
| const result = getBillingAddressFromControls( | ||
| buildControls({ country: "CA", taxId: "987654321" }), | ||
| ); | ||
|
|
||
| expect(result.taxId).toEqual({ code: "ca_bn", value: "987654321" }); | ||
| }); | ||
|
|
||
| it("resolves Canadian GST/HST and QST values to their matching types", () => { | ||
| expect( | ||
| getBillingAddressFromControls(buildControls({ country: "CA", taxId: "123456789RT0002" })) | ||
| .taxId?.code, | ||
| ).toBe("ca_gst_hst"); | ||
| expect( | ||
| getBillingAddressFromControls(buildControls({ country: "CA", taxId: "1234567890TQ1234" })) | ||
| .taxId?.code, | ||
| ).toBe("ca_qst"); | ||
| }); | ||
|
|
||
| it("resolves United Kingdom values to their matching types", () => { | ||
| expect( | ||
| getBillingAddressFromControls(buildControls({ country: "GB", taxId: "GB123456789" })).taxId | ||
| ?.code, | ||
| ).toBe("gb_vat"); | ||
| expect( | ||
| getBillingAddressFromControls(buildControls({ country: "GB", taxId: "XI123456789" })).taxId | ||
| ?.code, | ||
| ).toBe("eu_vat"); | ||
| }); | ||
|
|
||
| it("returns a null taxId when the taxId control is null", () => { | ||
| const result = getBillingAddressFromControls(buildControls({ country: "CA", taxId: null })); | ||
|
|
||
| expect(result.taxId).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns a null taxId when the taxId control is an empty string", () => { | ||
| const result = getBillingAddressFromControls(buildControls({ country: "CA", taxId: "" })); | ||
|
|
||
| expect(result.taxId).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns a null taxId when the country has no tax ID types", () => { | ||
| const result = getBillingAddressFromControls( | ||
| buildControls({ country: "ZZ", taxId: "123456789" }), | ||
| ); | ||
|
|
||
| expect(result.taxId).toBeNull(); | ||
| }); | ||
|
|
||
| it("passes the address fields through unchanged and replaces the raw taxId string", () => { | ||
| const controls = buildControls({ country: "CA", taxId: "987654321" }); | ||
|
|
||
| const result = getBillingAddressFromControls(controls); | ||
|
|
||
| expect(result.country).toBe(controls.country); | ||
| expect(result.postalCode).toBe(controls.postalCode); | ||
| expect(result.line1).toBe(controls.line1); | ||
| expect(result.line2).toBe(controls.line2); | ||
| expect(result.city).toBe(controls.city); | ||
| expect(result.state).toBe(controls.state); | ||
| expect(result.taxId).toEqual({ code: "ca_bn", value: "987654321" }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("EnterBillingAddressComponent", () => { | ||
| let component: EnterBillingAddressComponent; | ||
|
|
||
| const i18nService = mock<I18nService>(); | ||
| i18nService.t.mockImplementation((key: string, ...args: unknown[]) => [key, ...args].join(":")); | ||
|
|
||
| const setup = (scenario: EnterBillingAddressComponent["scenario"]) => { | ||
| component = new EnterBillingAddressComponent(i18nService); | ||
| component.scenario = scenario; | ||
| component.group = EnterBillingAddressComponent.getFormGroup(); | ||
| component.ngOnInit(); | ||
| }; | ||
|
|
||
| const setCountry = (country: string) => component.group.controls.country.setValue(country); | ||
| const setTaxId = (taxId: string) => component.group.controls.taxId.setValue(taxId); | ||
| const hint = () => (component as any).taxIdHint as string | null; | ||
|
|
||
| afterEach(() => { | ||
| component?.ngOnDestroy(); | ||
| }); | ||
|
|
||
| it("shows the example up front for a single-format country", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
|
|
||
| setCountry("FR"); | ||
|
|
||
| expect(hint()).toBe("taxIdFormatExample:FRAB123456789"); | ||
| }); | ||
|
|
||
| it("shows no hint for a multi-format country with no value", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
|
|
||
| setCountry("CA"); | ||
| expect(hint()).toBeNull(); | ||
|
|
||
| setCountry("GB"); | ||
| expect(hint()).toBeNull(); | ||
| }); | ||
|
|
||
| it("reacts to the entered value for a multi-format country", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
| setCountry("CA"); | ||
|
|
||
| setTaxId("987654321"); | ||
| expect(hint()).toBe("recognizedTaxIdFormat:Canadian Business Number"); | ||
|
|
||
| setTaxId("123456789RT0002"); | ||
| expect(hint()).toBe("recognizedTaxIdFormat:Canadian GST/HST number"); | ||
|
|
||
| setTaxId("12345"); | ||
| expect(hint()).toBeNull(); | ||
| }); | ||
|
|
||
| it("resolves United Kingdom values by the entered value", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
| setCountry("GB"); | ||
|
|
||
| setTaxId("GB123456789"); | ||
| expect(hint()).toBe("recognizedTaxIdFormat:United Kingdom VAT number"); | ||
|
|
||
| setTaxId("XI123456789"); | ||
| expect(hint()).toBe("recognizedTaxIdFormat:Northern Ireland VAT number"); | ||
| }); | ||
|
|
||
| it("prefixes the failed-verification warning with the example", () => { | ||
| setup({ | ||
| type: "update", | ||
| supportsTaxId: true, | ||
| taxIdWarning: TaxIdWarningTypes.FailedVerification, | ||
| }); | ||
| setCountry("CA"); | ||
| setTaxId("987654321"); | ||
|
|
||
| expect((component as any).taxIdWarningActive).toBe(true); | ||
| expect(hint()).toBe("checkInputFormat taxIdFormatExample:123456789"); | ||
| }); | ||
|
|
||
| it("shows the failed-verification prefix alone when there is no example", () => { | ||
| setup({ | ||
| type: "update", | ||
| supportsTaxId: true, | ||
| taxIdWarning: TaxIdWarningTypes.FailedVerification, | ||
| }); | ||
|
|
||
| setCountry("CA"); | ||
|
|
||
| expect(hint()).toBe("checkInputFormat"); | ||
| }); | ||
|
|
||
| it("shows guidance during checkout", () => { | ||
| setup({ type: "checkout", supportsTaxId: true }); | ||
|
|
||
| setCountry("FR"); | ||
|
|
||
| expect((component as any).taxIdWarningActive).toBe(false); | ||
| expect(hint()).toBe("taxIdFormatExample:FRAB123456789"); | ||
| }); | ||
|
|
||
| it("returns no hint when tax IDs are unsupported", () => { | ||
| setup({ type: "update", supportsTaxId: false }); | ||
|
|
||
| setCountry("FR"); | ||
|
|
||
| expect(hint()).toBeNull(); | ||
| }); | ||
|
|
||
| it("resets the tax ID when switching to an unsupported country", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
| setCountry("CA"); | ||
| setTaxId("987654321"); | ||
|
|
||
| setCountry("US"); | ||
|
|
||
| expect(component.group.controls.taxId.value).toBeNull(); | ||
| expect(component.group.controls.taxId.disabled).toBe(true); | ||
| }); | ||
|
|
||
| it("does not submit a stale tax ID after switching to an unsupported country", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
| setCountry("CA"); | ||
| setTaxId("987654321"); | ||
|
|
||
| setCountry("US"); | ||
|
|
||
| expect(getBillingAddressFromControls(component.group.getRawValue()).taxId).toBeNull(); | ||
| }); | ||
|
|
||
| it("preserves the tax ID when switching between supported countries", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
| setCountry("CA"); | ||
| setTaxId("987654321"); | ||
|
|
||
| setCountry("GB"); | ||
|
|
||
| expect(component.group.controls.taxId.value).toBe("987654321"); | ||
| }); | ||
|
|
||
| it("re-enables the tax ID when returning to a supported country", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
| setCountry("CA"); | ||
| setCountry("US"); | ||
| setCountry("CA"); | ||
|
|
||
| expect(component.group.controls.taxId.enabled).toBe(true); | ||
| }); | ||
|
|
||
| it("returns no hint for the United States", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
|
|
||
| setCountry("US"); | ||
|
|
||
| expect(hint()).toBeNull(); | ||
| }); | ||
|
|
||
| it("shows no stale hint after switching from an unsupported country to a supported one", () => { | ||
| setup({ type: "update", supportsTaxId: true }); | ||
|
|
||
| setCountry("US"); | ||
| setCountry("CA"); | ||
|
|
||
| expect(hint()).toBeNull(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.