Merge pull request #14639 from mozilla/fxa-6391-hide-taxes-fees-inclusive

feat(payments): hide taxes for inclusive tax
This commit is contained in:
Reino Muhl 2022-12-19 14:57:46 -05:00 коммит произвёл GitHub
Родитель 4c14d3e2c2 142a608ef2
Коммит 5dc1f39863
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 231 добавлений и 57 удалений

Просмотреть файл

@ -8,7 +8,11 @@ import '@testing-library/jest-dom/extend-expect';
import TestRenderer from 'react-test-renderer';
import PlanDetails from './index';
import { getLocalizedCurrency } from '../../lib/formats';
import {
formatPlanPricing,
getLocalizedCurrency,
getLocalizedCurrencyString,
} from '../../lib/formats';
import { MOCK_PLANS, getLocalizedMessage } from '../../lib/test-utils';
import { getFtlBundle } from 'fxa-react/lib/test-utils';
import { FluentBundle } from '@fluent/bundle';
@ -17,6 +21,18 @@ import { Plan } from 'fxa-shared/subscriptions/types';
import { CouponDetails } from 'fxa-shared/dto/auth/payments/coupon';
import { Profile } from '../../store/types';
import AppContext, { defaultAppContext } from '../../lib/AppContext';
import { apiInvoicePreview } from '../../lib/apiClient';
import {
INVOICE_PREVIEW_EXCLUSIVE_TAX,
INVOICE_PREVIEW_INCLUSIVE_TAX,
} from '../../lib/mock-data';
jest.mock('../../lib/apiClient', () => {
return {
...jest.requireActual('../../lib/apiClient'),
apiInvoicePreview: jest.fn(),
};
});
const userProfile: Profile = {
avatar: './avatar.svg',
@ -31,6 +47,7 @@ const userProfile: Profile = {
};
const selectedPlan: Plan = {
active: true,
plan_id: 'planId',
plan_name: 'Pro level',
product_id: 'fpnID',
@ -48,11 +65,15 @@ const selectedPlan: Plan = {
product_metadata: null,
};
const selectedPlanWithConfig = {
const selectedPlanWithConfig: Plan = {
...selectedPlan,
configuration: {
urls: {
webIcon: 'https://webicon',
successActionButton: '',
privacyNotice: '',
termsOfService: '',
termsOfServiceDownload: '',
},
uiContent: {
subtitle: 'VPN subtitle',
@ -67,20 +88,27 @@ const selectedPlanWithConfig = {
},
},
},
support: {},
styles: {},
},
};
beforeEach(() => {
(apiInvoicePreview as jest.Mock).mockClear();
});
afterEach(() => {
updateConfig({
featureFlags: {
useFirestoreProductConfigs: false,
useStripeAutomaticTax: false,
},
});
cleanup();
});
describe('PlanDetails', () => {
it('renders as expected', () => {
it('renders as expected without tax', () => {
const subject = () => {
return render(
<PlanDetails
@ -107,6 +135,88 @@ describe('PlanDetails', () => {
expect(queryByTestId('list')).not.toBeTruthy();
});
it('renders as expected with inclusive tax', async () => {
updateConfig({
featureFlags: {
useStripeAutomaticTax: true,
},
});
(apiInvoicePreview as jest.Mock)
.mockClear()
.mockResolvedValue(INVOICE_PREVIEW_INCLUSIVE_TAX);
const props = {
...{
profile: userProfile,
showExpandButton: false,
isMobile: false,
selectedPlan,
},
};
const subject = () => {
return render(<PlanDetails {...props} />);
};
const { queryByTestId } = subject();
const formattedExpectedAmount = formatPlanPricing(
INVOICE_PREVIEW_INCLUSIVE_TAX.total,
selectedPlan.currency,
selectedPlan.interval,
selectedPlan.interval_count
);
await waitFor(() => {
const totalPriceComponent = queryByTestId('total-price');
expect(totalPriceComponent).toBeInTheDocument();
expect(totalPriceComponent?.innerHTML).toContain(formattedExpectedAmount);
expect(queryByTestId('tax-amount')).not.toBeInTheDocument();
});
});
it('renders as expected with exclusive tax', async () => {
updateConfig({
featureFlags: {
useStripeAutomaticTax: true,
},
});
(apiInvoicePreview as jest.Mock)
.mockClear()
.mockResolvedValue(INVOICE_PREVIEW_EXCLUSIVE_TAX);
const props = {
...{
profile: userProfile,
showExpandButton: false,
isMobile: false,
selectedPlan,
},
};
const subject = () => {
return render(<PlanDetails {...props} />);
};
const { queryByTestId } = subject();
const formattedExpectedAmount = formatPlanPricing(
INVOICE_PREVIEW_EXCLUSIVE_TAX.total,
selectedPlan.currency,
selectedPlan.interval,
selectedPlan.interval_count
);
const expectedTaxAmount = getLocalizedCurrencyString(
INVOICE_PREVIEW_EXCLUSIVE_TAX.tax?.amount!,
selectedPlan.currency
);
await waitFor(() => {
const totalPriceComponent = queryByTestId('total-price');
expect(totalPriceComponent).toBeInTheDocument();
expect(totalPriceComponent?.innerHTML).toContain(formattedExpectedAmount);
const taxAmount = queryByTestId('tax-amount');
expect(taxAmount).toBeInTheDocument();
expect(taxAmount?.innerHTML).toContain(expectedTaxAmount);
});
});
it('renders as expected using firestore config', () => {
updateConfig({
featureFlags: {
@ -130,10 +240,10 @@ describe('PlanDetails', () => {
const productLogo = queryByTestId('product-logo');
expect(productLogo).toHaveAttribute(
'src',
selectedPlanWithConfig.configuration.urls.webIcon
selectedPlanWithConfig.configuration?.urls.webIcon
);
expect(
queryByText(selectedPlanWithConfig.configuration.uiContent.subtitle)
queryByText(selectedPlanWithConfig.configuration?.uiContent.subtitle!)
).toBeInTheDocument();
});
@ -164,11 +274,12 @@ describe('PlanDetails', () => {
const productLogo = queryByTestId('product-logo');
expect(productLogo).toHaveAttribute(
'src',
selectedPlanWithConfig.configuration.locales['fy-NL'].urls.webIcon
selectedPlanWithConfig.configuration?.locales['fy-NL']?.urls?.webIcon
);
expect(
queryByText(
selectedPlanWithConfig.configuration.locales['fy-NL'].uiContent.subtitle
selectedPlanWithConfig.configuration?.locales['fy-NL']?.uiContent
?.subtitle!
)
).toBeInTheDocument();
});
@ -360,17 +471,24 @@ describe('PlanDetails', () => {
const { queryByTestId } = subject();
const expectedAmount = getLocalizedCurrency(
const totalAmount =
selectedPlan.amount && coupon.discountAmount
? selectedPlan.amount - coupon.discountAmount
: selectedPlan.amount,
selectedPlan.currency
: selectedPlan.amount;
const formattedExpectedAmount = formatPlanPricing(
totalAmount,
selectedPlan.currency,
selectedPlan.interval,
selectedPlan.interval_count
);
await waitFor(() => {
const totalPriceComponent = queryByTestId('total-price');
expect(totalPriceComponent).toBeInTheDocument();
expect(totalPriceComponent?.innerHTML).toContain(expectedAmount.value);
expect(totalPriceComponent?.innerHTML).toContain(
formattedExpectedAmount
);
});
});

Просмотреть файл

@ -49,11 +49,14 @@ export const PlanDetails = ({
selectedPlan;
const [loading, setLoading] = useState(true);
const [taxAmount, setTaxAmount] = useState(0);
const [discountAmount, setDiscountAmount] = useState(0);
const [totalAmount, setTotalAmount] = useState(0);
const [subTotal, setSubTotal] = useState(0);
const [totalPrice, setTotalPrice] = useState('');
const [priceAmounts, setPriceAmounts] = useState({
taxAmount: 0,
taxInclusive: true,
discountAmount: 0,
totalAmount: 0,
subTotal: 0,
totalPrice: '',
});
const invoice = useRef(invoicePreview);
const { webIcon, webIconBackground } = webIconConfigFromProductConfig(
@ -85,23 +88,24 @@ export const PlanDetails = ({
setLoading(true);
const fallBack = () => {
setSubTotal(amount!);
if (coupon && coupon.discountAmount && amount) {
setDiscountAmount(coupon.discountAmount);
setTotalAmount(amount - coupon.discountAmount);
} else {
setDiscountAmount(0);
setTotalAmount(amount!);
}
const discountAmount = coupon?.discountAmount || 0;
const totalAmount = amount! - discountAmount;
const price = formatPlanPricing(
totalAmount as unknown as number,
totalAmount! as unknown as number,
currency,
interval,
interval_count
);
setTotalPrice(price);
setPriceAmounts({
taxAmount: 0,
taxInclusive: true,
discountAmount,
totalAmount,
subTotal: amount!,
totalPrice: price,
});
setLoading(false);
};
@ -118,29 +122,26 @@ export const PlanDetails = ({
});
}
setSubTotal(invoice.current!.subtotal);
setTotalAmount(invoice.current!.total);
if (invoice.current!.tax && invoice.current?.tax.amount) {
setTaxAmount(invoice.current!.tax.amount);
} else {
setTaxAmount(0);
}
if (invoice.current!.discount) {
setDiscountAmount(invoice.current!.discount.amount);
} else {
setDiscountAmount(0);
if (!invoice.current) {
throw new Error('Could not retrieve Invoice Preview');
}
const latestInvoicePreview = invoice.current;
const price = formatPlanPricing(
totalAmount as unknown as number,
latestInvoicePreview.total as unknown as number,
currency,
interval,
interval_count
);
setTotalPrice(price);
setPriceAmounts({
taxAmount: latestInvoicePreview.tax?.amount || 0,
taxInclusive: !latestInvoicePreview.tax?.inclusive,
discountAmount: latestInvoicePreview.discount?.amount || 0,
totalAmount: latestInvoicePreview.total,
subTotal: latestInvoicePreview.subtotal,
totalPrice: price,
});
setLoading(false);
} catch (e: any) {
// gracefully fail/set the state according to the data we have
@ -156,7 +157,6 @@ export const PlanDetails = ({
interval,
interval_count,
selectedPlan.plan_id,
totalAmount,
invoicePreview,
]);
@ -223,7 +223,7 @@ export const PlanDetails = ({
) : (
<>
<div className="row-divider-grey-200 py-6">
{!!subTotal && (
{!!priceAmounts.subTotal && (
<div className="plan-details-item">
<Localized id="plan-details-list-price">
<div>List Price</div>
@ -233,18 +233,24 @@ export const PlanDetails = ({
id={`list-price`}
attrs={{ title: true }}
vars={{
amount: getLocalizedCurrency(subTotal, currency),
amount: getLocalizedCurrency(
priceAmounts.subTotal,
currency
),
intervalCount: interval_count,
}}
>
<div>
{getLocalizedCurrencyString(subTotal, currency)}
{getLocalizedCurrencyString(
priceAmounts.subTotal,
currency
)}
</div>
</Localized>
</div>
)}
{!!taxAmount && (
{!!priceAmounts.taxAmount && priceAmounts.taxInclusive && (
<div className="plan-details-item">
<Localized id="plan-details-tax">
<div>Taxes and Fees</div>
@ -254,18 +260,24 @@ export const PlanDetails = ({
id={`tax`}
attrs={{ title: true }}
vars={{
amount: getLocalizedCurrency(taxAmount, currency),
amount: getLocalizedCurrency(
priceAmounts.taxAmount,
currency
),
intervalCount: interval_count,
}}
>
<div>
{getLocalizedCurrencyString(taxAmount, currency)}
<div data-testid="tax-amount">
{getLocalizedCurrencyString(
priceAmounts.taxAmount,
currency
)}
</div>
</Localized>
</div>
)}
{!!discountAmount && (
{!!priceAmounts.discountAmount && (
<div className="plan-details-item">
<Localized id="coupon-promo-code">
<div>Promo Code</div>
@ -275,13 +287,16 @@ export const PlanDetails = ({
id={`coupon-amount`}
attrs={{ title: true }}
vars={{
amount: getLocalizedCurrency(discountAmount, currency),
amount: getLocalizedCurrency(
priceAmounts.discountAmount,
currency
),
intervalCount: interval_count,
}}
>
<div>
{`- ${getLocalizedCurrencyString(
discountAmount,
priceAmounts.discountAmount,
currency
)}`}
</div>
@ -291,7 +306,7 @@ export const PlanDetails = ({
</div>
<div className="pt-4 pb-6">
{!!totalAmount && (
{!!priceAmounts.totalAmount && (
<div className="plan-details-item font-semibold">
<Localized id="plan-details-total-label">
<div className="total-label">Total</div>
@ -302,17 +317,20 @@ export const PlanDetails = ({
data-testid="plan-price-total"
attrs={{ title: true }}
vars={{
amount: getLocalizedCurrency(totalAmount, currency),
amount: getLocalizedCurrency(
priceAmounts.totalAmount,
currency
),
intervalCount: interval_count,
}}
>
<div
className="total-price"
title={totalPrice}
title={priceAmounts.totalPrice}
data-testid="total-price"
id="total-price"
>
{totalPrice}
{priceAmounts.totalPrice}
</div>
</Localized>
</div>

Просмотреть файл

@ -378,3 +378,41 @@ export const INVOICE_PREVIEW_WITH_INVALID_DISCOUNT: FirstInvoicePreview = {
percent_off: null,
},
};
export const INVOICE_PREVIEW_INCLUSIVE_TAX: FirstInvoicePreview = {
line_items: [
{
amount: 500,
currency: 'usd',
id: 'plan_GqM9N64ksvxaVk',
name: '1 x 123Done Pro (at $5.00 / month)',
},
],
subtotal: 500,
subtotal_excluding_tax: 377,
total: 500,
total_excluding_tax: 377,
tax: {
amount: 123,
inclusive: true,
},
};
export const INVOICE_PREVIEW_EXCLUSIVE_TAX: FirstInvoicePreview = {
line_items: [
{
amount: 500,
currency: 'usd',
id: 'plan_GqM9N64ksvxaVk',
name: '1 x 123Done Pro (at $5.00 / month)',
},
],
subtotal: 500,
subtotal_excluding_tax: 500,
total: 623,
total_excluding_tax: 500,
tax: {
amount: 123,
inclusive: false,
},
};