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

...

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):

    • selectedProduct: 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)

    • selectedOffer: 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!

...