зеркало из https://github.com/mozilla/fxa.git
Merge pull request #14639 from mozilla/fxa-6391-hide-taxes-fees-inclusive
feat(payments): hide taxes for inclusive tax
This commit is contained in:
Коммит
5dc1f39863
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче