зеркало из https://github.com/mozilla/fxa.git
WIP - fix
This commit is contained in:
Родитель
73d418d869
Коммит
a1ae46d0ae
|
@ -38,8 +38,7 @@ export default async function UpgradeLayout({
|
|||
cms.defaultPurchase.purchaseDetails.localizations.at(0) ||
|
||||
cms.defaultPurchase.purchaseDetails;
|
||||
|
||||
const currentOfferingId = cart.fromOfferingConfigId;
|
||||
const currentCmsDataPromise = fetchCMSData(currentOfferingId, locale);
|
||||
const currentCmsDataPromise = fetchCMSData(cart.fromOfferingConfigId, locale);
|
||||
const currentCms = await currentCmsDataPromise;
|
||||
const currentPurchaseDetails =
|
||||
currentCms.defaultPurchase.purchaseDetails.localizations.at(0) ||
|
||||
|
|
|
@ -163,19 +163,3 @@ export class CartSuccessMissingRequired extends CartError {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CartUpgradeMissingRequired extends CartError {
|
||||
constructor(cartId: string) {
|
||||
super('Upgrade cart is missing required fields', {
|
||||
cartId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CartUpgradeNotValid extends CartError {
|
||||
constructor(cartId: string) {
|
||||
super('Upgrade cart does not have current plan', {
|
||||
cartId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
SuccessCart,
|
||||
TaxAmount,
|
||||
UpdateCart,
|
||||
UpgradeCart,
|
||||
WithContextCart,
|
||||
} from './cart.types';
|
||||
|
||||
|
@ -133,3 +134,17 @@ export const SuccessCartFactory = (
|
|||
paymentInfo: PaymentInfoFactory(),
|
||||
...override,
|
||||
});
|
||||
|
||||
export const UpgradeCartFactory = (
|
||||
override?: Partial<UpgradeCart>
|
||||
): UpgradeCart => ({
|
||||
...WithContextCartFactory(),
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE,
|
||||
fromOfferingConfigId: faker.string.uuid(),
|
||||
upgradeFromPrice: {
|
||||
currency: faker.finance.currencyCode(),
|
||||
interval: faker.helpers.arrayElement(['day', 'month', 'week', 'year']),
|
||||
listAmount: faker.number.int({ max: 1000 }),
|
||||
},
|
||||
...override,
|
||||
});
|
||||
|
|
|
@ -51,7 +51,6 @@ import {
|
|||
} from '@fxa/profile/client';
|
||||
import {
|
||||
MockStrapiClientConfigProvider,
|
||||
PageContentOfferingTransformedFactory,
|
||||
ProductConfigurationManager,
|
||||
StrapiClient,
|
||||
} from '@fxa/shared/cms';
|
||||
|
@ -81,12 +80,12 @@ import {
|
|||
ResultCartFactory,
|
||||
SuccessCartFactory,
|
||||
UpdateCartFactory,
|
||||
WithContextCartFactory,
|
||||
} from './cart.factories';
|
||||
import { CartManager } from './cart.manager';
|
||||
import { CartService } from './cart.service';
|
||||
import { CheckoutService } from './checkout.service';
|
||||
import {
|
||||
CartEligibilityMismatchError,
|
||||
CartError,
|
||||
CartInvalidCurrencyError,
|
||||
CartInvalidPromoCodeError,
|
||||
|
@ -94,8 +93,6 @@ import {
|
|||
CartStateProcessingError,
|
||||
CartSubscriptionNotFoundError,
|
||||
CartSuccessMissingRequired,
|
||||
CartUpgradeMissingRequired,
|
||||
CartUpgradeNotValid,
|
||||
} from './cart.error';
|
||||
import { CurrencyManager } from '@fxa/payments/currency';
|
||||
import { MockCurrencyConfigProvider } from 'libs/payments/currency/src/lib/currency.config';
|
||||
|
@ -728,6 +725,7 @@ describe('CartService', () => {
|
|||
it('returns cart and upcomingInvoicePreview', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.CREATE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
|
@ -773,6 +771,7 @@ describe('CartService', () => {
|
|||
it('returns cart and upcomingInvoicePreview and latestInvoicePreview', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeSubscriptionId: mockSubscription.id,
|
||||
eligibilityStatus: CartEligibilityStatus.CREATE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
|
@ -835,6 +834,7 @@ describe('CartService', () => {
|
|||
it('returns cart and upcomingInvoicePreview if customer is undefined', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeCustomerId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.CREATE,
|
||||
});
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory();
|
||||
|
@ -881,6 +881,7 @@ describe('CartService', () => {
|
|||
const mockCart = ResultCartFactory({
|
||||
uid: mockUid,
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.CREATE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
|
@ -915,6 +916,7 @@ describe('CartService', () => {
|
|||
const mockCart = ResultCartFactory({
|
||||
uid: mockUid,
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.CREATE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
|
@ -961,154 +963,6 @@ describe('CartService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getUpgradeCart', () => {
|
||||
it('returns cart with current plan and offering id', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory({
|
||||
oneTimeCharge: 4500,
|
||||
});
|
||||
const mockCurrentPrice = StripePriceFactory();
|
||||
const mockCurrentOffering = PageContentOfferingTransformedFactory();
|
||||
|
||||
jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockCart);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest.spyOn(eligibilityService, 'checkEligibility').mockResolvedValue({
|
||||
subscriptionEligibilityResult: EligibilityStatus.UPGRADE,
|
||||
fromOfferingConfigId: mockCurrentOffering.apiIdentifier,
|
||||
upgradeFromPrice: mockCurrentPrice,
|
||||
});
|
||||
jest
|
||||
.spyOn(invoiceManager, 'previewUpcomingForUpgrade')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
|
||||
const result = await cartService.getUpgradeCart(mockCart.id);
|
||||
expect(result).toEqual({
|
||||
...mockCart,
|
||||
upcomingInvoicePreview: mockInvoicePreview,
|
||||
metricsOptedOut: false,
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE,
|
||||
fromOfferingConfigId: mockCurrentOffering.apiIdentifier,
|
||||
oneTimeCharge: 4500,
|
||||
upgradeFromPrice: {
|
||||
currency: mockCurrentPrice.currency,
|
||||
interval: mockCurrentPrice.recurring?.interval,
|
||||
listAmount: mockCurrentPrice.unit_amount,
|
||||
},
|
||||
});
|
||||
|
||||
expect(cartManager.fetchCartById).toHaveBeenCalledWith(mockCart.id);
|
||||
expect(
|
||||
productConfigurationManager.retrieveStripePrice
|
||||
).toHaveBeenCalledWith(mockCart.offeringConfigId, mockCart.interval);
|
||||
expect(customerManager.retrieve).toHaveBeenCalledWith(
|
||||
mockCart.stripeCustomerId
|
||||
);
|
||||
expect(invoiceManager.previewUpcomingForUpgrade).toHaveBeenCalledWith({
|
||||
priceId: mockPrice.id,
|
||||
currency: mockCart.currency,
|
||||
customer: mockCustomer,
|
||||
taxAddress: mockCart.taxAddress,
|
||||
upgradeFromPrice: mockCurrentPrice,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error if eligibility status is not upgrade for getUpgradeCart', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.CREATE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory({
|
||||
oneTimeCharge: 4500,
|
||||
});
|
||||
|
||||
jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockCart);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest.spyOn(eligibilityService, 'checkEligibility').mockResolvedValue({
|
||||
subscriptionEligibilityResult: EligibilityStatus.CREATE,
|
||||
});
|
||||
jest
|
||||
.spyOn(invoiceManager, 'previewUpcomingForUpgrade')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
|
||||
await expect(
|
||||
cartService.getUpgradeCart(mockCart.id)
|
||||
).rejects.toThrowError(CartEligibilityMismatchError);
|
||||
});
|
||||
|
||||
it('throws error if upgrade is missing offering id or price from current plan', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory({
|
||||
oneTimeCharge: 4500,
|
||||
});
|
||||
|
||||
jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockCart);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest.spyOn(eligibilityService, 'checkEligibility').mockResolvedValue({
|
||||
subscriptionEligibilityResult: EligibilityStatus.UPGRADE,
|
||||
});
|
||||
jest
|
||||
.spyOn(invoiceManager, 'previewUpcomingForUpgrade')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
|
||||
await expect(
|
||||
cartService.getUpgradeCart(mockCart.id)
|
||||
).rejects.toThrowError(CartUpgradeNotValid);
|
||||
});
|
||||
|
||||
it('throws error if upgrade is missing oneTimeCharge from invoice', async () => {
|
||||
const mockCart = ResultCartFactory({
|
||||
stripeSubscriptionId: null,
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE,
|
||||
});
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockPrice = StripePriceFactory();
|
||||
const mockInvoicePreview = InvoicePreviewFactory({
|
||||
oneTimeCharge: undefined,
|
||||
});
|
||||
const mockCurrentPrice = StripePriceFactory();
|
||||
const mockCurrentOffering = PageContentOfferingTransformedFactory();
|
||||
|
||||
jest.spyOn(cartManager, 'fetchCartById').mockResolvedValue(mockCart);
|
||||
jest
|
||||
.spyOn(productConfigurationManager, 'retrieveStripePrice')
|
||||
.mockResolvedValue(mockPrice);
|
||||
jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer);
|
||||
jest.spyOn(eligibilityService, 'checkEligibility').mockResolvedValue({
|
||||
subscriptionEligibilityResult: EligibilityStatus.UPGRADE,
|
||||
fromOfferingConfigId: mockCurrentOffering.apiIdentifier,
|
||||
upgradeFromPrice: mockCurrentPrice,
|
||||
});
|
||||
jest
|
||||
.spyOn(invoiceManager, 'previewUpcomingForUpgrade')
|
||||
.mockResolvedValue(mockInvoicePreview);
|
||||
|
||||
await expect(
|
||||
cartService.getUpgradeCart(mockCart.id)
|
||||
).rejects.toThrowError(CartUpgradeMissingRequired);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuccessCart', () => {
|
||||
const mockSuccessCart = SuccessCartFactory();
|
||||
it('should return success cart', async () => {
|
||||
|
@ -1118,11 +972,10 @@ describe('CartService', () => {
|
|||
});
|
||||
|
||||
it('should throw error if cart state is not success', async () => {
|
||||
jest
|
||||
.spyOn(cartService, 'getCart')
|
||||
.mockResolvedValue(SuccessCartFactory({ state: CartState.FAIL }));
|
||||
const mockCart = WithContextCartFactory({ state: CartState.FAIL });
|
||||
jest.spyOn(cartService, 'getCart').mockResolvedValue(mockCart);
|
||||
await expect(
|
||||
cartService.getSuccessCart(mockSuccessCart.id)
|
||||
cartService.getSuccessCart(mockCart.id)
|
||||
).rejects.toThrowError(CartInvalidStateForActionError);
|
||||
});
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
AccountCustomerManager,
|
||||
AccountCustomerNotFoundError,
|
||||
StripeCustomer,
|
||||
StripePrice,
|
||||
StripeSubscription,
|
||||
} from '@fxa/payments/stripe';
|
||||
import { ProductConfigurationManager } from '@fxa/shared/cms';
|
||||
|
@ -37,6 +38,7 @@ import { GeoDBManager } from '@fxa/shared/geodb';
|
|||
import { CartManager } from './cart.manager';
|
||||
import type {
|
||||
CheckoutCustomerData,
|
||||
GetCartResult,
|
||||
GetNeedsInputResponse,
|
||||
NoInputNeededResponse,
|
||||
PaymentInfo,
|
||||
|
@ -44,14 +46,11 @@ import type {
|
|||
StripeHandleNextActionResponse,
|
||||
SuccessCart,
|
||||
UpdateCart,
|
||||
UpgradeCart,
|
||||
WithContextCart,
|
||||
} from './cart.types';
|
||||
import { NeedsInputType } from './cart.types';
|
||||
import { CurrentPrice, NeedsInputType } from './cart.types';
|
||||
import { handleEligibilityStatusMap } from './cart.utils';
|
||||
import { CheckoutService } from './checkout.service';
|
||||
import {
|
||||
CartEligibilityMismatchError,
|
||||
CartError,
|
||||
CartInvalidCurrencyError,
|
||||
CartInvalidPromoCodeError,
|
||||
|
@ -60,8 +59,6 @@ import {
|
|||
CartStateProcessingError,
|
||||
CartSubscriptionNotFoundError,
|
||||
CartSuccessMissingRequired,
|
||||
CartUpgradeMissingRequired,
|
||||
CartUpgradeNotValid,
|
||||
} from './cart.error';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
import assert from 'assert';
|
||||
|
@ -437,7 +434,7 @@ export class CartService {
|
|||
* Fetch a cart from the database by ID
|
||||
*/
|
||||
@SanitizeExceptions()
|
||||
async getCart(cartId: string): Promise<WithContextCart> {
|
||||
async getCart(cartId: string): Promise<GetCartResult> {
|
||||
const cart = await this.cartManager.fetchCartById(cartId);
|
||||
|
||||
const [price, metricsOptedOut] = await Promise.all([
|
||||
|
@ -457,13 +454,39 @@ export class CartService {
|
|||
]);
|
||||
}
|
||||
|
||||
const upcomingInvoicePreview = await this.invoiceManager.previewUpcoming({
|
||||
priceId: price.id,
|
||||
currency: cart.currency || DEFAULT_CURRENCY,
|
||||
customer,
|
||||
taxAddress: cart.taxAddress || undefined,
|
||||
couponCode: cart.couponCode || undefined,
|
||||
});
|
||||
let fromOfferingConfigId: string | undefined;
|
||||
let upgradeFromPrice: StripePrice | undefined;
|
||||
let currentPrice: CurrentPrice | undefined;
|
||||
if (cart.eligibilityStatus === CartEligibilityStatus.UPGRADE) {
|
||||
const eligibility = await this.eligibilityService.checkEligibility(
|
||||
cart.interval as SubplatInterval,
|
||||
cart.offeringConfigId,
|
||||
cart.stripeCustomerId
|
||||
);
|
||||
fromOfferingConfigId = eligibility.fromOfferingConfigId;
|
||||
upgradeFromPrice = eligibility.upgradeFromPrice;
|
||||
}
|
||||
|
||||
let upcomingInvoicePreview: InvoicePreview | undefined;
|
||||
if (cart.eligibilityStatus === CartEligibilityStatus.UPGRADE) {
|
||||
upcomingInvoicePreview =
|
||||
await this.invoiceManager.previewUpcomingForUpgrade({
|
||||
priceId: price.id,
|
||||
currency: cart.currency || DEFAULT_CURRENCY,
|
||||
customer,
|
||||
taxAddress: cart.taxAddress || undefined,
|
||||
couponCode: cart.couponCode || undefined,
|
||||
upgradeFromPrice,
|
||||
});
|
||||
} else {
|
||||
upcomingInvoicePreview = await this.invoiceManager.previewUpcoming({
|
||||
priceId: price.id,
|
||||
currency: cart.currency || DEFAULT_CURRENCY,
|
||||
customer,
|
||||
taxAddress: cart.taxAddress || undefined,
|
||||
couponCode: cart.couponCode || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
let paymentInfo: PaymentInfo | undefined;
|
||||
if (customer?.invoice_settings.default_payment_method) {
|
||||
|
@ -513,6 +536,8 @@ export class CartService {
|
|||
metricsOptedOut,
|
||||
latestInvoicePreview,
|
||||
paymentInfo,
|
||||
fromOfferingConfigId,
|
||||
upgradeFromPrice: currentPrice,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -531,123 +556,21 @@ export class CartService {
|
|||
);
|
||||
}
|
||||
|
||||
if (!cart.latestInvoicePreview || !cart.paymentInfo?.type) {
|
||||
throw new CartSuccessMissingRequired(cartId);
|
||||
}
|
||||
|
||||
return {
|
||||
...cart,
|
||||
latestInvoicePreview: cart.latestInvoicePreview,
|
||||
paymentInfo: cart.paymentInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a upgrade cart
|
||||
*/
|
||||
async getUpgradeCart(cartId: string): Promise<UpgradeCart> {
|
||||
const cart = await this.cartManager.fetchCartById(cartId);
|
||||
|
||||
if (cart.eligibilityStatus !== CartEligibilityStatus.UPGRADE)
|
||||
throw new CartEligibilityMismatchError(
|
||||
cartId,
|
||||
cart.eligibilityStatus,
|
||||
CartEligibilityStatus.UPGRADE
|
||||
);
|
||||
|
||||
const [price, metricsOptedOut] = await Promise.all([
|
||||
this.productConfigurationManager.retrieveStripePrice(
|
||||
cart.offeringConfigId,
|
||||
cart.interval as SubplatInterval
|
||||
),
|
||||
this.metricsOptedOut(cart.uid),
|
||||
]);
|
||||
|
||||
let customer: StripeCustomer | undefined;
|
||||
if (cart.stripeCustomerId) {
|
||||
customer = await this.customerManager.retrieve(cart.stripeCustomerId);
|
||||
}
|
||||
|
||||
const { fromOfferingConfigId, upgradeFromPrice } =
|
||||
await this.eligibilityService.checkEligibility(
|
||||
cart.interval as SubplatInterval,
|
||||
cart.offeringConfigId,
|
||||
cart.stripeCustomerId
|
||||
);
|
||||
|
||||
if (
|
||||
!fromOfferingConfigId ||
|
||||
!upgradeFromPrice ||
|
||||
!upgradeFromPrice.recurring ||
|
||||
!upgradeFromPrice.unit_amount
|
||||
)
|
||||
throw new CartUpgradeNotValid(cartId);
|
||||
|
||||
const upcomingInvoicePreview =
|
||||
await this.invoiceManager.previewUpcomingForUpgrade({
|
||||
priceId: price.id,
|
||||
currency: cart.currency || DEFAULT_CURRENCY,
|
||||
customer,
|
||||
taxAddress: cart.taxAddress || undefined,
|
||||
couponCode: cart.couponCode || undefined,
|
||||
upgradeFromPrice,
|
||||
});
|
||||
|
||||
if (!upcomingInvoicePreview.oneTimeCharge) {
|
||||
throw new CartUpgradeMissingRequired(cartId);
|
||||
'latestInvoicePreview' in cart &&
|
||||
cart.latestInvoicePreview !== undefined &&
|
||||
'paymentInfo' in cart &&
|
||||
cart.paymentInfo !== undefined &&
|
||||
'type' in cart.paymentInfo
|
||||
) {
|
||||
return {
|
||||
...cart,
|
||||
state: CartState.SUCCESS,
|
||||
latestInvoicePreview: cart.latestInvoicePreview,
|
||||
paymentInfo: cart.paymentInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// Cart latest invoice data
|
||||
let latestInvoicePreview: InvoicePreview | undefined;
|
||||
let paymentInfo: PaymentInfo | undefined;
|
||||
if (customer && cart.stripeSubscriptionId) {
|
||||
// fetch latest payment info from subscription
|
||||
const subscription = await this.subscriptionManager.retrieve(
|
||||
cart.stripeSubscriptionId
|
||||
);
|
||||
assert(subscription.latest_invoice, 'Subscription not found');
|
||||
latestInvoicePreview = await this.invoiceManager.preview(
|
||||
subscription.latest_invoice
|
||||
);
|
||||
|
||||
// fetch payment method info
|
||||
if (subscription.collection_method === 'send_invoice') {
|
||||
// PayPal payment method collection
|
||||
// TODO: render paypal payment info in the UI (FXA-10608)
|
||||
paymentInfo = {
|
||||
type: 'external_paypal',
|
||||
};
|
||||
} else {
|
||||
// Stripe payment method collection
|
||||
if (customer.invoice_settings.default_payment_method) {
|
||||
const paymentMethod = await this.paymentMethodManager.retrieve(
|
||||
customer.invoice_settings.default_payment_method
|
||||
);
|
||||
paymentInfo = {
|
||||
type: paymentMethod.type,
|
||||
last4: paymentMethod.card?.last4,
|
||||
brand: paymentMethod.card?.brand,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentPrice = {
|
||||
currency: upgradeFromPrice.currency,
|
||||
interval: upgradeFromPrice.recurring?.interval,
|
||||
listAmount: upgradeFromPrice.unit_amount,
|
||||
};
|
||||
|
||||
return {
|
||||
...cart,
|
||||
upcomingInvoicePreview,
|
||||
metricsOptedOut,
|
||||
latestInvoicePreview,
|
||||
paymentInfo,
|
||||
fromOfferingConfigId: fromOfferingConfigId,
|
||||
oneTimeCharge: upcomingInvoicePreview.oneTimeCharge,
|
||||
upgradeFromPrice: currentPrice,
|
||||
};
|
||||
throw new CartSuccessMissingRequired(cartId);
|
||||
}
|
||||
|
||||
async metricsOptedOut(accountId?: string): Promise<boolean> {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { TaxAddress } from '@fxa/payments/customer';
|
||||
import { InvoicePreview, TaxAddress } from '@fxa/payments/customer';
|
||||
import {
|
||||
Cart,
|
||||
CartEligibilityStatus,
|
||||
|
@ -29,20 +29,6 @@ export type FinishErrorCart = {
|
|||
stripeCustomerId?: string;
|
||||
};
|
||||
|
||||
export interface Invoice {
|
||||
currency: string;
|
||||
listAmount: number;
|
||||
totalAmount: number;
|
||||
taxAmounts: TaxAmount[];
|
||||
discountAmount: number | null;
|
||||
subtotal: number;
|
||||
discountEnd?: number | null;
|
||||
discountType?: string;
|
||||
number: string | null; // customer-facing invoice identifier
|
||||
paypalTransactionId?: string;
|
||||
oneTimeCharge?: number;
|
||||
}
|
||||
|
||||
export type PaymentProvidersType =
|
||||
| Stripe.PaymentMethod.Type
|
||||
| 'google_iap'
|
||||
|
@ -69,24 +55,30 @@ export interface CurrentPrice {
|
|||
listAmount: number;
|
||||
}
|
||||
|
||||
export type GetCartResult = WithContextCart | SuccessCart | UpgradeCart;
|
||||
|
||||
export type WithContextCart = ResultCart & {
|
||||
metricsOptedOut: boolean;
|
||||
upcomingInvoicePreview: Invoice;
|
||||
latestInvoicePreview?: Invoice;
|
||||
upcomingInvoicePreview: InvoicePreview;
|
||||
latestInvoicePreview?: InvoicePreview;
|
||||
paymentInfo?: PaymentInfo;
|
||||
fromOfferingConfigId?: string;
|
||||
oneTimeCharge?: number;
|
||||
upgradeFromPrice?: CurrentPrice;
|
||||
};
|
||||
|
||||
export type SuccessCart = WithContextCart & {
|
||||
latestInvoicePreview: Invoice;
|
||||
export type SuccessCart = ResultCart & {
|
||||
state: CartState.SUCCESS;
|
||||
metricsOptedOut: boolean;
|
||||
upcomingInvoicePreview: InvoicePreview;
|
||||
latestInvoicePreview: InvoicePreview;
|
||||
paymentInfo: PaymentInfo;
|
||||
};
|
||||
|
||||
export type UpgradeCart = WithContextCart & {
|
||||
export type UpgradeCart = ResultCart & {
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE;
|
||||
metricsOptedOut: boolean;
|
||||
upcomingInvoicePreview: InvoicePreview;
|
||||
fromOfferingConfigId: string;
|
||||
oneTimeCharge: number;
|
||||
upgradeFromPrice: CurrentPrice;
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Localized } from '@fluent/react';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { Invoice } from '@fxa/payments/cart';
|
||||
import { InvoicePreview } from '@fxa/payments/customer';
|
||||
import infoLogo from '@fxa/shared/assets/images/info.svg';
|
||||
import {
|
||||
getLocalizedCurrencyString,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
import chevron from './images/chevron.svg';
|
||||
|
||||
type PurchaseDetailsProps = {
|
||||
invoice: Invoice;
|
||||
invoice: InvoicePreview;
|
||||
priceInterval: React.ReactNode;
|
||||
purchaseDetails: {
|
||||
details: string[];
|
||||
|
|
|
@ -6,10 +6,11 @@ import { Injectable } from '@nestjs/common';
|
|||
import { Validator } from 'class-validator';
|
||||
|
||||
import { GoogleManager } from '@fxa/google';
|
||||
import { CartService } from '@fxa/payments/cart';
|
||||
import { CartService, UpgradeCart } from '@fxa/payments/cart';
|
||||
import { ContentServerManager } from '@fxa/payments/content-server';
|
||||
import { CheckoutTokenManager } from '@fxa/payments/paypal';
|
||||
import { ProductConfigurationManager } from '@fxa/shared/cms';
|
||||
import { CartEligibilityStatus } from '@fxa/shared/db/mysql/account';
|
||||
|
||||
import { CheckoutCartWithPaypalActionArgs } from './validators/CheckoutCartWithPaypalActionArgs';
|
||||
import { CheckoutCartWithStripeActionArgs } from './validators/CheckoutCartWithStripeActionArgs';
|
||||
|
@ -60,12 +61,31 @@ export class NextJSActionsService {
|
|||
return cart;
|
||||
}
|
||||
|
||||
async getUpgradeCart(args: GetCartActionArgs) {
|
||||
async getUpgradeCart(args: GetCartActionArgs): Promise<UpgradeCart> {
|
||||
await new Validator().validateOrReject(args);
|
||||
|
||||
const cart = await this.cartService.getUpgradeCart(args.cartId);
|
||||
const cart = await this.cartService.getCart(args.cartId);
|
||||
|
||||
return cart;
|
||||
if (cart.eligibilityStatus !== CartEligibilityStatus.UPGRADE)
|
||||
throw new Error('Cart eligibility is not upgrade');
|
||||
|
||||
if (
|
||||
'fromOfferingConfigId' in cart &&
|
||||
cart.fromOfferingConfigId !== undefined &&
|
||||
'upgradeFromPrice' in cart &&
|
||||
cart.upgradeFromPrice !== undefined
|
||||
) {
|
||||
return {
|
||||
...cart,
|
||||
eligibilityStatus: CartEligibilityStatus.UPGRADE,
|
||||
fromOfferingConfigId: cart.fromOfferingConfigId,
|
||||
upgradeFromPrice: cart.upgradeFromPrice,
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
'ActionsService - cart is missing required fields for upgrade'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async updateCart(args: UpdateCartActionArgs) {
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Image from 'next/image';
|
||||
import { CurrentPrice, Invoice } from '@fxa/payments/cart';
|
||||
import { CurrentPrice } from '@fxa/payments/cart';
|
||||
import { InvoicePreview } from '@fxa/payments/customer';
|
||||
import { PriceInterval } from '@fxa/payments/ui/server';
|
||||
import { LocalizerRsc } from '@fxa/shared/l10n/server';
|
||||
|
||||
|
@ -15,7 +16,7 @@ type UpgradePurchaseDetailsProps = {
|
|||
webIcon: string;
|
||||
};
|
||||
interval: string;
|
||||
invoice: Invoice;
|
||||
invoice: InvoicePreview;
|
||||
l10n: LocalizerRsc;
|
||||
purchaseDetails: {
|
||||
subtitle: string | null;
|
||||
|
|
Загрузка…
Ссылка в новой задаче