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;
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:
...
title | 1 - 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.
We will create a new hook to contain all the rules of this flow
...
...
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.
How to create a config offer and vinculate to a Voucherify voucher
...
intl-whitelabel-app
Acceptance criteria
We can’t impact the other markets
Apply promotional codes at checkout
Use the current design of the promotional code field at checkout
If the customer has a promo code saved on the offers page, we don’t validate the promo code on voucherify
When the checkout page is refreshed, the promo code should continue applied
The “apply promotional codes” would follow the sanity rules
When the customer uses the promo code, would hides from the offers page
This feature just works if the feature flag is enabled
Would be possible to apply the promo code on the offers page
If the promotional code is removed, must refresh page to request price order again.
Tasks:
Expand |
---|
title | 1 - TASK: Apply a promo code flow for loyalty |
---|
|
Code Block |
---|
| 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) => {
if (appliedOfferPromoCode?.loyaltyEngineId) { const reason = buildErrorMessageFromPromoCodeError(e)
constsetPromoCodeErrorMessageId(
standardOffersLimit = earningCalculationData?.EarningCalculation?.offerRedemptionLimits?.standardOffersLimit(reason as TPromoCodeErrorMessageIds) || PromoCodeErrorMessageIds.default
1; dispatch(actions.loyalty.setSelectedOffer(appliedOfferPromoCode));
dispatch(actions.loyalty.setAppliedOffers([appliedOfferPromoCode])); logger.error(`Error validating promo if (isDiscountLoyaltyOffer(appliedOfferPromoCode)code ${e}`)
{ event // If limit of offers reached remove the first one
= createRbiApplyPromoCodeEvent(promoCodeInput, 'Failed', e.message)
})
if (appliedOffers?.length >= standardOffersLimit) {.finally(() => {
setPromoCodeValidationLoading(false)
removeFromCart({ cartId: appliedOffers[0].cartId });
trackEvent(event)
} if (personalizedOffer) {
//Discount offers should not show menu item detailsawait handleRedemption(personalizedOffer)
// clear promo code input & dispatch(error message
setPromoCodeErrorMessageId(null)
actions.loyalty.applyOffer({ setPromoCodeInput('')
toast.success(formatMessage({ id: appliedOfferPromoCode.loyaltyEngineId,'offerAddedToCart' }))
}
}, [
type: OfferType.GLOBALformatMessage,
handleRedemption,
promoCodeInput,
isStackable: appliedOfferPromoCode.isStackable redeemMutation,
trackEvent,
user?.loyaltyId,
])
isSurprise:
isSurpriseOffer(appliedOfferPromoCode),
useEffect(() => {
cmsId:if (appliedOfferPromoCode?._id,loyaltyEngineId) {
const standardOffersLimit =
cartId: 'discount-offer', earningCalculationData?.EarningCalculation?.offerRedemptionLimits?.standardOffersLimit || 1
}) dispatch(actions.loyalty.setSelectedOffer(appliedOfferPromoCode))
); dispatch(actions.loyalty.setAppliedOffers([appliedOfferPromoCode]))
} if dispatch(actions.loyalty.setCmsOffers([appliedOfferPromoCode](isDiscountLoyaltyOffer(appliedOfferPromoCode)); {
return; // If }limit of offers reached remove dispatch(actions.loyalty.setSelectedOffer(null));
the first one
dispatch(actions.loyalty.setAppliedOffers([])); if dispatch(actions.loyalty.setCmsOffers([]));appliedOffers?.length >= standardOffersLimit) {
// 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. Image RemovedWhen 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. Image Removed |
Expand |
---|
title | 2 - 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 |
---|
| 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 |
---|
|
|
const onRemove = () => {
...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. |
removeCmsOffersselectedLoyaltyOffer;
};
Expand |
---|
title | 3 - TASK: Hide CBA Options |
---|
|
|
Expand |
---|
title | 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.
|
Expand |
---|
title | 5 - TASK: Clean the applied offer when finished the order |
---|
|
When finish the order, we need to clean the offers on cookies, sessions, etc.
dispatch(actions.loyalty.setAppliedOffers([]))
dispatch(actions.loyalty.setCmsOffers([]))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appliedOfferPromoCode]) |
The code above was developed in the PoC and is a new code. 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. Image AddedWhen the customers reload the page, the offer must continue applied. Verify the need for other validations If the user is not authenticated we need to ask for the login The behavior will be the same as we have on offers page
With this update, the frontend is prepared to apply the promo code to loyalty. Image Added |
Expand |
---|
title | 2 - TASK: Updated action Remove button |
---|
|
useEffect(() => removeCmsOffers: (state, { payload }: |
if (!loading && !orderErrors) PayloadAction<LoyaltyOffer>) => {
state.cmsOffers = |
dispatch(actionsloyaltysetSelectedOffer(null));
dispatch(actions.loyalty.setAppliedOffers([]));
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)
URL: https://dev-plk-es-whitelabel-cms.rbi.tools/desk/orderLevelContent;loyalty;incentives;offers;configOffers;a6f3eace-435e-49ea-9a79-e173f6172dcc
Image RemovedWe will need to send the sanityId: a6f3eace-435e-49ea-9a79-e173f6172dcc
to Winrest via some means of communication this information;
They will register this ID on their system;
When the intl-partners-service calls the Winrest API, the integration will know how much will be the discount;
The discount is always on the total value of the cart;
Solution proposal
Currently, we already sent the sanityId offer to the backend (intl-partners-service
);
Image RemovedThen, we need to send the discount information to Winrest;
We will add a new attribute orderDiscounts
on the webhook payload;
Note |
---|
We will send it to Winrest always in percentage (requested to Winrest, 'cause will be easier for them); |
...
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 |
---|
title | 1 - 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: The method return will be: 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; So the payload will be: Code Block |
---|
| "orderDiscounts": [
{
"type": "percentage",
"value": 50
}
] |
payload Code Block |
---|
| {
"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 |
---|
title | 3 - TASK: Show only promo code input |
---|
|
We need to render only the input for the promotional code instead of the collapse structure if our flag is ON Raphael Ferreira Gomes said that when the component is expanded some events are dispatched to mParticle. We need to validate what will happen now that the component will be loaded in the screen without the collapse. He recommended that we need to talk with Manuel (Manuel Rodrigues da Silva) to understand if this will be a problem and what’s expect for the events part ← To be validate during development
|
Expand |
---|
title | 4 - TASK: Apply discount again if not used - NOT FOR NOW |
---|
|
This task is not for now. We’ll develop a PoC and validate the ideas first 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. When have the promo code information on file:
Important: We don’t have the POC to this task |
Expand |
---|
title | 5 - TASK: Clean the applied offer when finished the order - MOVED TO ANOTHER TASK |
---|
|
This task was moved to another task This task was moved to another task as this objective conflicts with we’ll do in the Jira Legacy |
---|
server | System JIRA |
---|
serverId | 255417eb-03fa-3e2f-a6ba-05d325fec50d |
---|
key | IBFEC-1419 |
---|
|
Important (BLOCKED): apparently this flow changed in the current app and the result was not what Rodrigo showed in the screenshots below (redeem in restaurant modal). We don’t have enough time during the A&D to go deep on this. We can talk with de Sousa Santos, Rodrigo to understand better what code we should base in.
There is a feature flag that limits what kind of modal should be shown: ENABLE_IMPROVED_SIMPLIFIED_REDEEM_IN_RESTAURANT_STATIC_OFFERS With this flag on, we won’t have a close modal button like the one below: Image AddedAnd when it’s turned on it will be shown as below: Image Added Code Block |
---|
| 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)
URL: https://dev-plk-es-whitelabel-cms.rbi.tools/desk/orderLevelContentloyaltyincentivesoffersconfigOffersa6f3eace-435e-49ea-9a79-e173f6172dcc
Image AddedWe will need to send the sanityId: a6f3eace-435e-49ea-9a79-e173f6172dcc
to Winrest via some means of communication this information
They will register this ID on their system
When the intl-partners-service calls the Winrest API, the integration will know how much will be the discount
The discount is always on the total value of the cart
Solution proposal
Currently, we already sent the sanityId offer to the backend (intl-partners-service
)
Image AddedThen, we need to send the discount information to Winrest
We will add a new attribute orderDiscounts
on the webhook IWebhookPriceOrder
payload
Acceptance criteria
We can’t impact the other markets
If there is an offer applied on the “input cart“, we need to send the discount to Winrest
Tasks:
Expand |
---|
title | 6 - TASK: Send to orderDiscount to Winrest |
---|
|
Image Added |
Expand |
---|
title | 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)
|
Expand |
---|
title | 3 - TASK: Add the discount on the order; |
---|
|
|
Expand |
---|
title | 4 - TASK: Send the new discount object to Winrest |
---|
|
We will need to send the new discount attribute to Winrest by webhook: Image RemovedThen, 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 Removed Code Block |
---|
| 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 |
---|
| const verifyDiscountTypes = (cartEntry: ICartEntry): boolean => {
return (
cartEntry.type !== CartEntryType.offerDiscount &&
cartEntry.type !== CartEntryType.offerCombo &&
cartEntry.type !== CartEntryType.offerItem
);
};Create function to get LaunchDarkly. - Image Added
In the line 328 we should validate flag ENABLE_AT_LOYALTY_OFFER_CHECKOUT Image AddedImage AddedTo 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
After that, we will send the discount to Winrest via IWebhookPriceOrder adding the orderDiscounts Image Added
Payload will be:
Code Block |
---|
| {
"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
}
],
"price": {
"amount": 1100,
"currency": "EUR"
},
"quantity": 1
}
]
},
"channel": "WHITELABEL_IN_STORE",
"fees": [],
"id": "84b5f1df-28df-4e6f-a871-a2783602436f",
"instructions": [],
"serviceMode": "TAKEOUT",
"storeId": "1111",
"orderDiscounts": [
{
"type": "percentage", // or amount
"value": 50
}
]
} |
|
Screenshots
POC
FRONTEND (WHITELABEL)
BACKEND (INTL-PARTNERS-SERVICE)
Impact Analysis
Dependencies
...