Questions:
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.
Acceptance criteria
Apply promotional codes at checkout;
Use the current design of the promotional code field at checkout;
Hide the link “View saved promotional“
intl-whitelabel-app
Tasks:
1 - TASK: Create a new promo code flow for loyalty
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:
We need to apply (dispatch) the personalised offer on some contexts:
actions.loyalty.setSelectedOffer(personalizedOffer)
actions.loyalty.setAppliedOffers
actions.loyalty.applyOffer
actions.loyalty.setCmsOffers
OBS.: It’s possible we need to apply the offer in another context too.
When we click on remove in offer info (after applying the offer), we need to remove the offer in all contexts.
When the customers reload the page, the offer must continue applied.
Verify the need for other validations
With this update, the frontend is prepared to apply the promo code to loyalty.
2 - TASK: Updated action Remove button
3 - TASK: Hide CBA Options
4 - 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.
5 - TASK: Clean the applied offer when finished the order
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:
How is the calculation done?
So basically, we need to send (via slack) to Winrest the offers sanityId.
They register these sanityId on their system and after that, always we use some offers with these sanityId registered, Winrest will return the calculation offers correctly.
Example:
To use the offer Test: Test Discount Offer (Sanity DEV)
Solution proposal
Currently, we already sent the sanityId offer to the backend (intl-partners-service
);
Then, we need to send the discount information to Winrest;
We will add a new attribute orderDiscounts
on the webhook payload;
The orderDiscounts
object would be in each cart object;
If the discount is amount
type, we will transform it to percentage
type;
Tasks:
1 - TASK: Create a method to calculate the percentage offer;
{
"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"
}
2 - 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;
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)
3 - TASK: Add the discount on the order;
4 - TASK: Send the new discount object to Winrest
We will need to send the new discount attribute to Winrest by webhook:
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:
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;
const verifyDiscountTypes = (cartEntry: ICartEntry): boolean => {
return (
cartEntry.type !== CartEntryType.offerDiscount &&
cartEntry.type !== CartEntryType.offerCombo &&
cartEntry.type !== CartEntryType.offerItem
);
};
Screenshots
POC
Impact Analysis
Dependencies
Unit Test
Useful Links
Add Comment