Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import {
} from "@bitwarden/web-vault/app/billing/warnings/types";

import { SharedModule } from "../../../shared";
import { BillingAddress, getTaxIdTypeForCountry, selectableCountries, taxIdTypes } from "../types";
import {
BillingAddress,
findTaxIdTypeByValue,
getTaxIdTypeForCountry,
selectableCountries,
taxIdTypes,
} from "../types";

export interface BillingAddressControls {
country: string;
Expand All @@ -29,7 +35,7 @@ export const getBillingAddressFromForm = (formGroup: BillingAddressFormGroup): B

export const getBillingAddressFromControls = (controls: BillingAddressControls) => {
const { taxId, ...addressFields } = controls;
const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null;
const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country, taxId) : null;
return taxIdType
? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } }
: { ...addressFields, taxId: null };
Expand Down Expand Up @@ -140,16 +146,18 @@ type Scenario =
[formControl]="group.controls.taxId"
data-testid="tax-id"
/>
@let hint = taxIdWarningHint;
@let hint = taxIdHint;
@if (hint) {
<bit-hint
><i
class="bwi bwi-exclamation-triangle tw-mr-1"
title="{{ hint }}"
aria-hidden="true"
></i
>{{ hint }}</bit-hint
>
<bit-hint>
@if (taxIdWarningActive) {
<bit-icon
name="bwi-exclamation-triangle"
class="tw-mr-1"
title="{{ hint }}"
></bit-icon>
}
{{ hint }}
</bit-hint>
}
</bit-form-field>
</div>
Expand Down Expand Up @@ -207,6 +215,8 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy {
this.group.controls.taxId.enable();
} else {
this.group.controls.taxId.disable();
// getRawValue() includes disabled controls, so clear the value rather than just disabling.
this.group.controls.taxId.reset();
}
Comment thread
amorask-bitwarden marked this conversation as resolved.
Outdated
});
}
Expand All @@ -223,38 +233,40 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy {
this.group.controls.state.disable();
};

get taxIdWarningHint() {
if (
this.scenario.type === "checkout" ||
!this.scenario.supportsTaxId ||
!this.group.value.country ||
this.scenario.taxIdWarning !== TaxIdWarningTypes.FailedVerification
) {
return null;
}
get taxIdHint(): string | null {
return this.computeTaxIdHint(this.group.value.country, this.group.value.taxId);
}

const taxIdType = getTaxIdTypeForCountry(this.group.value.country);
get taxIdWarningActive(): boolean {
return (
this.scenario.type === "update" &&
this.scenario.taxIdWarning === TaxIdWarningTypes.FailedVerification
);
}

if (!taxIdType) {
private computeTaxIdHint(country: string | null | undefined, taxId: string | null | undefined) {
if (!this.scenario.supportsTaxId || !country || country === "US") {
return null;
}
const types = taxIdTypes.filter((type) => type.iso === country);
if (types.length === 0) {
return null;
}
const resolved =
types.length === 1 ? types[0] : taxId ? findTaxIdTypeByValue(types, taxId) : undefined;

const checkInputFormat = this.i18nService.t("checkInputFormat");
if (this.taxIdWarningActive) {
const check = this.i18nService.t("checkInputFormat");
return resolved
? `${check} ${this.i18nService.t("taxIdFormatExample", resolved.example)}`
: check;
}

switch (taxIdType.code) {
case "au_abn": {
const exampleFormat = this.i18nService.t("exampleTaxIdFormat", "ABN", taxIdType.example);
return `${checkInputFormat} ${exampleFormat}`;
}
case "eu_vat": {
const exampleFormat = this.i18nService.t("exampleTaxIdFormat", "EU VAT", taxIdType.example);
return `${checkInputFormat} ${exampleFormat}`;
}
case "gb_vat": {
const exampleFormat = this.i18nService.t("exampleTaxIdFormat", "GB VAT", taxIdType.example);
return `${checkInputFormat} ${exampleFormat}`;
}
if (types.length === 1) {
return this.i18nService.t("taxIdFormatExample", types[0].example);
}

return resolved ? this.i18nService.t("recognizedTaxIdFormat", resolved.description) : null;
}

static getFormGroup = (): BillingAddressFormGroup =>
Expand Down
Loading
Loading