Versions Compared

Key

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

Table of Contents

...

More details here: Promo code at checkout page X config offer type (WIP)

...

PoC working example

  • Important: Please disregard the delay when adding the offer. This is a PoC example. This needs to be improved in the final solution.

...

  • Main file to be changed: src/state/loyalty/hooks/use-redeem-promo-code-checkout.ts

  • PoC branch: to be added

Task 1 - Create a new method responsible to add other types of offer

  • Get the whole Picker/Combo from Config Offers incentives

  • Use ProductWizardUtils.ComputeSelectedOption to transform the itens correctly. This is important for Picker. The method will convert the Picker to a regular item

  • Use the ProductWizardUtils.SetDefaultSelections to get the default selections

  • Use the transformMenuObjectToCartItem to convert the Combo/Picker (the data that will come from Option1 or Option 2)

  • Update the Redux state using the dispatch and the actions.loyalty.applyOffer

  • Use the totalPrice method (src/hooks/menu/use-product-price.ts) to calculate the correct price ----> We have had a problem here and we need to adjust the price calculation for pickerI was able to fix the code (suggestion on the PoC code example below). The value always needs to be correct (see PoC code example for more details and tips)

    • If we have a vendor config the value should be from there

    • If we don’t have a vendor config the value should be the offer price (offer config content)

    • If we don’t have any value at all then we need to look at the vendor config for the item

  • Adjust the loading state to only finish when we in fact add the new item into the cart

  • Use the upsertCart to add the transformed item to the cart

...

PoC code example and mentions

  • The code below should not be the final solution

  • The objective of the PoC is to prove an idea. I didn't make the code semantic or beautiful. This will be a responsibility for the dev

  • We should create new methods and reuse the code at maximum (see Suggestion of implementation below)

PoC code is inside this collapse:

Expand
titlePoC code example
  • use-redeem-promo-code-checkout.ts path of solution changes:

Code Block
languagetypescript
  // this is a method from /src/state/menu-options/legacy-menu-options.tsx
  const getItemIdAndType = useCallback((id: string) => {
    const [type, ...idSplit] = id.split('-');
    const itemID = idSplit.join('-');
    return { type, itemID };
  }, []);

  // this is a method from /src/state/menu-options/legacy-menu-options.tsx
  const queryMenuData = useCallback(
    async (
      menuItemId: string,
      menuItemType: string,
      dataQuery: DocumentNode,
      availabilityQuery: DocumentNode
    ) => {
      const { type } = getItemIdAndType(`${menuItemType}-` + menuItemId);

      const result = await Promise.all([
        client.query({
          fetchPolicy: 'no-cache',
          context: { uri },
          query: dataQuery,
          variables: {
            id: menuItemId,
          },
        }),
        client.query({
          fetchPolicy: 'no-cache',
          context: { uri },
          query: availabilityQuery,
          variables: {
            id: menuItemId,
          },
        }),
      ]);

      const { data: rawData, loading } = result.reduce(
        (finalResponse, res) => {
          const capitalizedType = type[0].toUpperCase() + type.slice(1);
          const data = res?.data;
          return {
            data: data?.[capitalizedType]
              ? [...finalResponse.data, data[capitalizedType]]
              : finalResponse.data,
            loading: res.loading || finalResponse.loading,
          };
        },
        { data: [], loading: false }
      );

      const [uiData, availability] = rawData;
      const eitinha = merge(uiData, availability);

      return eitinha;
    },
    [client, getItemIdAndType, uri]
  );

  // this method was adjusted
  const applyOfferAtCheckout = useCallback(
    async (offer: LoyaltyOffer, standardOffersLimit: number) => {
      try {
        if (appliedOffers?.length >= standardOffersLimit) {
          removeFromCart({ cartId: appliedOffers[0].cartId });
        }

        if (isDiscountLoyaltyOffer(offer)) {
          dispatch(actions.loyalty.setCmsOffers([offer]));
          dispatch(actions.loyalty.setSelectedOffer(offer));
          dispatch(actions.loyalty.setPersonalizedOffers([offer]));
          dispatch(actions.loyalty.unshiftPersonalizedOffer(offer));
          dispatch(actions.loyalty.setAppliedOffers([offer]));

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

        if (!isDiscountLoyaltyOffer(offer) && offer?.incentives?.length && offer?.incentives[0]) {
          // Dispatching to select the offer. The legacy flow only does this. The other dispatches
          // aren't necessary.
          dispatch(actions.loyalty.setSelectedOffer(offer));
          
          // getting only the first incentive item for PoC purposes. This needs to be fixed at task 3
          const offerItemIncentive = offer.incentives[0];

          const offerItemType = offerItemIncentive._type;
          
          // Swap items don't have an id. We need to ensure this to not get a Typescript error
          const offerItemId =
            offerItemIncentive.__typename === 'Combo' || offerItemIncentive.__typename === 'Picker'
              ? offerItemIncentive._id
              : '';

          // We need to check what document we'll need to use (Combo/Picker)
          const itemQueryDoc =
            offerItemIncentive.__typename === 'Combo' ? GetComboDocument : GetPickerDocument;
          const itemAvailabilityQueryDoc =
            offerItemIncentive.__typename === 'Combo'
              ? GetComboAvailabilityDocument
              : GetPickerAvailabilityDocument;

          // Get all the needed menu data
          const offerItemData = await queryMenuData(
            offerItemId,
            offerItemType,
            itemQueryDoc,
            itemAvailabilityQueryDoc
          );

          // Compute the selected option as the menu page does. This is important because 
          // for Picker items the item will be converted to Item type          
          const computedOfferItem = ProductWizardUtils.ComputeSelectedOption(offerItemData);

          // Set default selections as the menu page does. This is important. Without this
          // the sub-items will not be shown
          const defaultSelections = ProductWizardUtils.SetDefaultSelections({
            menuObject: computedOfferItem,
          });

          // We need to convert the menu object from the query to a cart item valid structure. This
          // is important. We can't use a Sanity menu object directly
          const { mainEntry, upsellEntries, modifiersInjected } = transformMenuObjectToCartItem(
            computedOfferItem,
            priceForItemOptionModifier,
            pricingFunction,
            priceForComboSlotSelection,
            {
              quantity: 1, // The offer should be 1:1 always. This could will be iterable (Task 3)
              modifierSelections:
                transformUserSelectionsModifierToModifierSelection(defaultSelections),
              pickerSelections: defaultSelections.pickerAspects,
              comboSlotSelections: transformUserSelectionsComboToComboSlotSelections(
                defaultSelections.comboSlot,
                computedOfferItem as ICombo
              ),
              location,
              existingCartId: editingCartEntry?.cartId,
            }
          );

          // We need to pass new params to totalPrice method to avoid problems with the price calculator
          // All price-related problems will have relation with this method
          const isOfferBenefit = incentiveIsOfferBenefit(offer);
          const offerPrice = totalPrice(modifiersInjected, computedOfferItem, isOfferBenefit, offer);

          // Dispatching to apply the offer
          dispatch(
            actions.loyalty.applyOffer({
              id: offer.loyaltyEngineId,
              type: (offer as LoyaltyAppliedOffer).type,
              cartId: mainEntry?.cartId,
              isStackable: offer.isStackable,
              isSurprise: isSurpriseOffer(offer),
              cmsId: offer._id,
            })
          );
          dispatch(actions.loyalty.setSelectedOffer(null));

          // Adds the item on the cart
          upsertCart([
            {
              ...mainEntry,
              price: offerPrice,
              offerVendorConfigs: offer.vendorConfigs,
              ...(mainEntry && { type: mapEntryTypeToOfferType(mainEntry.type) }),
            },
            ...upsellEntries,
          ]);
        }

        queryLoyaltyUserStandardOffers({ loyaltyId: user?.loyaltyId || '' });

        refreshLoyaltyUserStandardOffers();
      } catch (e: any) {
        trackEvent(createRbiApplyPromoCodeEvent(offer.loyaltyEngineId!, 'Failed', e.message));
      }
    },
    [
      appliedOffers,
      dispatch,
      editingCartEntry?.cartId,
      location,
      priceForComboSlotSelection,
      priceForItemOptionModifier,
      pricingFunction,
      queryLoyaltyUserStandardOffers,
      queryMenuData,
      refreshLoyaltyUserStandardOffers,
      removeFromCart,
      totalPrice,
      trackEvent,
      upsertCart,
      user?.loyaltyId,
    ]
  );

Tips to solve the problem related to the price calculation (only Picker has problem with the PoC code):

Option 1:

Changes to totalPrice method to calculate correctly the price (suggestion to try)

  • PS: this problem is not 100% solved but this can be a path to solution

  • File: /src/hooks/menu/use-product-price.ts

  • Extend this method to receive two new optional props (suggestion. Feel free to name as you want):

    • productToCalcselectedProduct: an specific product to be calculated (without the relationship with the select product from menu as we are not in the menu)

    • isOfferBenefit: here we can pass directly to the method if our implementation is an offer or not. The legacy code for this is also tied to the menu flow (as we are not in the menu)

    • offerselectedOffer: from what I detected the Redux is not getting the correct status for the selector. One option is to pass directly through the method the desired offer to be calculated. This appears to solve the problem

    • As the props are optional this will not cause any problem to the legacy code

Option 2:

To avoid the “Redux timming” we can separate better our methods and only use the upsertCart if we get with success the value from selectors.loyalty.selectSelectedOffer. As this state from the Redux is the same used at the totalPrice method perhaps this will allow the correct calculation without the need to change the totalPrice implementation.

...

  • Sometimes, when we enter the cart page for the first time, we can get an error about invalid items in the cart. As we have our useEffect running all the time, I think that this is part of the problem. We need to ensure that nothing will be running at the wrong time

  • When we add a Picker item, the price can be wrong (without considering the offer vendorConfig information). The best way to know if the offer item was added correctly is to compare what happens if we add the same offer through the legacy flow (/offers page). If something on the item (price, sub-items, etc) is wrong, then we have a problem!

...