[Solution] new mandatory parameters VISA/Master/Sibs - new mandatory fields

[Solution] new mandatory parameters VISA/Master/Sibs - new mandatory fields

Document Status

CLOSED

Document Owner(s)

@Capovilla, Evandro (Deactivated)

Reviewers

@Ferreira Gomes, Raphael (Deactivated)
@Felipe Rooke
@Garozzo de Sobral, Wellington
@Augusto Romao, Vinicius (Deactivated)

Potential Solutions

Assumptions

  • This solution does not include the “browser height/width” information because paycomet did not implement and does not provide access to send this new field.

  • The address fields provided are not optional and are a new request for Visa/master/Sibs payments as a way of detecting fraud.

  • The email will be mandatory and telephone number optional, but both will be sent to paycomet.

  • Only the delivery service mode will show the checkbox to use billing address.

  • The “use delivery address” checkbox starts the toggled and the information is pre-filled.

  • This solution is related to Paycomet.

  • For all card brands we send all the information in the mastercard column in the table below.

Payments Mandatory Fields

Fields

Visa

Mastercard

Fields

Visa

Mastercard

Card Number

Name on Card

Expiry

CVV

IP Address

Address Line 1

(new field)

Address Line 2

(new field)

Postal Code

(new field)

City

(new field)

State

(new field)

Country

(new field)

Email (user data)

(new field)

Cellphone (user data)

(if email is sent)

Legend

Mandatory fields

Optional fields

#1 - Create new fields for the new data needed.

The idea is to create a new input for the user to fill in and we send this information in addition to other relevant information such as the height and width of the browser (Paycomet is working to include height and width in their system, so this solution does not include those properties yet.). It was also agreed that the email and telephone fields will be sent via backend without showing these fields to the user and getting this information in user details.

The user will fill in the correct information.

We guarantee that all information provided to Paycomet/Visa/master/Sibs belongs to the user.

The user has more fields to fill in.

#2 - Send the pre-filled data of user details.

The idea is to use the user's pre-filled information and send it as a request to paycomet.

The user does not need to fill out all the forms.

The user data may not be the same as their Visa/master credit card information.

Proposed Solution

#1 - Create new fields for the new data needed.

Whitelabel App

Implement new input fields for users to provide additional data necessary for payment processing, specifically: billing address, email, and phone number. This data will be captured, validated, and transmitted to the Payment Service Provider (PSP) Paycomet.

Diagram

image-20241029-181552.png
title Make purchase/No pre-auth FE->Graphql:CommitOrder Graphql->Payment-Service:MakePayment Payment-Service->Paycomet-Service:MakePayment Paycomet-Service->Paycomet: executePurchase Paycomet-Service<-Paycomet: result Payment-Service<-Paycomet-Service:result Graphql<-Payment-Service:result FE<-Graphql:result
image-20241029-181815.png
title Pre-auth FE->Graphql:CreatePreAuth Graphql->Paycomet-Service:CreatePreAuth Paycomet-Service->Paycomet: createPreauthorization Paycomet-Service<-Paycomet: result Graphql<-Paycomet-Service:result FE<-Graphql:result

For this solution, we will only modify the payment request part, as shown in the diagram above. The entire flow after sending payment to paycomet will not be modified.

New Forms

Development

One-time / Pre-auth

Frontend

For postal/zip code we already have a field for this purpose, and it is hidden by the payment variation fields. We can use the same flag to enable or disable these new fields.

<TextInputPaycomet data-testid="address-city" id="addressCity" name="addressCity" type="hidden" value={addressCity} /> <TextInputPaycomet data-testid="address-country" id="addressCountry" name="addressCountry" type="hidden" value={addressCountry} /> <TextInputPaycomet data-testid="address-line" id="addressLine" name="addressLine" type="hidden" value={addressLine} /> <TextInputPaycomet data-testid="address-state" id="addressState" name="addressState" type="hidden" value={addressState} /> <TextInputPaycomet data-testid="phone-number" id="phoneNumber" name="phoneNumber" type="hidden" value={phoneNumber} /> <TextInputPaycomet data-testid="email" id="email" name="email" type="hidden" value={email} />

You could found other references in IPaymentState/IPaycometState and following the reference below.

<TextInput ... onChange={handleChange} value={paymentValues.billingStreetAddress} required data-paycomet="billingStreetAddress" name="billingStreetAddress" />

If you use the state solution, you must add the user field with phone number and email

export interface IPaycometState extends IPaymentState { ..., user?: { phoneNumber: string; email: string; }; }
generateCreatePreAuthorization({ variables: { input: { rbiOrderId: rbiOrder.rbiOrderId, cardHolderName: paymentValues.nameOnCard, ..., userPaymentDetails, billingAddress }, }, });
commitInput = { ..., payment: { ..., billingAddress: { locality: payCometValues.billingCity, //city country: payCometValues.billingCountry, //country streetAddress: payCometValues.billingStreetAddress, //addressLine postalCode: payCometValues.billingZip, //postalCode region: payCometValues.billingState, //state }, userPaymentDetails: { phoneNumber: payCometValues.phoneNumber, email: payCometValues.email, } }, ... };

Graphql

The graphql CommitOrder function has already been made to deal with billingAddress, I put the code here just for reference for name changes.

function mapBillingAddress( region: string, billing: IBillingAddress | undefined, ): PaymentClient.IAddress | undefined { if (!billing) { return { regionCode: region, }; } return { postalCode: billing.postalCode || undefined, // normalize empty string //postalCode administrativeArea: billing.region, //state locality: billing.locality, //city regionCode: billing.country ?? region, //country streetNumber: billing.streetAddress, //address }; }
input AdditionalPayment { """ Represents the phone number of the user who owns the requested payment """ phoneNumber: String """ Represents the email of the user who owns the requested payment """ email: String } input PaycometPayment { ... """ Additional Payment Information """ userPaymentDetails: UserPaymentDetails }
paycometSale = { ..., userPaymentDetails: { email: payment?.paycometInput?.userPaymentDetails?.email || '', phoneNumber: payment?.paycometInput?.userPaymentDetails?.phoneNumber || '', }, };
export interface IUserPaymentDetails { email: string; phoneNumber: string; } export interface ICreatePreAuthPaycometRequest { userPaymentDetails: IUserPaymentDetails } public async createPreAuthorizationPaycomet({ .... }) { .... const paycometPreauth = await this.paymentsClient.paycometClient.request<ICreatePreAuthorizationResponse>( (apis) => apis.paymentsApi.createPreAuthorization({ iCreatePreAuthorizationRequest: { ..., billingAddress, user: { email, phonenumber, } }, region: regionCountry, }), ); ... }

Intl-Packages

export class PaymentBaseRequestDto extends PaymentBase { @IsObject() @IsOptional() @ApiProperty({ required: false, type: UserPaymentDetails, description: "User's payment additional information", }) public userPaymentDetails?: UserPaymentDetails; } export class UserPaymentDetails { @IsObject() @IsOptional() @ApiProperty({ required: false, type: UserPaymentDetails, description: "User's mobile phone", }) public mobilePhone?: string; @IsObject() @IsOptional() @ApiProperty({ required: false, type: UserPaymentDetails, description: "User's email", }) public email?: string; }
export interface IUserPaymentDetails { email: string; phoneNumber: string; } export interface ICreatePreAuthPaycometRequest { userPaymentDetails: IUserPaymentDetails } export interface ICreatePreAuthorizationRequest { billingAddress: IBillingAddress, userPaymentDetails: IUserPaymentDetails }

Payment-service

const pspRequestDto: PaymentBaseRequestDto = { ..., billingAddress: requestDto.billingAddress, //already exists userPaymentDetails: { email: requestDto.user.email, phoneNumber: requestDto.user.phoneNumber } ..., }; try { const resp = await this.pspClient.makePayment(region, psp, pspRequestDto); ... } ... }

Paycomet-service

const response: ExecutePurchase.Response = await this.client.exec( region, new ExecutePurchase.Request({ payment: { ..., merchantData: { billing: { billAddrCity: request.billingAddress?.locality, billAddrCountry: request.billingAddress?.regionCode, billAddrLine1: request.billingAddress?.streetNumber, billAddrPostCode: request.billingAddress?.postalCode, billAddrState: request.billingAddress?.administrativeArea, }, customer: { mobilePhone: { cc: mappedPhone.cc subscriber: mappedPhone.subscriber } email: request.user.email, } }, }, }), );
interface IBillingData { billAddrCity?: string; billAddrCountry?: string; billAddrLine1?: string; billAddrPostCode?: string; billAddrState?: string; } interface ICustomerData{ mobilePhone: IMobilePhone; email: string; } interface IMobilePhone{ cc: string; subscriber: string; } interface IMerchantData { billing?: IBillingData; customer?: ICustomerData; } interface IRequestPaymentBody { ... merchantData: IMerchantData; ... }
export abstract class PreAuthBodyDto { ... @ApiProperty({ required: true, description: 'User required payment details', oneOf: [{ $ref: getSchemaPath(UserPaymentDetails) }], }) @IsObject() @ValidateNested() public userPaymentDetails!: UserPaymentDetailsDto; } export class UserPaymentDetailsDto { @ApiProperty({ required: true, example: 'john@gmail.com', description: 'User payment email ', }) @IsString() public email!: string; @ApiProperty({ required: true, example: '351333399999', description: 'User payment phone number', }) @IsString() public phoneNumber!: string; }
const authRequest = new TransactionAuthPre.Request({ payment: { ..., merchantData: { billing: { billAddrCity: request.billingAddress?.locality, billAddrCountry: request.billingAddress?.regionCode, billAddrLine1: request.billingAddress?.streetNumber, billAddrPostCode: request.billingAddress?.postalCode, billAddrState: request.billingAddress?.administrativeArea, }, customer: { mobilePhone: { cc: mappedPhone.cc, subscriber: mappedPhone.subscriber, } email: request.userPaymentDetails?.email, } }, ..., }, });
interface IBillingData { billAddrCity?: string; billAddrCountry?: string; billAddrLine1?: string; billAddrPostCode?: string; billAddrState?: string; } interface ICustomerData{ mobilePhone: IMobilePhone; email: string; } interface IMobilePhone{ cc: string; subscriber: string; } interface IMerchantData { billing?: IBillingData; customer?: ICustomerData; } interface IRequestPaymentBody { ... merchantData: IMerchantData; }

 

Vaulted

Intl-Packages

The idea of ​​this story is to follow the same flow that VRPayment did to save the billing address on their side. One of the ways they did this is to send the billingAddress information and add it to the user's account along with the card. We can follow the processVrCreditPayment function in src/functions/graphql/providers/payments.ts.

For the existing vaulted card, we can add the option for the user to add the billingAddress below the vaulted card option, if they do not already have this information. Vaulted cards are obtained throughgetUserAccounts - src/functions/graphql/providers/payments.ts

 

  • We will need to create a new method to add the Paycomet accounts in the PaycometClient class:

    • path: intl-packages/packages/payments/src/services/paycomet/paycomet-client.ts

export interface IPaycometAddAccountParams extends IAddAccountParams { accountIdentifier: string; bin?: string; cardType?: string; expiryMonth?: string; expiryYear?: string; last4?: string; } export class PaycometClient { ... public async addAccount(params: IPaycometAddAccountParams): Promise<IAddAccountResult> { const account = await this.accountService.create({ credit: { billingAddress: params.billingAddress, }, storedPaymentMethodId: params.accountIdentifier, userId: params.userId, } as ICreateAccountPaycometPayment); this.logger.info( { billingAddress: params.billingAddress, storedPaymentMethodId: account?.accountIdentifier, userId: params.userId, }, 'Created Paycomet payment account for user.', ); return { body: account, statusCode: HttpStatus.OK, type: AddAccountResultType.AccountCreated, }; } }
  • After this, we will export the Paycomet client in the payment index.ts to access the interfaces and classes on GraphQl and Fulfillment repos.

    • path: intl-packages/packages/payments/src/index.ts

... export * as Paycomet from './services/paycomet/paycomet-client';
  • The next step will be to update the params type in the create method in the Accounts Class with the new interface ICreateAccountPaycometPayment

    • path: intl-packages/packages/payments/src/services/account-service/accounts.ts

export interface ICreateAccountPaycometPayment extends ICreateAccount { storedPaymentMethodId: string; credit: ICredit; userId: string; } public async create( params: ... | ICreateAccountPaycometPayment, ): Promise<AccountItem> { ... }
  • In the end, we will update the addAccount method changing the params type, adding the IPaycometAddAccountParams, and adding the new condition to call the add account for Paycomet.

    • path: intl-packages/packages/payments/src/client.ts

public async addAccount( params: ... | IPaycometAddAccountParams, ): Promise<IAddAccountResult | undefined> { ... if (paymentClient instanceof PaycometClient) { return paymentClient.addAccount(params as IPaycometAddAccountParams); } ... }
  • In this method, we can refactor the code to make it better.

Graphql

After the intl-package merge in the intl-whitelabel-graphql

  • We will need to call the addAccount (in the intl-payment lib) and create on intl-packages the last steps, in the makePayment.

    • path: intl-whitelabel-graphql/src/functions/graphql/utils/make-payment.ts

import { Paycomet } from '@rbilabs/intl-payments/'; ... public async makePayment(params: IPaycometPaymentSaleEvent): Promise<ISalePaycometResult> { ... if (resp.data.result === PaymentClient.IResultCode.Authorized && resp.data.paymentMethod) { ... const paycometAddAccount: Paycomet.IPaycometAddAccountParams = { accountIdentifier: params.storedPaymentMethodId ?? '', billingAddress: params.billingAddress ?? {}, bin: params.bin, cardType: params.cardType, expiryMonth: resp?.data?.paymentMethod?.expiryMonth, expiryYear: resp?.data?.paymentMethod?.expiryYear, fullName: params.fullName ?? '', isoCountryCode: params.country, last4: resp?.data?.paymentMethod?.last4, userId: params.userId ?? '', }; await this.client.addAccount(paycometAddAccount); ... } }

 

 

Fulfillment

[THE SAME THING MADE IN THE GRAPHQL] - After the intl-package merge in the intl-whitelabel-graphql

  • We will need to call the addAccount (in the intl-payment lib) and create on intl-packages the last steps, in the makePayment.

    • path: intl-fulfillment-service/src/modules/legacy/utils/make-payment.provider.ts

import { Paycomet } from '@rbilabs/intl-payments/'; ... public async makePayment( paramsPaycomet: IPaycometPaymentSaleEvent, ): Promise<ISalePaycometResult> { ... if (paymentData.result === PaymentClient.IResultCode.Authorized && paymentData.paymentMethod) { ... const paycometAddAccount: Paycomet.IPaycometAddAccountParams = { accountIdentifier: paramsPaycomet.storedPaymentMethodId ?? '', billingAddress: paramsPaycomet.billingAddress ?? {}, bin: paramsPaycomet.bin ?? '', cardType: paramsPaycomet.cardType, expiryMonth: paymentData?.paymentMethod?.expiryMonth, expiryYear: paymentData?.paymentMethod?.expiryYear, fullName: paramsPaycomet.fullName ?? '', isoCountryCode: paramsPaycomet.country, last4: paymentData?.paymentMethod?.last4, userId: paramsPaycomet.userId ?? '', }; await this.client.addAccount(paycometAddAccount); ... } }

 

 

Frontend

For the WL front-end solution, we’ll just reuse the form that we have inside the “add new credit card” form:

image-20241122-153829.png

Reinforcing the expected behavior:

  • If the vaulted card doesn’t have the mandatory fields we need to show the form below the selected dropdown

  • If the vaulted card has the mandatory fields we’ll not show the form

  • If this process need some time to get the necessary data we should consider add a loading/shimmer effect to this new part of the UI

Tasks breakdown
  1. Isolate the billing address form to a new reusable component (+ container component to use it)

    • File where the original form is: src/pages/cart/payment/order-payment/paycomet-hosted-page-payment/paycomet-credit-card-form/paycomet-credit-card-form.tsx

    • Here we want to reuse the same form in both places (inside the original form and now below the dropdown)

  2. Add the necessary query and logic to show the form in the container component

    • Here we’ll get the necessary data to discover if the vaulted card has the mandatory field or not. If not, show the form as expected

    • To get the account data, we can use the Query UserAccounts

      • We will need to add the billingAddress in the query

        • path: intl-whitelabel-app/workspaces/frontend/src/queries/rbi/payments.graphql

    • After this, we will add this Query in the hook to get the account data.

    • If we need to delete the account, we can use the DeleteAccount Mutation

query UserAccounts($feCountryCode: IsoCountryCode!) @authRequired @authGuestAllowed @gateway(flag: "enable-gateway-payments") { userAccounts(feCountryCode: $feCountryCode) { ... accounts { ... credit { ... billingAddress { locality postalCode region streetAddress unitNumber country } } ... } ... } }
  1. Add and integrate the new component inside paycomet-hosted-page-payment below the dropdown and above totals info

    • File: src/pages/cart/payment/order-payment/paycomet-hosted-page-payment/paycomet-hosted-page-payment.tsx

    • Ensure that the form will keep the same behavior that we have inside the “add new credit card” form, as follows:

      • By default, the checkbox should always be checked.

      • Fields should be filled-in with address information from the checkout page

      • If unchecked, all fields should be cleared

    • The information in this form should be used to be sent when placing the order as expected, following the “add new credit card” idea

Configuration

Include here any configuration required (Sanity, LaunchDarkly flags, etc.) required to enable the solution.

Temporary Feature Flag

Temporary Feature Flag

payment-fields-variations

QA Plan

[Test Cases] [Deprecated] Visa/Sibs - new mandatory fields

Call-outs

This functionality is made exclusively for the PSP Paycomet which is used in the Spanish and Portuguese environments.

Figma File