Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 25 Next »

Questions:

  •  N/D

Technical Refinement

Goal

The main objective is to allow the customers to use promo codes at checkout to receive benefits coming from offers.

Front-end refinement

From what we understand the current feature at checkout allow promo code just to type CBA offers and this feature is deprecated. More infos: https://rbidigital.slack.com/archives/C04FZ5HTH35/p1693855082030399.

As CBA feature is deprecated we need to change the promo code field to accept loyalty promo codes. (here more details on how to configure the promo code on sanity and voucherify: /wiki/spaces/CG/pages/984973325- OBS.: The documentation explain how configure promo codes to CBA offers and Loyalty Offers, So follow just the loyalty steps.)

After the config, we will have the config offer like this:

OBS.: On In-App Benefits there is just one item. If there are more itens, the discount will be the first item.

intl-whitelabel-app

1 - TASK: Create new promo code flow to loyalty

  • The first step to use the loyalty promo code at checkout will be to change the validation promo code flow.

    • On promo code component field, we will need to add a feature flag:

      • When this flag is enabled: The promo code flow will be LOYALTY OFFER

      • When this flag is disabled: The promo code flow will be CBA OFFER (current flow)

  • We will create a new hook to contain all the rules of this flow

    • We can use the same hook used to offer page flow.

      • intl-whitelabel-app/workspaces/frontend/src/state/loyalty/hooks/use-redeem-promo-codes.ts

 Example code used in this hook to validate the promo code on backend
const onSubmitLoyaltyPromoCode = useCallback(async () => {
    setPromoCodeValidationLoading(true);

    // const loyaltyId = 'ec5cec01-b41b-509b-9111-310ab5a18154';

    let event = createRbiApplyPromoCodeEvent(promoCodeInput, 'Successful');
    const personalizedOffer = await redeemMutation(user?.loyaltyId || '', promoCodeInput)
      .catch((e: PromoCodeError) => {
        const reason = buildErrorMessageFromPromoCodeError(e);
        setPromoCodeErrorMessageId(
          (reason as TPromoCodeErrorMessageIds) || PromoCodeErrorMessageIds.default
        );

        logger.error(`Error validating promo code ${e}`);
        event = createRbiApplyPromoCodeEvent(promoCodeInput, 'Failed', e.message);
      })
      .finally(() => {
        setPromoCodeValidationLoading(false);
      });

    trackEvent(event);

    if (personalizedOffer) {
      await handleRedemption(personalizedOffer);

      // clear promo code input & error message
      setPromoCodeErrorMessageId(null);
      setPromoCodeInput('');

      toast.success(formatMessage({ id: 'offerAddedToCart' }));
    }
  }, [
    formatMessage,
    handleRedemption,
    promoCodeInput,
    redeemMutation,
    trackEvent,
    user?.loyaltyId,
  ]);
  
  
   useEffect(() => {
    if (appliedOfferPromoCode?.loyaltyEngineId) {
      const standardOffersLimit =
        earningCalculationData?.EarningCalculation?.offerRedemptionLimits?.standardOffersLimit || 1;

      dispatch(actions.loyalty.setSelectedOffer(appliedOfferPromoCode));
      dispatch(actions.loyalty.setAppliedOffers([appliedOfferPromoCode]));

      if (isDiscountLoyaltyOffer(appliedOfferPromoCode)) {
        // If limit of offers reached remove the first one
        if (appliedOffers?.length >= standardOffersLimit) {
          removeFromCart({ cartId: appliedOffers[0].cartId });
        }

        //Discount offers should not show menu item details
        dispatch(
          actions.loyalty.applyOffer({
            id: appliedOfferPromoCode.loyaltyEngineId,
            type: OfferType.GLOBAL,
            isStackable: appliedOfferPromoCode.isStackable,
            isSurprise: isSurpriseOffer(appliedOfferPromoCode),
            cmsId: appliedOfferPromoCode._id,
            cartId: 'discount-offer',
          })
        );
      }

      dispatch(actions.loyalty.setCmsOffers([appliedOfferPromoCode]));
      return;
    }

    dispatch(actions.loyalty.setSelectedOffer(null));
    dispatch(actions.loyalty.setAppliedOffers([]));
    dispatch(actions.loyalty.setCmsOffers([]));

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [appliedOfferPromoCode]);

Attention points:

  1. We need to apply (dispatch) the personalised offer on some contexts:

    1. actions.loyalty.setSelectedOffer(personalizedOffer)

    2. actions.loyalty.setAppliedOffers

    3. actions.loyalty.applyOffer

    4. actions.loyalty.setCmsOffers

OBS.: It’s possible we need to apply the offer in another contexts too.

  1. When we click on remove in offer info (after applied the offer), we need to remove the offer on all contexts.

  2. When the customers reload the page, the offer must continue applied.

  3. Verify need for other validations

With this update, the frontend is prepared to apply the promo code to loyalty.

3 - TASK: Updated action Remove button

  • We will need to update the button action to remove the applied offer on loyalty.offers.cmsOffers

    • path: intl-whitelabel-app/workspaces/frontend/src/state/global-state/models/loyalty/offers/offers.slice.ts

      removeCmsOffers: (state, { payload }: PayloadAction<LoyaltyOffer>) => {
        state.cmsOffers = state.cmsOffers.filter(offer => offer._id !== payload._id);
      },
    • path: intl-whitelabel-app/workspaces/frontend/src/pages/cart/your-cart/cart-offer.tsx

      const onRemove = () => {
        ...
        dispatch(actions.loyalty.removeCmsOffers(selectedLoyaltyOffer));
      };
      • OBS.: Change also:

        • path: intl-whitelabel-app/workspaces/frontend/src/state/global-state/models/loyalty/loyalty.actions.ts

2 - TASK: Clean the applied offer when finished the order

  • When finished the order, we need to clean the offers on cookies, sessions and etc.

    • path: intl-whitelabel-app/workspaces/frontend/src/pages/order-confirmation/order-confirmation.tsx

 Example code
useEffect(() => {
  if (!loading && !orderErrors) {
    dispatch(actions.loyalty.setSelectedOffer(null));
    dispatch(actions.loyalty.setAppliedOffers([]));
    dispatch(actions.loyalty.setCmsOffers([]));
  }
}, [dispatch, loading, orderErrors]);

Back-end

After applied the whitelabel will do a call to backend to calculate the discount value:

  • First, it will call the PriceOrder mutation on intl-gateway or intl-graphql

  • This mutation will call the backend: intl-partners-service repository.

    • on this repository, will call:

      • public async price(@Body() payload: PriceOrderRequestDto): Promise<OrderResponseDto>

        • path: intl-partners-service/src/modules/orders/orders.controller.ts

      • after: public async price(input: IPriceOrderRequest): Promise<IOrder>

        • path: intl-partners-service/src/modules/orders/orders.service.ts

        • on this file is where calculate the order and is generated the rbiOrderId.

Currently for some reason, the calc is wrong in 2 aspects:

  • Setting up wrong offers;

  • Missing configOffer calc;

intl-partners-service

  • When the frontend call the backend, the first method ran is:

    • public async price(@Body() payload: PriceOrderRequestDto): Promise<OrderResponseDto>

      • path: intl-partners-service/src/modules/orders/orders.controller.ts

    • after: public async price(input: IPriceOrderRequest): Promise<IOrder>

      • path: intl-partners-service/src/modules/orders/orders.service.ts

      • on this file is where calculate the order and is generated the rbiOrderId.

  • On price(input: IPriceOrderRequest) on file orders.service.ts, there is a call method named this.cartTotalService.calculate(input.cart).

    • Inside this method is calc the some type offers, for example System Wide offers, but there isn’t the calc relative Offer configs.

      • The method that calc relative the system wide offer is getSystemWideOfferIncentiveDiscountAndType(offerId: string): Promise<IRewardIncentive | undefined>

        • path: @intl-sanity/loyalty/index.ts

 getSystemWideOfferIncentiveDiscountAndType
getSystemWideOfferIncentiveDiscountAndType(offerId) {
    return this.reader.fetch(`
  *[_type == "systemwideOffer"
    && _id == $offerId
    && defined(incentives[_type == 'offerDiscount'][0])][0] {
      incentives[0]-> {
        discountType,
        discountValue
      }
    }
  `, { offerId });
}

Possible solution configOffer calc:

  • We will need to create a new method to get the config offers on intl-sanity repository:

    • path: intl-sanity/loyalty/index.ts

 getLoyatyConfigOfferType
getLoyatyConfigOfferType(offerId) {
  return this.reader.fetch(`
  *[_type == "configOffer"
    && _id == $offerId
    && defined(incentives[_type == 'offerDiscount'][0])][0] {
      incentives[0]-> {
        discountType,
        discountValue
      }
    }
  `, { offerId });
}
  • After it, we will need to update the intl-sanity lib on intl-partners-service to have access at this new query.

  • In the next, we can do the new method to calc this type offer that can be after getSystemWideOfferIncentiveDiscountAndType method:

    • path: intl-partners-service/src/modules/orders/cart-total.service.ts

 Currently code
 /**
   * Calculates cart total.
   *
   * @param {ICart} cart Contents of the cart.
   * @returns {Promise<number>} A cart total.
   */
  public async calculate(cart: ICart): Promise<number> {
    // our base will be a simple sum of prices of all items
    let total = cart.cartEntries.reduce((sum: number, current: ICartEntry) => {
      if (current.price) {
        // even though we have price for entire item already we need to look for premium items which are not included
        const premiumItemsPrice = this.pricePremiumItems(current);
        return sum + current.price + premiumItemsPrice;
      }
      return sum + this.priceCartItem(current);
    }, 0);

    this.logger.debug({ total }, `Base total calculated`);

    // apply discount (we assume that there cannot be more than one)
    const discountEntry = cart.cartEntries.find(
      (entry: ICartEntry) => entry.type === CartEntryType.offerDiscount,
    );

    if (discountEntry) {
      const { sanityId } = discountEntry;

      const offer = await this.sanityLoyalty.getSystemWideOfferIncentiveDiscountAndType(sanityId);

      if (offer) {
        const { discountValue } = offer.incentives;
        this.logger.debug({ discountValue, sanityId }, 'Applying discount (system wide offer)');
        total = total - Math.round((total * discountValue) / 100);
      } else {
        this.logger.warn({ sanityId }, 'Offer not found in sanity');
      }
    }

    this.logger.info({ total }, 'Cart total calculated');

    return total;
  }
 Possible calc code (POC - will need to refactor)
/**
   * Calculates cart total.
   *
   * @param {ICart} cart Contents of the cart.
   * @returns {Promise<number>} A cart total.
   */
  public async calculate(cart: ICart): Promise<ICart> {
    // apply discount (we assume that there cannot be more than one)
    const discountEntry = cart.cartEntries.find(
      (entry: ICartEntry) => entry.type === CartEntryType.offerDiscount,
    );
    
    const cartEntriesWithDiscount = cart.cartEntries.filter(
      (entry: ICartEntry) => entry.type !== CartEntryType.offerDiscount,
    );

    let offerSystemWide = {} as IRewardIncentive | undefined;
    let configOffer = {} as ILoyatyConfigOffer | undefined;

    if (discountEntry) {
      const { sanityId } = discountEntry;
      offerSystemWide = await this.sanityLoyalty.getSystemWideOfferIncentiveDiscountAndType(
        sanityId,
      );
      configOffer = await this.sanityLoyalty.getLoyatyConfigOfferType(sanityId);
    }

    const cartDiscounted = cart.cartEntries.map((cartEntrie: ICartEntry) => {
      if (cartEntrie.type === CartEntryType.offerDiscount) {
        return cartEntrie;
      }

      const discountPromo = offerSystemWide?.incentives || configOffer?.incentives;
      if (cartEntrie.price && discountPromo) {
        const { discountValue, discountType } = discountPromo;
        return {
          ...cartEntrie,
          price: this.calDiscount({
            description: offerSystemWide?.incentives
              ? '(system wide offer)'
              : configOffer?.incentives
              ? '(config offer)'
              : '',
            discountType,
            discountValue,
            length: cartEntriesWithDiscount.length,
            value: cartEntrie.price,
          }),
        };
      }
      return cartEntrie;
    });

    cart.cartEntries = cartDiscounted;

    // our base will be a simple sum of prices of all items
    const total = cart.cartEntries.reduce((sum: number, current: ICartEntry) => {
      if (current.type === CartEntryType.offerDiscount) {
        return sum;
      }

      if (current.price) {
        // even though we have price for entire item already we need to look for premium items which are not included
        const premiumItemsPrice = this.pricePremiumItems(current);
        return sum + current.price + premiumItemsPrice;
      }
      return sum + this.priceCartItem(current);
    }, 0);

    const cartUpdated = {
      ...cart,
      subTotalCents: total,
      totalCents: total,
    };

    return cartUpdated;
  }
  
  
  
  
private calDiscount({
    discountValue,
    discountType,
    value,
    length,
    description,
  }: {
    discountValue: number;
    discountType: string;
    value: number;
    length: number;
    description?: string;
  }): number {
    switch (discountType) {
      case 'amount':
        value = value - Math.round((discountValue * 100) / length);
        break;
      default:
        value = value - Math.round((value * discountValue) / 100);
        break;
    }
    this.logger.debug({ discountType, discountValue }, `Applied discount ${description}`);
    return value;
  }

Remembering that all these problems can occur because there is an error in the offer settings somewhere.

Screenshots

  • N/A

POC

Impact Analysis

  • N/A

Dependencies

  • N/A

Unit Test

  • N/A

  • N/A

  • No labels

0 Comments

You are not logged in. Any changes you make will be marked as anonymous. You may want to Log In if you already have an account.