Merge pull request #16992 from mozilla/next-temp-email-and-fixes

feat(next): set email and display name
This commit is contained in:
Reino Muhl 2024-05-30 16:51:53 -04:00 коммит произвёл GitHub
Родитель 943d591cf1 a9d84adcf6
Коммит 8d3c84a253
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
18 изменённых файлов: 182 добавлений и 117 удалений

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

@ -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;
}