Versions Compared

Key

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

Questions:

  •  N/D

Technical Refinement

...

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

Acceptance criteria

  • Apply promotional codes at checkout;

    • Same as when the promo code is applied on the offers page;

  • Use the current design of the promotional code field at checkout;

  • Hide the link “View saved promotional“

intl-whitelabel-app

Tasks:

Expand
title1 - TASK: Create a new promo code flow

...

for loyalty
  • Create a feature flag;

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

    • On the 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

...

Example code used in this hook to validate the promo code on backend
Expand
title
      • -promo-codes.ts

Code Block
languagetypescript
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

...

context too.

  1. When we click on remove in offer info (after

...

  1. applying the offer), we need to remove the offer

...

  1. in all contexts.

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

  3. Verify the need for other validations

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

Image Modified
Expand
title2 - 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

      Code Block
      languagetypescript
      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

      Code Block
      languagetypescript
      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

Expand
title3 - TASK: Hide CBA Options
  • We will need to hide the CBA option to ensure that anything CBA option shows.

    Image Modified
    • Then, we will create a new attribute to hide this option;

      • This attribute value can be the opposite of the value of feature flag (flag created on task 1)

        Image Modified
Expand
title4 - TASK: Apply discount again if not used
  • Currently, we don’t have the validated voucher flow on whitelabel-app and voucherify, Just the burn voucher flow when we applied the promo code.

  • When we applied the promo code, the offer get saved on the offers page.

    • So, We need to validate the offers saved and compare the promo code added on the field to offers saved before validating the promo code on voucherify.

      • If we have a promo code saved, we will apply the offer without validating voucherify.

      • If we don’t have one, we will validate the promo code with voucherify;

Expand
title5 - TASK: Clean the applied offer when finished the order
  • When finish the order, we need to clean the offers on cookies, sessions, etc.

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

...

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

...

intl-partners-service (backend)

After applying the Whitelabel will do a call to the backend to calculate the discount value:

...

  • The orderDiscounts object would be in each cart object;

  • If the discount is amount type, we will transform it to percentage type;

    • E.g: If the total cart is 10 euros and amount discount is 5 euros;

      • We will calculate the discount:

        Code Block
        discount = (100 * 5) / 100;
        discount = 50%;

Tasks:

expand
Expand
title1 - TASK: Create a method to calculate the percentage offer;
  • Create a feature flag;

  • Create a method to calculate the percentage offer.

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

    • We will send the discount values per cart item to Winrest, but the calculation of the discount percentage will be based on the cart total, which will be easier to calculate and in the end will have the same result if we calculate the discount per item.

    • The method will receive two params:

      • appliedOffers: IAppliedOffer[] | null | undefined

        • Offer applied on the cart;

          Image Modified
      • total: number | undefined

        • total cart value;

    • The method return will be:

      • Promise<IOrderDiscounts[] | undefined>

        • Where the IOrderDiscounts.type will be always a percentage;

    • To discover the discount values we will need to use the method getOffers

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

      • This method is private, so we will update it to public;

    • If the discount offer is a type amount we will calculate the percentage over the total cart value;

      • E.g: If the total cart is 10 euros and amount discount is 5 euros;

        • We will calculate the discount:

          Code Block
          discount = Math.round((5 * 100) / 10)
          discount = 50%; 
      • So the payload will be:

        Code Block
        languagejson
        "orderDiscounts": [
          {
            "type": "percentage",
            "value": 50
          }
        ]
      • payload

titlePayload JSON
Code Block
languagejson
{
  "callbackUrl": "https://euw3-dev-plk-partners-api.rbictg.com/api/v1/orders/84b5f1df-28df-4e6f-a871-a2783602436f/price/callback",
  "cart": {
    "menuSelections": [
      {
        "externalReferenceId": "800853",
        "id": "f9f5b02e-bee8-4dd1-a5fa-222b79516a98",
        "menuSelections": [
          {
            "externalReferenceId": "800852",
            "id": "item_67167",
            "menuSelections": [],
            "price": {
              "amount": 679,
              "currency": "EUR"
            },
            "quantity": 1,
            "orderDiscounts": [
              {
                "type": "percentage",
                "value": 50
              }
            ]
          },
          {
            "externalReferenceId": "940151",
            "id": "item_50929",
            "menuSelections": [],
            "price": {
              "amount": 0,
              "currency": "EUR"
            },
            "quantity": 1,
            "orderDiscounts": [
              {
                "type": "percentage",
                "value": 50
              }
            ]
          },
          {
            "externalReferenceId": "940180",
            "id": "item_66018",
            "menuSelections": [],
            "price": {
              "amount": 1500,
              "currency": "EUR"
            },
            "quantity": 1,
            "orderDiscounts": [
              {
                "type": "percentage",
                "value": 50
              }
            ]
          },
          {
            "externalReferenceId": "940325",
            "id": "45adf5bb-2f3d-4c40-a735-cf59d5a661ef",
            "menuSelections": [],
            "price": {
              "amount": 0,
              "currency": "EUR"
            },
            "quantity": 1,
            "orderDiscounts": [
              {
                "type": "percentage",
                "value": 50
              }
            ]
          }
        ],
        "price": {
          "amount": 1100,
          "currency": "EUR"
        },
        "quantity": 1,
        "orderDiscounts": [
          {
            "type": "percentage",
            "value": 50
          }
        ]
      }
    ]
  },
  "channel": "WHITELABEL_IN_STORE",
  "fees": [],
  "id": "84b5f1df-28df-4e6f-a871-a2783602436f",
  "instructions": [],
  "serviceMode": "TAKEOUT",
  "storeId": "1111"
}
Expand
title2 - TASK: Update the calculated total card;
  • Currently, the total cart is calculated the all item values including the offers configs, so we need to remove the discount offers config this calc;

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

    • method: calculate(cart: ICart)

  • We will refactor this method abstracting the logic into two parts because we will need just the part of the method code to this flow. But we can’t change the calculate(cart: ICart) because it is used in other flows. Then, we will create a new method.

  • As the POC method public calculateTotalCart(cart: ICart)

Expand
title3 - TASK: Add the discount on the order;
  • We will use all methods created above (mapOffersDiscounts and calculateTotalCart):

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

  • On method public async price(input: IPriceOrderRequest):

    • We will import the attribute appliedOffers on object input.cart;

    • We will protect the code with feature flag here;

    • We will calculate the total cart without possible discounts (calculateTotalCart) and use the method mapOffersDiscounts to create the object with percentage discount:

      Code Block
      languagetypescript
      let discountsOffer;
      if (FEATURE_FLAG && appliedOffers?.length) {
        const totalCart = this.cartTotalService.calculateTotalCart(input.cart);
        discountsOffer = await this.mapOffersDiscounts(appliedOffers, totalCart);
      }
      
    • We will add the variable orderDiscounts on attribute orderDiscounts from newOrder object;

      Image Modified
Expand
title4 - TASK: Send the new discount object to Winrest
  • We will need to send the new discount attribute to Winrest by webhook:

    Image Modified
  • Then, we will add the new attribute on mapper mapRbiCart;

  • So we will add the variable discountsOffer to mapRbiCart: mapRbiCart(pricingCart, discountsOffer);

    • In this method, we will add a new condition to send the discount values:

      Image Modified
      Code Block
      languagetypescript
      if (FEATURE_FLAG && orderDiscounts?.length && verifyDiscountTypes(cartEntry)) {
        result.orderDiscounts = orderDiscounts;
      }
    • Where, the verifyDiscountTypes method verify item type of the cart that there will be a discount because the types (CartEntryType.offerDiscount, CartEntryType.offerCombo, CartEntryType.offerItem) are already the offer of an item;

      Code Block
      languagejson
      const verifyDiscountTypes = (cartEntry: ICartEntry): boolean => {
        return (
          cartEntry.type !== CartEntryType.offerDiscount &&
          cartEntry.type !== CartEntryType.offerCombo &&
          cartEntry.type !== CartEntryType.offerItem
        );
      };

Screenshots

  • N/A

POC

...