Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Whitelabel App: https://github.com/rbilabs/intl-whitelabel-app

POC: https://github.com/rbilabs/intl-whitelabel-app/pull/3219/

Figma: Schroer, Gabriel (Deactivated)

Table of Contents

Sequence Diagram

Lucidchart
pageCount1
autoUpdatefalse
alignleft
typerich
autoSize1
macroId212c18aa-82a2-44ac-8050-f390618c5f25
instanceId920ae5b1-83d6-3e36-b794-1710780f64f3
pages
width700
documentTokenv2_553b49157110c9a058af38fe91f599cb1b5e1258ab98da315d6f1ca4a1227d7e-a=133831322&c=920ae5b1-83d6-3e36-b794-1710780f64f3&d=68c0744f-0f16-45a8-98dc-41f5610b0e39&p=4447830094
documentId68c0744f-0f16-45a8-98dc-41f5610b0e39
updated1705609545171
height500

...

Business rules

  • A new Bizum payment must be created for Paycomet.

  • This payment must work the same as Paycomet's PayPal, it must generate a link and be used in an Iframe where the payment will be processed.

  • The confirmation/tracking page should show the correct text in the payment reminder for this new payment method

  • The receipt email should show the correct payment option for this new payment method

  • The new method should be available in the user account methods list

  • We have to pay attention to changes from the Payments team, they are making some changes to their routine, and this new implementation cannot change the current behavior.

    There are two flags that change the application's behavior: enable-paycomet-webhook-notifications (BE) and enable-backend-commit-order (BE & FE), we must test both ways, with the flags enabled and disabled.

...

Tasks breakdown

Task 1: Create a new feature flag

Flag should be added in: src/utils/launchdarkly/flags.ts

...

Example of use: frontend/src/state/payment/hooks/use-payment.tsx

Task 2: Add the new payment method in the payment state and structure

  • Add Bizum in IPaymentMethod

Expand
titlefrontend/src/state/payment/types.ts
Code Block
languagetypescript
export interface IPaymentMethod {
  accountIdentifier: string | null;
  fdAccountId: string | null;
  paymentMethodBrand?: string | null;
  chaseProfileId: string | null;
  paypalIdentifier: string | null;
  credit: ICredit | null;
  selected?: boolean;
  prepaid: IPrepaid | null;
  // make cash required eventually
  cash?: boolean | null;
  paypal: boolean | null;
  ideal: boolean | null;
  blik?: boolean | null;
  sodexo?: boolean | null;
  chequeGourmet?: boolean | null;
  paymentOnDeliveryCard?: boolean | null;
  bizum?: boolean | null;
  transient?: boolean;
}

  • Create a new placeholder for the new payment method

    • The idea is to send this new payment method identified as cash

Expand
titlefrontend/src/state/payment/constants.ts
Code Block
languagetypescript
export const BIZUM_PAYMENT_METHOD_PLACEHOLDER: IPaymentMethod = {
  bizum: true,
  fdAccountId: 'BIZUM',
  accountIdentifier: 'BIZUM',
  paymentMethodBrand: 'BIZUM',
  chaseProfileId: null,
  credit: null,
  prepaid: null,
  paypalIdentifier: null,
  ideal: null,
  paypal: false,
  sodexo: null,
  chequeGourmet: false,
  transient: true,
};
  • Add the new payment method in getPaymentMethodsState:

Expand
titlefrontend/src/state/payment/hooks/getPaymentMethodsState/index.ts

Update the interface:

frontend/src/state/payment/hooks/types.ts:

Code Block
languagetypescript
export interface ISetupPaymentMethodState {
  enableBizum: boolean;
}

Adjust the getPaymentMethodsState:

Code Block
languagetypescript
export const getPaymentMethodsState = ({
  // rest of the code
  enableNewMethod,
}: ISetupPaymentMethodState) => {
  // rest of the code

  if (enableBizum) {
    availablePaymentMethodList.push(BIZUM_PAYMENT_METHOD_PLACEHOLDER);
  }
  // rest of the code
};

  • Adjust the payment hook

Expand
titlefrontend/src/state/payment/hooks/use-payment.tsx

Add the new feature flag here and then pass through the getPaymentMethodState:

Code Block
languagetypescript
const usePayment = ({
  // rest of the code
  const enableBizum = useFlag(LaunchDarklyFlag.ENABLE_BIZUM_PAYCOMET); // new line
    
    const initPaymentMethods = useCallback(() => {
      const {
        availablePaymentMethodList,
        validDefaultPaymentMethodId,
        validDefaultReloadPaymentMethodId,
      } = getPaymentMethodsState({
         enableBizum, // new line here
      });
    },
    []
  );
});

Adjust the unit tests mocking the new flag where necessary: frontend/src/state/payment/tests/use-payment.test.tsx

  • Adjust IPaycometState interface

Expand
titleworkspaces/frontend/src/utils/payment/index.ts

Update isPayPal to isPayLink and make a refactor code.

Code Block
languagetypescript
export interface IPaycometState extends IPaymentState {
  merchantAccount?: string;
  pspReference?: string;
  jetId?: string;
  iframeUrl?: string;
  isPayLink?: boolean;
  card?: {
    expiryMonth: string;
    expiryYear: string;
    bin: string;
    last4: string;
  };
  paymentMethodBrand?: PaymentMethodBrand;
  paytpvToken?: string;
}

  • Adjust use-order-payment hook

Expand
titlefrontend/src/pages/cart/payment/order-payment/use-order-payment.ts

Add a new state for the new payment method:

const [isPaycometBizumMethodSelected, setIsPaycometBizumMethodSelected] = useState(false);

Update the handlePaymentMethodSelected adding the new method:

Code Block
languagetypescript
  const handlePaymentMethodSelected = (newFdAccountId?: string) => {
    if (!newFdAccountId) {
      payment.setCheckoutPaymentMethodId('');
      return;
    }
    
    // rest of the code
    setIsPaycometBizumMethodSelected(false); // New line
  };

Clear our new method in the useEffect logic that already exist:

Code Block
languagetypescript
  // when checkout payment method have been updated from payment context
  // the flags to show add new credit card or add gift card
  // have to be updated.
  useEffect(() => {
      // rest of the code
      setIsPaycometBizumMethodSelected(false);
    }
  }, [setCheckoutPaymentMethodId, checkoutPaymentMethodId]);

Update the placeOrder function to deal with the new method:

  • Adjust the if condition for paycomet processor to using isPayLink

    Code Block
    languagetypescript
    } else if (payment.isPaycomet) {
            // rest of the code
            if (payCometValues.isPayLink) {
                commitInput = {
                creditType: payCometValues.cardType,
                order,
                payment: {
                  fullName: payCometValues.nameOnCard || '',
                  ccMetadata: undefined,
                  paycometInput: {
                    pspReference: payCometValues.pspReference || '',
                    storePaymentMethod: payCometValues.saveCard || false,
                    storedPaymentMethodId: payCometValues.cardNumber || undefined,
                  },
                },
                skipCoolingPeriod: true,
              };
            } else {
              ...
          }
      // rest of the code

Adjust the processOrderWithAccount function for the new method like this Sodexo example:

Code Block
languagetypescript
        // rest of the code
        const isPaymentWithVoucher =
          paymentAccount.paymentMethodBrand === PaymentMethodBrand.SODEXO_VOUCHER ||
          paymentAccount.paymentMethodBrand === PaymentMethodBrand.CHEQUE_GOURMET_VOUCHER;
        if (payment.checkoutPaymentMethodId === CASH_ACCOUNT_IDENTIFIER || isPaymentWithVoucher) {
          const paymentDetails: IPayment = {
            cashPayment: true,
            fullName,
            paymentMethodBrand: paymentAccount?.paymentMethodBrand ?? undefined,
          };

          if (payment.isVrPayment) {
            // The GraphQL endpoint for VR Payment expects this object even
            // for Cash orders, otherwise the order creation fails
            paymentDetails.vrPaymentInput = {
              merchantAccount: '',
              pspReference: '',
              storePaymentMethod: true,
            };
          }

          return commitOrder({
            ...commitInput,
            creditType: CASH_ACCOUNT_IDENTIFIER,
            payment: paymentDetails,
          });
        }

Adjust the handleOrderCommit to place the order with the new method:

Code Block
languagetypescript
      // Add new condition here as the isPaymentWithVoucher from the Sodexo example
      
      const isPaymentWithVoucher = isPaymentWithSodexoVoucher || isPaymentWithChequeGourmetVoucher;

      if (
        (showAddPaymentMethod ||
          isAdyenBlikMethodSelected ||
          reloadAddPaymentMethod ||
          isPayPalRegistration) &&
        !(selectedPaymentMethod?.cash || isPaymentWithVoucher || payment.isFreeOrderPayment)
      ) {
        await placeOrder();
      } else {
        await placeOrderWithAccount();
      }
  • Extend the getCurrentSelectedMethodType:

Expand
titlefrontend/src/components/payment-method/utils/get-current-selected-method-type.ts

Add the new payment in the enum CurrentSelectedMethodType:

  • frontend/src/components/payment-method/types.ts:

Code Block
languagetypescript
export enum CurrentSelectedMethodType {
  // rest of the code
  ADD_NEW_PAYMENT_BIZUM = 'addNewPaymentBizum',
}

Adjust the IPaymentMethodProps:

Code Block
languagetypescript
export interface IPaymentMethodProps {
  // rest of the code
  setIsPaycometBizumMethodSelected?: Dispatch<SetStateAction<boolean>>;
  isPaycometBizumMethodSelected?: boolean;
}

Add the new condition in getCurrentSelectedMethodType:

Code Block

type GetCurrentSelectedMethodTypeParams = {
  // rest of the code
  isPaycometBizumMethodSelected?: boolean;
};

function getCurrentSelectedMethodType({
  // rest of the code
  isPaycometNewMethodSelected
}: GetCurrentSelectedMethodTypeParams): CurrentSelectedMethodType {
  let currentSelectedMethodType = CurrentSelectedMethodType.ADD_NEW_PAYMENT_METHOD;
  // rest of the code
  if (isPaycometBizumMethodSelected) {
    currentSelectedMethodType = CurrentSelectedMethodType.ADD_NEW_PAYMENT_BIZUM;
  }
  // rest of the code
}

  • Adjust order-payment comp:

Expand
titlefrontend/src/pages/cart/payment/order-payment/payment.tsx

Return the new create state from useOrderPayment:

Code Block
languagetypescript
  const {
   // rest of the code
   isPaycometBizumMethodSelected, // new line here
  } = useOrderPayment({
    serverOrder,
    onPriceOrder,
    enableOrderTimedFire,
  });

Pass the returned value to the getCurrentSelectedMethodType:

Code Block
languagetypescript
  const currentSelectedMethodType = getCurrentSelectedMethodType({
    isPaycometBizumMethodSelected,
  });

Pass the new method to the <PaymentMethod> comp:

Code Block
languagetypescript
<PaymentMethod
   // rest of the code
   isPaycometBizumMethodSelected={isPaycometBizumMethodSelected}
/>

Task 3: Add the new method in payment-method structure

  • Create new styled component for the new payment method

Expand
titlefrontend/src/components/payment-method/styled.ts
Code Block
languagetypescript
// ... rest of the file
export const StyledBizumMethodCardIcon = styled(IconNewMethod)`
  ${cardIconSize}
`;
  • Add a new type

Expand
titlefrontend/src/components/payment-method/payment-method-options/types.ts
Code Block
languagetypescript
export type PaymentMethodOptionProps = {
  // ... rest of the file
  setIsPaycometBizumMethodSelected?: Dispatch<SetStateAction<boolean>>;
};
  • Add the new method in payment-method comp

Expand
titleworkspaces/frontend/src/components/payment-method/index.tsx

Add new props in PaymentMethod:

Code Block
languagetypescript

function PaymentMethod({
  // rest of the code
  isPaycometBizumMethodSelected,
  setIsPaycometBizumMethodSelected,
}: IPaymentMethodProps) {
  // rest of the code
  
  // pass the new prop in the getCurrentSelectedMethodType
  const currentSelectedMethodType = getCurrentSelectedMethodType({
    isPaycometBizumMethodSelected
  });
});

Create new item inside PaymentMethod comp:

Code Block
languagejsx
// ... rest of the file
const SelectedNewMethodOptionItem = (
  <PaymentMethodAddNewItemOption
    data-testid="payment-bizum-method-name"
    text={formatMessage({ id: 'addNewPaymentBizumMethod' })}
    Icon={<StyledNewMethodCardIcon />}
    onClick={getSelectedMethodClickHandler()}
    selected
  />
);

Add the new method in the map structure:

Code Block
  const CurrentSelectedMethodMap = {
    // ... rest of the file
    [CurrentSelectedMethodType.ADD_NEW_PAYMENT_BIZUM]: SelectedBizumOptionItem,
  };

Pass the new prop in the <PaymentMethodOptions> comp:

Code Block
languagetsx
 <PaymentMethodOptions
    setIsPaycometBizumMethodSelected={setIsPaycometBizumMethodSelected}
  />
  • Add the new method in payment-method-options comp

Expand
titlefrontend/src/components/payment-method/payment-method-options/payment-method-options.tsx

Add the new prop in PaymentMethodOptions comp:

Code Block
languagetypescript
function PaymentMethodOptions({
  // .. rest of the file
  setIsPaycometNewMethodSelected
}: PaymentMethodOptionProps) {
  const enableBizumMethod = useFlag(LaunchDarklyFlag.ENABLE_BIZUM_PAYCOMET); // new line

});

Add new condition in handleUnselectedMethodClick, handleAddNewCardOptionClick, handleAddNewGiftCardOptionClick, handlePaypalOptionClick and handleAdyenIdealOptionClick to clear our new state:

Code Block
languagetypescript
  if (setIsPaycometSodexoOptionSelected) {
    setIsPaycometBizumMethodSelected(false);
  }

Refactor openPaycometPayLinklPopup to add typePayment.

Change translate from PayPal to Bizum.

Code Block
interface IPaycometPayLinkPopupResult {
  openPaycometPayLinklPopup: (typePayment: string) => void;
  isLoadingPopup: boolean;
  isPopupOpen: boolean;
}

Add a new handle for our new method (using useCallback) where we’ll clear the other methods and set your:

Code Block
languagetypescript
const handleBizumOptionClick = useCallback(
    (e: React.FormEvent<HTMLButtonElement>) => {
      e.preventDefault();

      setIsAddNewCardOptionSelected(false);
      setIsAddNewGiftCardOptionSelected(false);

      if (setIsPaycometBizumOptionSelected) {
        setIsPaycometBizumOptionSelected(true);
      }
      if (setIsPaycometPaypalOptionSelected) {
        setIsPaycometPaypalOptionSelected(false);
      }
      if (setIsAdyenIdealOptionSelected) {
        setIsAdyenIdealOptionSelected(false);
      }
      if (setIsAdyenBlikOptionSelected) {
        setIsAdyenBlikOptionSelected(false);
      }
      if (setIsPaycometSodexoOptionSelected) {
        setIsPaycometSodexoOptionSelected('');
      }
      if (setIsPaycometChequeGourmetOptionSelected) {
        setIsPaycometChequeGourmetOptionSelected('');
      }

      openPaycometPayLinklPopup('BIZUM');
    },
    [
      setIsAddNewCardOptionSelected,
      setIsAddNewGiftCardOptionSelected,
      setIsPaycometBizumOptionSelected,
      setIsPaycometPaypalOptionSelected,
      setIsAdyenIdealOptionSelected,
      setIsAdyenBlikOptionSelected,
      setIsPaycometSodexoOptionSelected,
      setIsPaycometChequeGourmetOptionSelected,
      openPaycometPayLinklPopup,
    ]
  );

Add in the enableSortPaymentMethods && unSelectedMethods.map a new block for our new method:

  • We also need to condition this payment method to be shown

Code Block
languagetypescript
{method?.bizum && method?.transient && (
  <StyledOption>
    <PaymentMethodOptionItemDivider />
          <PaymentMethodAddNewItemOption
            data-testid="add-bizum-payment"
            text={'Bizum'}
            Icon={<StyledBizumCardIcon />} // should create this icon
            onClick={handleBizumOptionClick}
            isLoading={isLoadingPopup}
          />
  </StyledOption>
)}

Add a new block for for the case where we not have the enableSortPaymentMethods as true:

Code Block
languagetsx
{!enableSortPaymentMethods && isBizumEnabled && (
  <StyledOption>
    <PaymentMethodOptionItemDivider />
    <PaymentMethodAddNewItemOption
      data-testid="add-bizum-payment"
      text={formatMessage({ id: bizumlLabel(isGuestOrder) })}
      Icon={<StyledPaypalCardIcon />}
      onClick={handlePaypalOptionClick}
      isLoading={isLoadingPopup}
    />
  </StyledOption>
)}

Add the new payment method in the sortPaymentMethods

  • frontend/src/components/payment-method/utils/sort-payment-methods.ts

Code Block
languagetypescript
  const paymentMethodListTypeOrder = [
    // rest of the code
    'NEW_METHOD',
  ];
  
  // Add new case in switch for our new method
  function filterMethodByType(typeOrder: string, paymentMethods: IPaymentMethod[]): IPaymentMethod[] {
  switch (typeOrder) {
    // rest of the code
    case 'BIZUM':
      return paymentMethods.filter(
        p => !p?.transient && p?.bizum && p.accountIdentifier === typeOrder
      );
    // rest of the code
  }
}

Add a new logic to the filter !enableSortPaymentMethods && unSelectedMethods.filter(isNotLocalWallet)... to now show our payment method if delivery mode is not selected:

Code Block
languagetypescript
  const isNotLocalWallet = (method: IPaymentMethod) => {
    return !method.blik && !method.ideal;
  };

  const sortedMethodsFilterConditions = (method: IPaymentMethod) => {
    if (method.bizum) {
      return false; // remove our item from the list if delivery is not selected
    }

    return isNotLocalWallet(method);
  };
      
      // A suggestion would be the creation of a new function for the filter conditions
      {!enableSortPaymentMethods &&
        unSelectedMethods.filter(sortedMethodsFilterConditions).map(method => {

  • Add the new method in paycomet-hosted-page-payment comp

Expand
titlefrontend/src/pages/cart/payment/order-payment/paycomet-hosted-page-payment/paycomet-hosted-page-payment.tsx

Return the new values from the useOrderPayment hook

Code Block
languagetypescript
const {
    // rest of code
    setIsPaycometBizumMethodSelected,
    isPaycometBizumMethodSelected,
  } = useOrderPayment({
    enableOrderTimedFire,
    onPriceOrder,
    serverOrder,
  });

Pass down the values to the <PaymentMethod> comp

Code Block
languagetsx
// rest of the code
<PaymentInfoBackground $hostedPayment>
  {showPaymentMethodSelection && (
    <PaymentMethod
      // rest of the code
      setIsPaycometBizumMethodSelected={setIsPaycometBizumMethodSelected} // new line
      isPaycometBizumMethodSelected={isPaycometBizumMethodSelected} // new line
      onResult={onPaymentOutcome}
    />
  )}

Task 4: Create and add a new method in payment-method-option structure

...

  • Add the new icon for the new payment method in: frontend/src/components/icons/new-payment-method/index.tsx (replace “new-payment-method” for the new name, hahaha).

  • Add the new method option in the interface of payment methods:

Expand
titlefrontend/src/state/payment/types.ts
Code Block
languagetypescript
export interface IPaymentMethod {
  // rest of the code
  bizumPaymentMethod?: boolean | null;
}

  • Add new file in frontend/src/components/payment-method-option/as the following example:

    • Remember to add the translation that will be used in formatMessage

Expand
titlefrontend/src/components/payment-method-option/new-payment-method.tsx (example of new file)
Code Block
languagetypescript
import React from 'react';

import { useIntl } from 'react-intl';

import { MethodType } from './styled';

const NewPaymentMethod = () => { // Example SodexoMethod
  const { formatMessage } = useIntl();

  return <MethodType>{formatMessage({ id: 'payWithBizumMethod' })}</MethodType>;
};

export default NewPaymentMethod;
  • Use the created payment method option in payment-method-option:

Expand
titlefrontend/src/components/payment-method-option/index.tsx

Adjust the RenderMethodType and add the new method created above

Code Block
languagetsx

  const RenderMethodType = () => {
    // rest of the code

    // New if here
    if (method.bizumPaymentMethod) {
      return <BizumPaymentMethod />;
    }   
    
    return null;
  };
  
  // add the new payment method inside the <MethodTypeWrapper> in the return
  
   return 
    // rest of the code
    (
    // rest of the code
      <MethodTypeWrapper
        data-private
        data-dd-privacy="mask"
        onClick={onClickMethod}
        $isClickable={!!onClick}
        disableMethod={disableMethod}
        fromCheckout={fromCheckout}
        data-testid={`method-type-wrapper-${method.accountIdentifier ?? method.fdAccountId ?? ''}`}
        selected={selected}
      >
        // rest of the code
        <div>
          <MethodTypeDescription>
            // Add new method here inside the description
            {method.bizumPaymentMethod && <StyledBizumPaymentMethodCardIcon />}
          </MethodTypeDescription>
          // rest of the code
        </div>
      </MethodTypeWrapper>
     // rest of the code
  );

Task 5: Adjust account payment method lists to deal with the new method

...

Expand
titlefrontend/src/components/payment-method-flat-list-with-button/payment-method-flat-list-with-button.tsx

We’ll exclude the new payment method if it is not actually stored in the user account:

Code Block
languagetypescript
function filterOutUnsupportedPaymentMethods(paymentMethods: IPaymentMethod[]) {
  return paymentMethods.filter(
    n =>
      n.accountIdentifier !== PAYPAL_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier &&
      n.accountIdentifier !== SODEXO_VOUCHER_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier &&
      n.accountIdentifier !== BIZUM_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier &&
  );
}

Task 6: Adjust the receipt email to show the correct message for the new payment method

...

Today, when the commit order is processed, order-service triggers Braze API with the order info, that then sends the confirmation email to the customer.

We can create a validation when cardType different CASH and ccLast4 is empty.

Expand
titlepackages/emails/src/utils.ts
Code Block
languagejs
export const transformOrderProperties = ({
  rbiOrder,
  brand,
  fzStore,
  language,
}: ITransformOrderProperties): Record<string, any> => {
  ...
  const { cardType, ccLast4, paymentType, paymentMethodBrand }: Partial<IPayment> = payment || {};
  
  let last4 = ccLast4;

  if (cardType !== RBIPaymentCard.CASH && !last4?.trim().length) {
    last4 = '****';
  }
  ...
  
  return properties;
}

After we change intl-packages/email to send the necessary information to Braze API, we need to update the templates configured, which are split into templates and content blocks (reusable pieces of content). Using PLK-ES as example we can see:

Note

The example displays a LiquidJS template, which is similar but not equal to Braze templates. Braze access properties using the syntax {{event_properties.${language}}}

POC: https://github.com/rbilabs/intl-whitelabel-app/pull/3219/