зеркало из https://github.com/mozilla/fxa.git
Merge pull request #16992 from mozilla/next-temp-email-and-fixes
feat(next): set email and display name
This commit is contained in:
Коммит
8d3c84a253
|
@ -15,6 +15,8 @@ import {
|
|||
getContentfulContent,
|
||||
} from 'apps/payments/next/app/_lib/apiClient';
|
||||
import { PaymentSection } from '@fxa/payments/ui';
|
||||
import { PrimaryButton } from 'libs/payments/ui/src/lib/client/components/PrimaryButton';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
@ -84,8 +86,34 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
|
|||
|
||||
<hr className="mx-auto w-full border-grey-200" />
|
||||
|
||||
<div className="h-64 text-center flex items-center justify-center">
|
||||
{'<placeholder>Passwordless signup</placeholder>'}
|
||||
<div className="p-6 text-center">
|
||||
{/**
|
||||
Temporary Content. This will be replaced in M3b by the Passwordless
|
||||
email signup form.
|
||||
*/}
|
||||
<p className="mb-6">{`Current cart email: ${cart.email}`}</p>
|
||||
<form
|
||||
action={async (formData: FormData) => {
|
||||
'use server';
|
||||
const email =
|
||||
formData.get('email')?.toString() || 'test@example.com';
|
||||
await app.getActionsService().updateCart({
|
||||
cartId: cart.id,
|
||||
version: cart.version,
|
||||
cartDetails: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
revalidatePath('/');
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
|
||||
/>
|
||||
<PrimaryButton type="submit"> Set email</PrimaryButton>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr className="mx-auto w-full border-grey-200" />
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
CartState,
|
||||
} from '@fxa/shared/db/mysql/account';
|
||||
import {
|
||||
CheckoutCustomerData,
|
||||
FinishCart,
|
||||
FinishErrorCart,
|
||||
ResultCart,
|
||||
|
@ -29,6 +30,14 @@ const OFFERING_CONFIG_IDS = [
|
|||
|
||||
const INTERVALS = ['daily', 'weekly', 'monthly', '6monthly', 'yearly'];
|
||||
|
||||
export const CheckoutCustomerDataFactory = (
|
||||
override?: Partial<CheckoutCustomerData>
|
||||
): CheckoutCustomerData => ({
|
||||
locale: faker.helpers.arrayElement(['en-US', 'de', 'es', 'fr-FR']),
|
||||
displayName: faker.person.fullName(),
|
||||
...override,
|
||||
});
|
||||
|
||||
export const SetupCartFactory = (override?: Partial<SetupCart>): SetupCart => ({
|
||||
email: 'test@example.com',
|
||||
offeringConfigId: faker.helpers.arrayElement(OFFERING_CONFIG_IDS),
|
||||
|
|
|
@ -205,7 +205,7 @@ describe('CartManager', () => {
|
|||
it('succeeds', async () => {
|
||||
const items = FinishErrorCartFactory();
|
||||
|
||||
await cartManager.finishErrorCart(testCart.id, testCart.version, items);
|
||||
await cartManager.finishErrorCart(testCart.id, items);
|
||||
const cart = await cartManager.fetchCartById(testCart.id);
|
||||
|
||||
expect(cart).toEqual(expect.objectContaining(items));
|
||||
|
@ -218,7 +218,6 @@ describe('CartManager', () => {
|
|||
try {
|
||||
await cartManager.finishErrorCart(
|
||||
testCart.id,
|
||||
testCart.version,
|
||||
FinishErrorCartFactory()
|
||||
);
|
||||
fail('Error in finishErrorCart');
|
||||
|
|
|
@ -161,17 +161,13 @@ export class CartManager {
|
|||
}
|
||||
}
|
||||
|
||||
public async finishErrorCart(
|
||||
cartId: string,
|
||||
version: number,
|
||||
items: FinishErrorCart
|
||||
) {
|
||||
const cart = await this.fetchAndValidateCartVersion(cartId, version);
|
||||
public async finishErrorCart(cartId: string, items: FinishErrorCart) {
|
||||
const cart = await this.fetchCartById(cartId);
|
||||
|
||||
this.checkActionForValidCartState(cart, 'finishErrorCart');
|
||||
|
||||
try {
|
||||
await updateCart(this.db, Buffer.from(cartId, 'hex'), version, {
|
||||
await updateCart(this.db, Buffer.from(cartId, 'hex'), cart.version, {
|
||||
...items,
|
||||
uid: items.uid ? Buffer.from(items.uid, 'hex') : undefined,
|
||||
state: CartState.FAIL,
|
||||
|
|
|
@ -52,6 +52,7 @@ import {
|
|||
import { MockStatsDProvider } from '@fxa/shared/metrics/statsd';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
import {
|
||||
CheckoutCustomerDataFactory,
|
||||
FinishErrorCartFactory,
|
||||
ResultCartFactory,
|
||||
UpdateCartFactory,
|
||||
|
@ -192,8 +193,9 @@ describe('CartService', () => {
|
|||
});
|
||||
|
||||
describe('checkoutCartWithStripe', () => {
|
||||
const mockCustomerData = CheckoutCustomerDataFactory();
|
||||
|
||||
it('accepts payment with stripe', async () => {
|
||||
const locale = 'en-US';
|
||||
const mockCart = ResultCartFactory();
|
||||
const mockPaymentMethodId = faker.string.uuid();
|
||||
|
||||
|
@ -205,14 +207,14 @@ describe('CartService', () => {
|
|||
await cartService.checkoutCartWithStripe(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
locale,
|
||||
mockPaymentMethodId
|
||||
mockPaymentMethodId,
|
||||
mockCustomerData
|
||||
);
|
||||
|
||||
expect(checkoutService.payWithStripe).toHaveBeenCalledWith(
|
||||
mockCart,
|
||||
locale,
|
||||
mockPaymentMethodId
|
||||
mockPaymentMethodId,
|
||||
mockCustomerData
|
||||
);
|
||||
expect(cartManager.finishCart).toHaveBeenCalledWith(
|
||||
mockCart.id,
|
||||
|
@ -223,7 +225,6 @@ describe('CartService', () => {
|
|||
});
|
||||
|
||||
it('calls cartManager.finishErrorCart when error occurs during checkout', async () => {
|
||||
const locale = 'en-US';
|
||||
const mockCart = ResultCartFactory();
|
||||
const mockPaymentMethodId = faker.string.uuid();
|
||||
|
||||
|
@ -235,23 +236,20 @@ describe('CartService', () => {
|
|||
await cartService.checkoutCartWithStripe(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
locale,
|
||||
mockPaymentMethodId
|
||||
mockPaymentMethodId,
|
||||
mockCustomerData
|
||||
);
|
||||
|
||||
expect(cartManager.finishErrorCart).toHaveBeenCalledWith(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
{
|
||||
errorReasonId: CartErrorReasonId.Unknown,
|
||||
}
|
||||
);
|
||||
expect(cartManager.finishErrorCart).toHaveBeenCalledWith(mockCart.id, {
|
||||
errorReasonId: CartErrorReasonId.Unknown,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkoutCartWithPaypal', () => {
|
||||
const mockCustomerData = CheckoutCustomerDataFactory();
|
||||
|
||||
it('accepts payment with Paypal', async () => {
|
||||
const locale = 'en-US';
|
||||
const mockCart = ResultCartFactory();
|
||||
const mockToken = faker.string.uuid();
|
||||
|
||||
|
@ -263,13 +261,13 @@ describe('CartService', () => {
|
|||
await cartService.checkoutCartWithPaypal(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
locale,
|
||||
mockCustomerData,
|
||||
mockToken
|
||||
);
|
||||
|
||||
expect(checkoutService.payWithPaypal).toHaveBeenCalledWith(
|
||||
mockCart,
|
||||
locale,
|
||||
mockCustomerData,
|
||||
mockToken
|
||||
);
|
||||
expect(cartManager.finishCart).toHaveBeenCalledWith(
|
||||
|
@ -292,16 +290,13 @@ describe('CartService', () => {
|
|||
await cartService.checkoutCartWithPaypal(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
mockCustomerData,
|
||||
mockToken
|
||||
);
|
||||
|
||||
expect(cartManager.finishErrorCart).toHaveBeenCalledWith(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
{
|
||||
errorReasonId: CartErrorReasonId.Unknown,
|
||||
}
|
||||
);
|
||||
expect(cartManager.finishErrorCart).toHaveBeenCalledWith(mockCart.id, {
|
||||
errorReasonId: CartErrorReasonId.Unknown,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -314,15 +309,12 @@ describe('CartService', () => {
|
|||
|
||||
await cartService.finalizeCartWithError(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
mockErrorCart.errorReasonId
|
||||
);
|
||||
|
||||
expect(cartManager.finishErrorCart).toHaveBeenCalledWith(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
{ errorReasonId: mockErrorCart.errorReasonId }
|
||||
);
|
||||
expect(cartManager.finishErrorCart).toHaveBeenCalledWith(mockCart.id, {
|
||||
errorReasonId: mockErrorCart.errorReasonId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should swallow error if cart already in fail state', async () => {
|
||||
|
@ -336,7 +328,6 @@ describe('CartService', () => {
|
|||
|
||||
await cartService.finalizeCartWithError(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
mockErrorCart.errorReasonId
|
||||
);
|
||||
|
||||
|
@ -355,7 +346,6 @@ describe('CartService', () => {
|
|||
await expect(
|
||||
cartService.finalizeCartWithError(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
mockErrorCart.errorReasonId
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
|
@ -373,7 +363,6 @@ describe('CartService', () => {
|
|||
await expect(
|
||||
cartService.finalizeCartWithError(
|
||||
mockCart.id,
|
||||
mockCart.version,
|
||||
mockErrorCart.errorReasonId
|
||||
)
|
||||
).rejects.toThrow(Error);
|
||||
|
|
|
@ -18,7 +18,12 @@ import { CartErrorReasonId, CartState } from '@fxa/shared/db/mysql/account';
|
|||
import { GeoDBManager } from '@fxa/shared/geodb';
|
||||
|
||||
import { CartManager } from './cart.manager';
|
||||
import { ResultCart, UpdateCart, WithUpcomingInvoiceCart } from './cart.types';
|
||||
import {
|
||||
CheckoutCustomerData,
|
||||
ResultCart,
|
||||
UpdateCart,
|
||||
WithUpcomingInvoiceCart,
|
||||
} from './cart.types';
|
||||
import { handleEligibilityStatusMap } from './cart.utils';
|
||||
import { CheckoutService } from './checkout.service';
|
||||
|
||||
|
@ -130,18 +135,22 @@ export class CartService {
|
|||
async checkoutCartWithStripe(
|
||||
cartId: string,
|
||||
version: number,
|
||||
locale: string,
|
||||
paymentMethodId: string
|
||||
paymentMethodId: string,
|
||||
customerData: CheckoutCustomerData
|
||||
) {
|
||||
try {
|
||||
const cart = await this.cartManager.fetchCartById(cartId);
|
||||
|
||||
await this.checkoutService.payWithStripe(cart, locale, paymentMethodId);
|
||||
await this.checkoutService.payWithStripe(
|
||||
cart,
|
||||
paymentMethodId,
|
||||
customerData
|
||||
);
|
||||
|
||||
await this.cartManager.finishCart(cartId, version, {});
|
||||
} catch (e) {
|
||||
// TODO: Handle errors and provide an associated reason for failure
|
||||
await this.cartManager.finishErrorCart(cartId, version, {
|
||||
await this.cartManager.finishErrorCart(cartId, {
|
||||
errorReasonId: CartErrorReasonId.Unknown,
|
||||
});
|
||||
}
|
||||
|
@ -150,18 +159,18 @@ export class CartService {
|
|||
async checkoutCartWithPaypal(
|
||||
cartId: string,
|
||||
version: number,
|
||||
locale: string,
|
||||
customerData: CheckoutCustomerData,
|
||||
token?: string
|
||||
) {
|
||||
try {
|
||||
const cart = await this.cartManager.fetchCartById(cartId);
|
||||
|
||||
this.checkoutService.payWithPaypal(cart, locale, token);
|
||||
this.checkoutService.payWithPaypal(cart, customerData, token);
|
||||
|
||||
await this.cartManager.finishCart(cartId, version, {});
|
||||
} catch (e) {
|
||||
// TODO: Handle errors and provide an associated reason for failure
|
||||
await this.cartManager.finishErrorCart(cartId, version, {
|
||||
await this.cartManager.finishErrorCart(cartId, {
|
||||
errorReasonId: CartErrorReasonId.Unknown,
|
||||
});
|
||||
}
|
||||
|
@ -173,11 +182,10 @@ export class CartService {
|
|||
*/
|
||||
async finalizeCartWithError(
|
||||
cartId: string,
|
||||
version: number,
|
||||
errorReasonId: CartErrorReasonId
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.cartManager.finishErrorCart(cartId, version, {
|
||||
await this.cartManager.finishErrorCart(cartId, {
|
||||
errorReasonId,
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
|
@ -10,6 +10,11 @@ import {
|
|||
CartState,
|
||||
} from '@fxa/shared/db/mysql/account';
|
||||
|
||||
export type CheckoutCustomerData = {
|
||||
locale: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export type FinishCart = {
|
||||
uid?: string;
|
||||
amount?: number;
|
||||
|
|
|
@ -50,6 +50,7 @@ import {
|
|||
} from '@fxa/shared/db/mysql/account';
|
||||
import {
|
||||
CartManager,
|
||||
CheckoutCustomerDataFactory,
|
||||
ResultCartFactory,
|
||||
handleEligibilityStatusMap,
|
||||
} from '@fxa/payments/cart';
|
||||
|
@ -113,8 +114,7 @@ describe('CheckoutService', () => {
|
|||
});
|
||||
|
||||
describe('prePaySteps', () => {
|
||||
const acceptLanguage = 'en-US';
|
||||
|
||||
const mockCustomerData = CheckoutCustomerDataFactory();
|
||||
const uid = faker.string.uuid();
|
||||
|
||||
const mockCustomer = StripeResponseFactory(
|
||||
|
@ -189,7 +189,7 @@ describe('CheckoutService', () => {
|
|||
|
||||
describe('success - with stripeCustomerId attached to cart', () => {
|
||||
beforeEach(async () => {
|
||||
await checkoutService.prePaySteps(mockCart, acceptLanguage);
|
||||
await checkoutService.prePaySteps(mockCart, mockCustomerData);
|
||||
});
|
||||
|
||||
it('fetches the customer', () => {
|
||||
|
@ -244,14 +244,14 @@ describe('CheckoutService', () => {
|
|||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
await checkoutService.prePaySteps(mockCart, acceptLanguage);
|
||||
await checkoutService.prePaySteps(mockCart, mockCustomerData);
|
||||
});
|
||||
|
||||
it('creates a new account customer stub account', () => {
|
||||
expect(accountManager.createAccountStub).toHaveBeenCalledWith(
|
||||
mockCart.email,
|
||||
1,
|
||||
acceptLanguage
|
||||
mockCustomerData.locale
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -287,13 +287,14 @@ describe('CheckoutService', () => {
|
|||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
await checkoutService.prePaySteps(mockCart, acceptLanguage);
|
||||
await checkoutService.prePaySteps(mockCart, mockCustomerData);
|
||||
});
|
||||
|
||||
it('creates a new stripe customer stub account', () => {
|
||||
expect(stripeManager.createPlainCustomer).toHaveBeenCalledWith({
|
||||
uid: uid,
|
||||
email: mockCart.email,
|
||||
displayName: mockCustomerData.displayName,
|
||||
taxAddress: mockCart.taxAddress,
|
||||
});
|
||||
});
|
||||
|
@ -317,7 +318,7 @@ describe('CheckoutService', () => {
|
|||
);
|
||||
|
||||
await expect(
|
||||
checkoutService.prePaySteps(mockCart, acceptLanguage)
|
||||
checkoutService.prePaySteps(mockCart, mockCustomerData)
|
||||
).rejects.toBeInstanceOf(CartEmailNotFoundError);
|
||||
});
|
||||
|
||||
|
@ -333,7 +334,7 @@ describe('CheckoutService', () => {
|
|||
);
|
||||
|
||||
await expect(
|
||||
checkoutService.prePaySteps(mockCart, acceptLanguage)
|
||||
checkoutService.prePaySteps(mockCart, mockCustomerData)
|
||||
).rejects.toBeInstanceOf(CartEligibilityMismatchError);
|
||||
});
|
||||
|
||||
|
@ -349,14 +350,14 @@ describe('CheckoutService', () => {
|
|||
);
|
||||
|
||||
await expect(
|
||||
checkoutService.prePaySteps(mockCart, acceptLanguage)
|
||||
checkoutService.prePaySteps(mockCart, mockCustomerData)
|
||||
).rejects.toBeInstanceOf(CartTotalMismatchError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payWithStripe', () => {
|
||||
const acceptLanguage = 'en-US';
|
||||
const mockCustomerData = CheckoutCustomerDataFactory();
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockCart = StripeResponseFactory(
|
||||
ResultCartFactory({
|
||||
|
@ -376,6 +377,7 @@ describe('CheckoutService', () => {
|
|||
const mockPaymentIntent = StripeResponseFactory(
|
||||
StripePaymentIntentFactory()
|
||||
);
|
||||
const mockPriceId = StripePriceFactory().id;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(checkoutService, 'prePaySteps').mockResolvedValue({
|
||||
|
@ -383,6 +385,7 @@ describe('CheckoutService', () => {
|
|||
customer: mockCustomer,
|
||||
enableAutomaticTax: true,
|
||||
promotionCode: mockPromotionCode,
|
||||
priceId: mockPriceId,
|
||||
});
|
||||
jest
|
||||
.spyOn(stripeClient, 'paymentMethodsAttach')
|
||||
|
@ -408,15 +411,15 @@ describe('CheckoutService', () => {
|
|||
beforeEach(async () => {
|
||||
await checkoutService.payWithStripe(
|
||||
mockCart,
|
||||
acceptLanguage,
|
||||
mockPaymentMethod.id
|
||||
mockPaymentMethod.id,
|
||||
mockCustomerData
|
||||
);
|
||||
});
|
||||
|
||||
it('calls prePaySteps', async () => {
|
||||
expect(checkoutService.prePaySteps).toHaveBeenCalledWith(
|
||||
mockCart,
|
||||
acceptLanguage
|
||||
mockCustomerData
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -449,7 +452,7 @@ describe('CheckoutService', () => {
|
|||
promotion_code: mockPromotionCode.id,
|
||||
items: [
|
||||
{
|
||||
price: undefined, // TODO: fetch price from cart after FXA-8893
|
||||
price: mockPriceId, // TODO: fetch price from cart after FXA-8893
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -474,7 +477,7 @@ describe('CheckoutService', () => {
|
|||
});
|
||||
|
||||
describe('payWithPaypal', () => {
|
||||
const acceptLanguage = 'en-US';
|
||||
const mockCustomerData = CheckoutCustomerDataFactory();
|
||||
const mockToken = faker.string.uuid();
|
||||
const mockCustomer = StripeResponseFactory(StripeCustomerFactory());
|
||||
const mockCart = StripeResponseFactory(
|
||||
|
@ -491,6 +494,7 @@ describe('CheckoutService', () => {
|
|||
const mockSubscription = StripeResponseFactory(StripeSubscriptionFactory());
|
||||
const mockPaypalCustomer = ResultPaypalCustomerFactory();
|
||||
const mockInvoice = StripeResponseFactory(StripeInvoiceFactory());
|
||||
const mockPriceId = StripePriceFactory().id;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(checkoutService, 'prePaySteps').mockResolvedValue({
|
||||
|
@ -498,6 +502,7 @@ describe('CheckoutService', () => {
|
|||
customer: mockCustomer,
|
||||
enableAutomaticTax: true,
|
||||
promotionCode: mockPromotionCode,
|
||||
priceId: mockPriceId,
|
||||
});
|
||||
jest
|
||||
.spyOn(paypalManager, 'getCustomerPayPalSubscriptions')
|
||||
|
@ -528,7 +533,7 @@ describe('CheckoutService', () => {
|
|||
beforeEach(async () => {
|
||||
await checkoutService.payWithPaypal(
|
||||
mockCart,
|
||||
acceptLanguage,
|
||||
mockCustomerData,
|
||||
mockToken
|
||||
);
|
||||
});
|
||||
|
@ -556,7 +561,7 @@ describe('CheckoutService', () => {
|
|||
promotion_code: mockPromotionCode.id,
|
||||
items: [
|
||||
{
|
||||
price: undefined, // TODO: fetch price from cart after FXA-8893
|
||||
price: mockPriceId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
CartEmailNotFoundError,
|
||||
} from './cart.error';
|
||||
import { CartManager } from './cart.manager';
|
||||
import { ResultCart } from './cart.types';
|
||||
import { CheckoutCustomerData, ResultCart } from './cart.types';
|
||||
import { handleEligibilityStatusMap } from './cart.utils';
|
||||
import { ContentfulService } from '@fxa/shared/contentful';
|
||||
import { AccountManager } from '@fxa/shared/account/account';
|
||||
|
@ -39,7 +39,7 @@ export class CheckoutService {
|
|||
private accountCustomerManager: AccountCustomerManager
|
||||
) {}
|
||||
|
||||
async prePaySteps(cart: ResultCart, locale: string) {
|
||||
async prePaySteps(cart: ResultCart, customerData: CheckoutCustomerData) {
|
||||
let customer, stripeCustomerId, uid;
|
||||
const taxAddress = cart.taxAddress as any as TaxAddress;
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class CheckoutService {
|
|||
uid = await this.accountManager.createAccountStub(
|
||||
cart.email,
|
||||
1, // verifierVersion
|
||||
locale
|
||||
customerData.locale
|
||||
);
|
||||
} else {
|
||||
uid = cart.uid;
|
||||
|
@ -63,9 +63,10 @@ export class CheckoutService {
|
|||
// if stripeCustomerId not found, create plain stripe account
|
||||
if (!cart.stripeCustomerId) {
|
||||
customer = await this.stripeManager.createPlainCustomer({
|
||||
uid: uid,
|
||||
uid,
|
||||
email: cart.email,
|
||||
taxAddress: taxAddress,
|
||||
displayName: customerData.displayName,
|
||||
taxAddress,
|
||||
});
|
||||
|
||||
stripeCustomerId = customer.id;
|
||||
|
@ -150,6 +151,7 @@ export class CheckoutService {
|
|||
customer,
|
||||
enableAutomaticTax,
|
||||
promotionCode,
|
||||
priceId,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -163,11 +165,11 @@ export class CheckoutService {
|
|||
|
||||
async payWithStripe(
|
||||
cart: ResultCart,
|
||||
locale: string,
|
||||
paymentMethodId: string
|
||||
paymentMethodId: string,
|
||||
customerData: CheckoutCustomerData
|
||||
) {
|
||||
const { customer, enableAutomaticTax, promotionCode } =
|
||||
await this.prePaySteps(cart, locale);
|
||||
const { customer, enableAutomaticTax, promotionCode, priceId } =
|
||||
await this.prePaySteps(cart, customerData);
|
||||
|
||||
await this.stripeClient.paymentMethodsAttach(paymentMethodId, {
|
||||
customer: customer.id,
|
||||
|
@ -189,7 +191,7 @@ export class CheckoutService {
|
|||
promotion_code: promotionCode?.id,
|
||||
items: [
|
||||
{
|
||||
price: undefined, // TODO: fetch price from cart after FXA-8893
|
||||
price: priceId,
|
||||
},
|
||||
],
|
||||
// TODO: Generate and use idempotency key using util
|
||||
|
@ -225,9 +227,13 @@ export class CheckoutService {
|
|||
await this.postPaySteps(cart, subscription);
|
||||
}
|
||||
|
||||
async payWithPaypal(cart: ResultCart, locale: string, token?: string) {
|
||||
const { uid, customer, enableAutomaticTax, promotionCode } =
|
||||
await this.prePaySteps(cart, locale);
|
||||
async payWithPaypal(
|
||||
cart: ResultCart,
|
||||
customerData: CheckoutCustomerData,
|
||||
token?: string
|
||||
) {
|
||||
const { uid, customer, enableAutomaticTax, promotionCode, priceId } =
|
||||
await this.prePaySteps(cart, customerData);
|
||||
|
||||
const paypalSubscriptions =
|
||||
await this.paypalManager.getCustomerPayPalSubscriptions(customer.id);
|
||||
|
@ -251,7 +257,7 @@ export class CheckoutService {
|
|||
promotion_code: promotionCode?.id,
|
||||
items: [
|
||||
{
|
||||
price: undefined, // TODO: fetch price from cart after FXA-8893
|
||||
price: priceId,
|
||||
},
|
||||
],
|
||||
// TODO: Generate and use idempotency key
|
||||
|
|
|
@ -79,7 +79,7 @@ export class StripeManager {
|
|||
async createPlainCustomer(args: {
|
||||
uid: string;
|
||||
email: string;
|
||||
displayName?: string;
|
||||
displayName: string;
|
||||
taxAddress?: TaxAddress;
|
||||
}) {
|
||||
const { uid, email, displayName, taxAddress } = args;
|
||||
|
|
|
@ -11,14 +11,14 @@ import { CheckoutCartWithPaypalActionArgs } from '../nestapp/validators/Checkout
|
|||
export const checkoutCartWithPaypal = async (
|
||||
cartId: string,
|
||||
version: number,
|
||||
locale: string,
|
||||
customerData: { locale: string; displayName: string },
|
||||
token?: string
|
||||
) => {
|
||||
await app.getActionsService().checkoutCartWithPaypal(
|
||||
plainToClass(CheckoutCartWithPaypalActionArgs, {
|
||||
cartId,
|
||||
version,
|
||||
locale,
|
||||
customerData,
|
||||
token,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -6,19 +6,22 @@
|
|||
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { app } from '../nestapp/app';
|
||||
import { CheckoutCartWithStripeActionArgs } from '../nestapp/validators/CheckoutCartWithStripeActionArgs';
|
||||
import {
|
||||
CheckoutCartWithStripeActionArgs,
|
||||
CheckoutCartWithStripeActionCustomerData,
|
||||
} from '../nestapp/validators/CheckoutCartWithStripeActionArgs';
|
||||
|
||||
export const checkoutCartWithStripe = async (
|
||||
cartId: string,
|
||||
version: number,
|
||||
locale: string,
|
||||
paymentMethodId: string
|
||||
paymentMethodId: string,
|
||||
customerData: CheckoutCartWithStripeActionCustomerData
|
||||
) => {
|
||||
await app.getActionsService().checkoutCartWithStripe(
|
||||
plainToClass(CheckoutCartWithStripeActionArgs, {
|
||||
cartId,
|
||||
version,
|
||||
locale,
|
||||
customerData,
|
||||
paymentMethodId,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -11,14 +11,12 @@ import { stripeErrorToErrorReasonId } from '@fxa/payments/cart';
|
|||
|
||||
export const handleStripeErrorAction = async (
|
||||
cartId: string,
|
||||
version: number,
|
||||
stripeError: StripeError
|
||||
) => {
|
||||
const errorReasonId = stripeErrorToErrorReasonId(stripeError);
|
||||
|
||||
await app.getActionsService().finalizeCartWithError({
|
||||
cartId,
|
||||
version,
|
||||
errorReasonId,
|
||||
});
|
||||
|
||||
|
|
|
@ -100,17 +100,15 @@ export function CheckoutForm({ readOnly, cart, locale }: CheckoutFormProps) {
|
|||
if (methodError.type === 'validation_error') {
|
||||
return;
|
||||
} else {
|
||||
await handleStripeErrorAction(cart.id, cart.version, methodError);
|
||||
await handleStripeErrorAction(cart.id, methodError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await checkoutCartWithStripe(
|
||||
cart.id,
|
||||
cart.version,
|
||||
await checkoutCartWithStripe(cart.id, cart.version, paymentMethod.id, {
|
||||
locale,
|
||||
paymentMethod.id
|
||||
);
|
||||
displayName: fullName,
|
||||
});
|
||||
|
||||
// TODO - To be added in M3B - Redirect customer to '/processing' page
|
||||
router.push('./start');
|
||||
|
|
|
@ -73,7 +73,6 @@ export class NextJSActionsService {
|
|||
|
||||
await this.cartService.finalizeCartWithError(
|
||||
args.cartId,
|
||||
args.version,
|
||||
args.errorReasonId
|
||||
);
|
||||
}
|
||||
|
@ -92,7 +91,7 @@ export class NextJSActionsService {
|
|||
await this.cartService.checkoutCartWithPaypal(
|
||||
args.cartId,
|
||||
args.version,
|
||||
args.locale,
|
||||
args.customerData,
|
||||
args.token
|
||||
);
|
||||
}
|
||||
|
@ -103,8 +102,8 @@ export class NextJSActionsService {
|
|||
await this.cartService.checkoutCartWithStripe(
|
||||
args.cartId,
|
||||
args.version,
|
||||
args.locale,
|
||||
args.paymentMethodId
|
||||
args.paymentMethodId,
|
||||
args.customerData
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,21 @@
|
|||
* 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 { IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CheckoutCartWithPaypalActionCustomerData {
|
||||
@IsString()
|
||||
locale!: string;
|
||||
|
||||
@IsString()
|
||||
displayName!: string;
|
||||
}
|
||||
|
||||
export class CheckoutCartWithPaypalActionArgs {
|
||||
@IsString()
|
||||
|
@ -11,10 +25,11 @@ export class CheckoutCartWithPaypalActionArgs {
|
|||
@IsNumber()
|
||||
version!: number;
|
||||
|
||||
@IsString()
|
||||
locale!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
token?: string;
|
||||
|
||||
@Type(() => CheckoutCartWithPaypalActionCustomerData)
|
||||
@ValidateNested()
|
||||
customerData!: CheckoutCartWithPaypalActionCustomerData;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,16 @@
|
|||
* 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 { IsNumber, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNumber, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class CheckoutCartWithStripeActionCustomerData {
|
||||
@IsString()
|
||||
locale!: string;
|
||||
|
||||
@IsString()
|
||||
displayName!: string;
|
||||
}
|
||||
|
||||
export class CheckoutCartWithStripeActionArgs {
|
||||
@IsString()
|
||||
|
@ -11,9 +20,10 @@ export class CheckoutCartWithStripeActionArgs {
|
|||
@IsNumber()
|
||||
version!: number;
|
||||
|
||||
@IsString()
|
||||
locale!: string;
|
||||
|
||||
@IsString()
|
||||
paymentMethodId!: string;
|
||||
|
||||
@Type(() => CheckoutCartWithStripeActionCustomerData)
|
||||
@ValidateNested()
|
||||
customerData!: CheckoutCartWithStripeActionCustomerData;
|
||||
}
|
||||
|
|
|
@ -2,16 +2,13 @@
|
|||
* 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 { IsNumber, IsString, ValidateNested } from 'class-validator';
|
||||
import { IsString, ValidateNested } from 'class-validator';
|
||||
import { CartErrorReasonId } from '@fxa/shared/db/mysql/account';
|
||||
|
||||
export class FinalizeCartWithErrorArgs {
|
||||
@IsString()
|
||||
cartId!: string;
|
||||
|
||||
@IsNumber()
|
||||
version!: number;
|
||||
|
||||
@ValidateNested()
|
||||
errorReasonId!: CartErrorReasonId;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче