Skip to content

Commit

Permalink
upcoming: [M3-9155] Add billing agreement checkbox for tax id (#11563)
Browse files Browse the repository at this point in the history
* upcoming: [M3-9155] Add billing agreement checkbox for tax id

* Add e2e and more unit tests

* Add changesets

* Add legal privacy statement

* Update packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts

Co-authored-by: Hana Xu <[email protected]>

* Remove unit tests since we have e2e coverage

---------

Co-authored-by: Jaalah Ramos <[email protected]>
Co-authored-by: Hana Xu <[email protected]>
  • Loading branch information
3 people authored Feb 3, 2025
1 parent 7511a29 commit c47ddd4
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 18 deletions.
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-11563-added-1737734348416.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Added
---

`billing_agreement` to Agreements interface ([#11563](https://github.com/linode/manager/pull/11563))
1 change: 1 addition & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export type AgreementType = 'eu_model' | 'privacy_policy';
export interface Agreements {
eu_model: boolean;
privacy_policy: boolean;
billing_agreement: boolean;
}

export type NotificationType =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add billing agreement checkbox to non-US countries for tax id purposes ([#11563](https://github.com/linode/manager/pull/11563))
85 changes: 78 additions & 7 deletions packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { mockGetAccount, mockUpdateAccount } from 'support/intercepts/account';
import {
mockGetAccount,
mockUpdateAccount,
mockUpdateAccountAgreements,
} from 'support/intercepts/account';
import { accountFactory } from 'src/factories/account';
import type { Account } from '@linode/api-v4';
import { ui } from 'support/ui';
import { TAX_ID_HELPER_TEXT } from 'src/features/Billing/constants';
import {
TAX_ID_AGREEMENT_TEXT,
TAX_ID_HELPER_TEXT,
} from 'src/features/Billing/constants';
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';
import { mockGetUserPreferences } from 'support/intercepts/profile';
import { accountAgreementsFactory } from 'src/factories';

/* eslint-disable sonarjs/no-duplicate-string */
const accountData = accountFactory.build({
Expand Down Expand Up @@ -49,6 +57,10 @@ const newAccountData = accountFactory.build({
zip: '19108',
});

const newAccountAgreement = accountAgreementsFactory.build({
billing_agreement: true,
});

const checkAccountContactDisplay = (accountInfo: Account) => {
cy.findByText('Billing Contact').should('be.visible');
cy.findByText(accountInfo['company']).should('be.visible');
Expand Down Expand Up @@ -158,11 +170,6 @@ describe('Billing Contact', () => {
.click()
.clear()
.type(newAccountData['phone']);
cy.get('[data-qa-contact-country]').click().type('Afghanistan{enter}');
cy.findByText(TAX_ID_HELPER_TEXT).should('be.visible');
cy.get('[data-qa-contact-country]')
.click()
.type('United States{enter}');
cy.get('[data-qa-contact-state-province]')
.should('be.visible')
.click()
Expand All @@ -187,4 +194,68 @@ describe('Billing Contact', () => {
checkAccountContactDisplay(newAccountData);
});
});

it('Edit Contact Info: Tax ID Agreement', () => {
mockGetUserPreferences({ maskSensitiveData: false }).as(
'getUserPreferences'
);
// mock the user's account data and confirm that it is displayed correctly upon page load
mockGetAccount(accountData).as('getAccount');
cy.visitWithLogin('/account/billing');

// edit the billing contact information
mockUpdateAccount(newAccountData).as('updateAccount');
mockUpdateAccountAgreements(newAccountAgreement).as(
'updateAccountAgreements'
);
cy.get('[data-qa-contact-summary]').within((_contact) => {
checkAccountContactDisplay(accountData);
cy.findByText('Edit').should('be.visible').click();
});

ui.drawer
.findByTitle('Edit Billing Contact Info')
.should('be.visible')
.within(() => {
cy.findByLabelText('City')
.should('be.visible')
.click()
.clear()
.type(newAccountData['city']);
cy.findByLabelText('Postal Code')
.should('be.visible')
.click()
.clear()
.type(newAccountData['zip']);
cy.get('[data-qa-contact-country]').click().type('Afghanistan{enter}');
cy.findByLabelText('Tax ID')
.should('be.visible')
.click()
.clear()
.type(newAccountData['tax_id']);
cy.findByText(TAX_ID_HELPER_TEXT).should('be.visible');
cy.findByText(TAX_ID_AGREEMENT_TEXT)
.scrollIntoView()
.should('be.visible');
cy.findByText('Akamai Privacy Statement.').should('be.visible');
cy.get('[data-qa-save-contact-info="true"]').should('be.disabled');
cy.get('[data-testid="tax-id-checkbox"]').click();
cy.get('[data-qa-save-contact-info="true"]')
.should('be.enabled')
.click()
.then(() => {
cy.wait('@updateAccount').then((xhr) => {
expect(xhr.response?.body).to.eql(newAccountData);
});
cy.wait('@updateAccountAgreements').then((xhr) => {
expect(xhr.response?.body).to.eql(newAccountAgreement);
});
});
});

// check the page updates to reflect the edits
cy.get('[data-qa-contact-summary]').within(() => {
checkAccountContactDisplay(newAccountData);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('GDPR agreement', () => {
mockGetAccountAgreements({
privacy_policy: false,
eu_model: false,
billing_agreement: false,
}).as('getAgreements');

cy.visitWithLogin('/linodes/create');
Expand Down Expand Up @@ -73,6 +74,7 @@ describe('GDPR agreement', () => {
mockGetAccountAgreements({
privacy_policy: false,
eu_model: true,
billing_agreement: false,
}).as('getAgreements');

cy.visitWithLogin('/linodes/create');
Expand Down Expand Up @@ -101,6 +103,7 @@ describe('GDPR agreement', () => {
mockGetAccountAgreements({
privacy_policy: false,
eu_model: false,
billing_agreement: false,
}).as('getAgreements');
const rootpass = randomString(32);
const linodeLabel = randomLabel();
Expand Down
17 changes: 17 additions & 0 deletions packages/manager/cypress/support/intercepts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,23 @@ export const mockGetAccountAgreements = (
);
};

/**
* Intercepts POST request to update account agreements and mocks response.
*
* @param agreements - Agreements with which to mock response.
*
* @returns Cypress chainable.
*/
export const mockUpdateAccountAgreements = (
agreements: Agreements
): Cypress.Chainable<null> => {
return cy.intercept(
'POST',
apiMatcher(`account/agreements`),
makeResponse(agreements)
);
};

/**
* Intercepts GET request to fetch child accounts and mocks the response.
*
Expand Down
4 changes: 3 additions & 1 deletion packages/manager/src/factories/accountAgreements.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Agreements } from '@linode/api-v4/lib/account';
import Factory from 'src/factories/factoryProxy';

import type { Agreements } from '@linode/api-v4/lib/account';

export const accountAgreementsFactory = Factory.Sync.makeFactory<Agreements>({
billing_agreement: false,
eu_model: false,
privacy_policy: true,
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Notice, TextField } from '@linode/ui';
import { Checkbox, Notice, TextField, Typography } from '@linode/ui';
import Grid from '@mui/material/Unstable_Grid2';
import { allCountries } from 'country-region-data';
import { useFormik } from 'formik';
Expand All @@ -7,13 +7,19 @@ import { makeStyles } from 'tss-react/mui';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import EnhancedSelect from 'src/components/EnhancedSelect/Select';
import { Link } from 'src/components/Link';
import { reportException } from 'src/exceptionReporting';
import {
getRestrictedResourceText,
useIsTaxIdEnabled,
} from 'src/features/Account/utils';
import { TAX_ID_HELPER_TEXT } from 'src/features/Billing/constants';
import {
TAX_ID_AGREEMENT_TEXT,
TAX_ID_HELPER_TEXT,
} from 'src/features/Billing/constants';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
import { useAccount, useMutateAccount } from 'src/queries/account/account';
import { useMutateAccountAgreements } from 'src/queries/account/agreements';
import { useNotificationsQuery } from 'src/queries/account/notifications';
import { useProfile } from 'src/queries/profile/profile';
import { getErrorMap } from 'src/utilities/errorUtils';
Expand All @@ -31,9 +37,13 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
const { data: account } = useAccount();
const { error, isPending, mutateAsync } = useMutateAccount();
const { data: notifications, refetch } = useNotificationsQuery();
const { mutateAsync: updateAccountAgreements } = useMutateAccountAgreements();
const { classes } = useStyles();
const emailRef = React.useRef<HTMLInputElement>();
const { data: profile } = useProfile();
const [billingAgreementChecked, setBillingAgreementChecked] = React.useState(
false
);
const { isTaxIdEnabled } = useIsTaxIdEnabled();
const isChildUser = profile?.user_type === 'child';
const isParentUser = profile?.user_type === 'parent';
Expand Down Expand Up @@ -69,6 +79,24 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {

await mutateAsync(clonedValues);

if (billingAgreementChecked) {
try {
await updateAccountAgreements({ billing_agreement: true });
} catch (error) {
let customErrorMessage =
'Expected to sign billing agreement, but the request resulted in an error';
const apiErrorMessage = error?.[0]?.reason;

if (apiErrorMessage) {
customErrorMessage += `: ${apiErrorMessage}`;
}

reportException(error, {
message: customErrorMessage,
});
}
}

// If there's a "billing_email_bounce" notification on the account, and
// the user has just updated their email, re-request notifications to
// potentially clear the email bounce notification.
Expand Down Expand Up @@ -122,14 +150,14 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
* - region[0] is the readable name of the region (e.g. "Alabama")
* - region[1] is the ISO 3166-2 code of the region (e.g. "AL")
*/
const countryResults: Item<string>[] = allCountries.map((country) => {
const countryResults: Item<string>[] = (allCountries || []).map((country) => {
return {
label: country[0],
value: country[1],
};
});

const currentCountryResult = allCountries.filter((country) =>
const currentCountryResult = (allCountries || []).filter((country) =>
formik.values.country
? country[1] === formik.values.country
: country[1] === account?.country
Expand Down Expand Up @@ -177,6 +205,8 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
formik.setFieldValue('tax_id', '');
};

const nonUSCountry = isTaxIdEnabled && formik.values.country !== 'US';

return (
<form onSubmit={formik.handleSubmit}>
<Grid
Expand Down Expand Up @@ -370,25 +400,51 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
</Grid>
<Grid xs={12}>
<TextField
helperText={
isTaxIdEnabled &&
formik.values.country !== 'US' &&
TAX_ID_HELPER_TEXT
}
data-qa-contact-tax-id
disabled={isReadOnly}
errorText={errorMap.tax_id}
helperText={nonUSCountry && TAX_ID_HELPER_TEXT}
label="Tax ID"
name="tax_id"
onChange={formik.handleChange}
value={formik.values.tax_id}
/>
</Grid>
{nonUSCountry && (
<Grid
alignItems="flex-start"
display="flex"
marginTop={(theme) => theme.tokens.spacing[60]}
xs={12}
>
<Checkbox
onChange={() =>
setBillingAgreementChecked(!billingAgreementChecked)
}
sx={(theme) => ({
marginRight: theme.tokens.spacing[40],
padding: 0,
})}
checked={billingAgreementChecked}
data-testid="tax-id-checkbox"
id="taxIdAgreementCheckbox"
/>
<Typography component="label" htmlFor="taxIdAgreementCheckbox">
{TAX_ID_AGREEMENT_TEXT}{' '}
<Link to="https://www.akamai.com/legal/privacy-statement">
Akamai Privacy Statement.
</Link>
</Typography>
</Grid>
)}
</Grid>
<ActionsPanel
primaryButtonProps={{
'data-testid': 'save-contact-info',
disabled: isReadOnly,
disabled:
isReadOnly ||
(nonUSCountry &&
(!billingAgreementChecked || !formik.values.tax_id)),
label: 'Save Changes',
loading: isPending,
type: 'submit',
Expand Down
2 changes: 2 additions & 0 deletions packages/manager/src/features/Billing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const ADD_PAYMENT_METHOD = 'Add Payment Method';
export const EDIT_BILLING_CONTACT = 'Edit';
export const TAX_ID_HELPER_TEXT =
'Tax Identification Numbers (TIN) are set by the national authorities and they have different names in different countries. Enter a TIN valid for the country of your billing address. It will be validated.';
export const TAX_ID_AGREEMENT_TEXT =
'I have reviewed and confirm the accuracy of the above information. I understand that the submission of inaccurate information will lead to billing errors and may result in our assessment of additional fees to your account. Information shared with Akamai is subject to the';

0 comments on commit c47ddd4

Please sign in to comment.