Ensuring Checkout Integrity Multi-Tab E-Commerce Journeys: Handling Cart Modifications and Navigation Flow
We are building OneShop, an e-commerce platform for telco products.
Building and managing an e-commerce platform for telco products comes with unique challenges. These platforms must cater to a complex user journey, ensure high data integrity, and handle frequent modifications while providing a seamless experience. Below are the key challenges and considerations:
Key Problems in the Checkout Journey
- Step Skipping via URL Manipulation
- Users bypass mandatory steps in the checkout process by directly altering the URL (e.g., navigating from the basket to the payment page without completing personal or shipping information).
- This leads to incomplete data submission, inconsistent user journeys, and potential errors during order processing.
2. Multi-Tab Modifications Creating Inconsistencies
- Users duplicate the checkout tab and perform conflicting actions across tabs. For example:
- Modifying the basket in one tab (e.g., adding or removing items).
- Continuing the checkout process in another tab without reflecting the changes made.
- This results in stale data and invalid states, making it difficult to ensure accurate and seamless order placement.
3. Data Consistency Across Steps
- Ensuring that user inputs (e.g., personal, shipping, or payment details) are captured and validated sequentially without skipping intermediate steps.
- Discrepancies in sequential validation can result in missing critical information like addresses, shipping preferences, or payment confirmations.
4. Concurrent Cart Updates
- Users sharing carts across multiple devices or sessions can create conflicting updates, where the checkout journey in one session invalidates the state in another. For example:
- Device A updates the cart while Device B is at the payment stage.
- The mismatch between cart states can lead to failed transactions or incorrect order details.
5. User Frustration Due to Soft Errors
- Users encountering sudden redirects (e.g., being sent back to the basket) without clear messaging about what went wrong.
- This can degrade user experience and reduce confidence in the checkout process.
6. Multi-Basket Scenarios Complicating Checkout
- If a user adds items to create multiple active baskets, the system must handle and clearly define which basket is active in the checkout flow.
- Handling multiple baskets during checkout can lead to confusion, especially when users try to proceed with a different basket than initially selected.
Solution Implemented
We identified the steps involved in the checkout journey and provided a ranking for each step:
BASKET("basket", 1),
PERSONAL_INFO("personalInfo", 2),
COMPANY_INFO("companyInfo", 2),
CONNECTION_SKIP("connectionSkip",3),
CONNECTION("connection",3),
BOOKING_APPOINTMENT("bookingAppointment", 3),
BOOKING_APPOINTMENT_SKIP("bookingAppointmentSkip", 3),
SHIPPING("shipping", 4),
SHIPPING_SKIP("shippingSkip", 4),
PAYMENT("payment", 5),
PAYMENT_SKIP("paymentSkip", 5),
BILLING("billing", 6),
BILLING_SKIP("billingSkip", 6),
ORDER_REVIEW("orderReview", 7);
1. Front-End Responsibilities
Sharing currentScreenStep
:
- For every API request during the user journey, the FE includes the
currentScreenStep
to indicate the current step in the sequence (e.g., personalInfo, shippingInfo).
Sharing completionStep
:
- The FE sends the
completionStep
only in the final API call of a particular screen/step.
Sharing lastModificationTimeStamp (LMT)
- The FE stores the
LastModificationTimeStamp (LMT)
in session storage (tab-specific). - Every API request includes the LMT to ensure consistency with the backend data.
2. Back-End Responsibilities
The backend validates requests based on currentScreenStep, completionStep, and LastModificationTimeStamp (LMT) to ensure data integrity and prevent inconsistencies.
Scenarios:
- Match: Proceed with request processing.
- Mismatch: Reject the request and send an error response to the FE indicating stale cart data.
- Request Validation on Arrival:
Validate LMT:
- The LMT sent by the FE is compared with the
CHANGED_DATE
stored in the database (ShoppingCart
table).
private boolean isValidCurrentTimeStampProvided(String lastModificationTimeStamp,
ShoppingCart shoppingCart) {
boolean isValid = true;
if (Objects.isNull(shoppingCart) || Objects.isNull(shoppingCart.getAudit())
|| Objects.isNull(shoppingCart.getAudit().getChangedDate())
|| StringUtils.isEmpty(lastModificationTimeStamp)
|| lastModificationTimeStamp.equals("N/A")) {
isValid = false;
}
if (Objects.nonNull(shoppingCart) && Objects.nonNull(shoppingCart.getAudit())
&& Objects.nonNull(shoppingCart.getAudit().getChangedDate())
&& !StringUtils.equals(lastModificationTimeStamp,
String.valueOf(shoppingCart.getAudit().getChangedDate().toEpochSecond()))) {
isValid = false;
}
return isValid;
}
Validate currentScreenStep
:
- Check if the
currentScreenStep
is consistent with the expected journey sequence. - Reject requests if the user is attempting to skip steps.
private boolean isValidCurrentStepRequest(String currentStep, ShoppingCart shoppingCart) {
boolean isValid = true;
if (Objects.isNull(shoppingCart) || Objects.isNull(shoppingCart.getCompletionStep())) {
isValid = false;
}
if (Objects.nonNull(shoppingCart) && Objects.nonNull(shoppingCart.getCompletionStep())) {
int rankCurrentStep = CurrentScreenStep.of(currentStep).getRanking();
int rankCompletionStep = CurrentScreenStep
.of(shoppingCart.getCompletionStep().getCurrentScreenStep()).getRanking();
int diff = rankCurrentStep - rankCompletionStep;
if (diff > 1) {
isValid = false;
}
}
return isValid;
}
Post-Execution Logic:
- AOP Aspect Logic (
AfterReturning
Advice): - After successfully processing the API request:
- Update the
completionStep
in the database with the value received from the FE. - Fetch the updated LMT from the
ShoppingCart
table. - Include the new LMT in the API response to the FE.
@AfterReturning("publicMethodInsideAClassMarkedWithRestController()")
public void updateCompletionStepAndReturnLMT() {
if (!Boolean.parseBoolean(HttpUtils.getHeaderValue(request, X_REQUEST_S_2_S_IDENTIFIER))
&& BffUtils.isMultiTabModificationValidationEnabled()) {
String completionScreenStep = request.getParameter(CommonConstants.COMPLETION_STEP);
String currentScreenStep = request.getHeader(CommonConstants.X_REQUEST_CURRENT_STEP);
String lastModificationTimeStampFromRequest =
request.getHeader(CommonConstants.X_REQUEST_CART_LAST_MODIFIED_TIMESTAMP);
if (StringUtils.isNotEmpty(completionScreenStep)
|| Objects.nonNull(CurrentScreenStep.of (currentScreenStep))
|| DEVICE_DETAIL.equals(currentScreenStep)){
ShoppingCart sc = BffUtils.getShoppingCart();
// check for back and forth of screen which are not modifying database by itself
if (StringUtils.isNoneEmpty(completionScreenStep, currentScreenStep,
lastModificationTimeStampFromRequest)) {
boolean completionStepSame = multiTabHandler.isCompletionStepAndLmtSameInDB(completionScreenStep,
lastModificationTimeStampFromRequest, sc);
if (completionStepSame) {
response.setHeader(CommonConstants.X_REQUEST_CART_LAST_MODIFIED_TIMESTAMP,
lastModificationTimeStampFromRequest);
return;
}
}
if (Objects.nonNull(CurrentScreenStep.of(currentScreenStep))
|| DEVICE_DETAIL.equals(currentScreenStep)) {
OffsetDateTime lastModificationTimeStamp = null;
if ((StringUtils.isNotEmpty(completionScreenStep)
|| CurrentScreenStep.BASKET.getCurrentScreenStep().equals(completionScreenStep)
|| DEVICE_DETAIL.equals(currentScreenStep)) && Objects.nonNull(sc)) {
sc.setCompletionStep(CurrentScreenStep.of(completionScreenStep));
lastModificationTimeStamp = multiTabHandler.updateShoppingCart(sc);
}
if (Objects.nonNull(lastModificationTimeStamp)) {
multiTabHandler.setHeaders(response,
String.valueOf(lastModificationTimeStamp.toEpochSecond()));
} else {
if (Objects.nonNull(sc) && Objects.nonNull(sc.getAudit())
&& Objects.nonNull(sc.getAudit().getChangedDate())) {
multiTabHandler.setHeaders(response,
String.valueOf(sc.getAudit().getChangedDate().toEpochSecond()));
}
}
}
}
}
}
Case 1 : Success
Tab 1 (Original) Journey with LMT Check:
Step 1 Step 2 Step 3 Step 4 Step 5
------------------->------------------->------------------->------------------->------------------->
"BASKET" "PERSONAL_INFO" "CONNECTION" "SHIPPING" "PAYMENT"
(currentStep) (currentStep) (currentStep) (currentStep) (currentStep)
(lastModTime) (lastModTime) (lastModTime) (lastModTime) (lastModTime)
(completionStep) (completionStep) (completionStep) (completionStep) (completionStep)
Step 1 Example:
{
"currentScreenStep": "basket",
"completionStep": "basket",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}
Step 2 Example:
{
"currentScreenStep": "personalInfo",
"completionStep": "personalInfo",
"lastModificationTimeStamp": "2024-01-17T12:05:00Z"
}
Step 3 Example:
{
"currentScreenStep": "connection",
"completionStep": "connection",
"lastModificationTimeStamp": "2024-01-17T12:10:00Z"
}
Step 4 Example:
{
"currentScreenStep": "shipping",
"completionStep": "shipping",
"lastModificationTimeStamp": "2024-01-17T12:15:00Z"
}
Step 5 Example:
{
"currentScreenStep": "payment",
"completionStep": "payment",
"lastModificationTimeStamp": "2024-01-17T12:20:00Z"
}
Case 2: Failure
Tab 1 (Original) and Tab 2 (Duplicate) Journey with LMT Check:
Tab 1 (Original) Tab 2 (Duplicate)
-------------------->---------------------------------------------->
"BASKET" (Step 1) → "PERSONAL_INFO" (Step 2) "BASKET" (Step 1) → "PERSONAL_INFO" (Step 2)
(currentStep) (currentStep)
(lastModTime) (lastModTime)
(completionStep) (completionStep)
--------------------------------------------------->------------------------------->
Step 1 Example: Step 1 Example:
{
"currentScreenStep": "basket", {
"completionStep": "basket", "currentScreenStep": "basket",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z" "completionStep": "basket",
} "lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}
Tab 2 Update:
{
"currentScreenStep": "personalInfo",
"completionStep": "personalInfo",
"lastModificationTimeStamp": "2024-01-17T12:05:00Z"
}
**At this point, the LMT on Tab 2 is updated (modified)** after changes are made.
Now, on **Tab 1** (Original):
{
"currentScreenStep": "personalInfo",
"completionStep": "personalInfo",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}
Tab 1 has an **older LMT** value ("2024-01-17T12:00:00Z"), while Tab 2 has an **updated LMT** ("2024-01-17T12:05:00Z").
--------------------------------------------------->------------------------------->
**LMT Mismatch Detected** **Invalid Cart Detected**
{ }
"currentScreenStep": "personalInfo" |
"completionStep": "personalInfo" |
"lastModificationTimeStamp": "2024-01-17T12:05:00Z" - LMT on Tab 2 is newer than Tab 1
}
Case 3: Failure
Tab 1 (Original) with URL manipulation by skipping step with currentScreenStep Check:
Tab 1 (Original) Tab 2 Manipulated(Duplicate)
-------------------->---------------------------------------------->
"BASKET" (Step 1) → "PERSONAL_INFO" (Step 2) "BASKET" (Step 1) → "SHIPPING" (Step 2)
(currentStep) (currentStep)
(lastModTime) (lastModTime)
(completionStep) (completionStep)
--------------------------------------------------->------------------------------->
Step 1 Example: Step 1 Example:
{
"currentScreenStep": "basket", {
"completionStep": "basket", "currentScreenStep": "SHIPPING",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z" "completionStep": "SHIPPING",
} "lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}
**At this point, the currentScreenStep on Tab 2 is updated (modified)** after changes are made in URL.
Now, on **Tab 2** :
{
"currentScreenStep": "SHIPPING",
"completionStep": "SHIPPING",
"lastModificationTimeStamp": "2024-01-17T12:00:00Z"
}
Tab 2 has an **new currentScreenStep ** value SHIPPING while BackEnd system has completionStep as Basket in database.
Ranking of Shipping = 4
Ranking of Basket = 1
CompletionStep(in db)- currentScreenStep(from FE)
Now the difference is 3, but the allowed difference is 1, So it will reject the reqeust.
--------------------------------------------------->------------------------------->
**CurrentScreenStep Mismatch Detected** **Invalid Cart Detected**
{ }
"currentScreenStep": "SHIPPING" |
"completionStep": "SHIPPING" |
"lastModificationTimeStamp": "2024-01-17T12:05:00Z"
}
Conclusion:
The OneShop e-commerce platform’s approach to multi-tab checkout integrity provides a highly reliable and robust solution that addresses common challenges in the e-commerce space. By implementing a comprehensive strategy that combines timestamp-based validation, step-by-step validation, and multi-tab synchronization, we ensure that users experience a seamless, error-free journey from cart to payment. This results in increased customer satisfaction, higher conversion rates, and a more reliable platform overall. The robust backend validation coupled with proactive error handling creates a frictionless experience, mitigating the risks associated with concurrent sessions and ensuring that data remains accurate and consistent across the entire checkout process.