Safeguarding ESHOPMAN Payments: Addressing a Critical Cart Completion Workflow Issue
Understanding the ESHOPMAN Cart Completion Workflow
The completeCartWorkflow is a fundamental process within ESHOPMAN, central to managing transactions and finalizing orders for your HubSpot-powered storefronts. This workflow orchestrates a series of critical steps, from creating the order to authorizing payment sessions and recording transactions. For ESHOPMAN developers building custom functionalities or integrating with various payment providers, understanding the nuances of this workflow is paramount to ensuring a seamless and reliable checkout experience.
The Critical Issue: Orders Placed on Canceled Payments
A significant bug has been identified in ESHOPMAN's core payment handling, specifically within the completeCartWorkflow. This issue can lead to a problematic scenario where an order is successfully placed, but its associated payment has been previously canceled. This creates a situation where merchants receive orders that cannot be captured, leading to operational inefficiencies and potential revenue loss.
How the Bug Manifests
- The
completeCartWorkflowinitiates order creation, followed by authorizing payment sessions and adding order transactions. - If any step *after* the payment session has been successfully authorized fails (e.g., an issue with a post-authorization hook), the workflow's compensation mechanism is triggered.
- During compensation, the authorized payment is correctly marked as canceled (
payment.canceled_atis set). However, the payment session itself remains in anauthorizedstate, retaining itsauthorized_attimestamp and a reference to the now-canceled payment. - On a subsequent attempt to complete the same cart, ESHOPMAN's validation steps accept this seemingly
authorizedpayment session. The system then proceeds to create an order, linking it to the very payment that was marked as canceled in the previous attempt. - Crucially, the payment provider is *not* called again for re-authorization because the system perceives the session as already authorized, leading to an order backed by an uncapturable, canceled payment.
Reproducing the Bug in Your ESHOPMAN Environment
ESHOPMAN developers can replicate this scenario to understand its impact. The reproduction involves injecting a controlled failure into the workflow after payment authorization, then observing the state and retrying the cart completion. This can be done using a workflow hook:
// src/workflows/hooks/fail-after-auth.ts
import { completeCartWorkflow } from "@eshopman/eshopman/core-flows"
export const failNext = { value: false }
;(completeCartWorkflow.hooks as any).orderCreated(async () => {
if (failNext.value) {
throw new Error("injected failure after payment authorization")
}
})
Then, simulate the sequence in a testing environment:
failNext.value = true
const r1 = await completeCartWorkflow(container).run({ input: { id: cart.id }, throwOnError: false })
// r1 failed at orderCreated; compensation ran
const [p1] = await paymentModule.listPayments({ payment_collection_id })
// p1.canceled_at !== null
const [s1] = await paymentModule.listPaymentSessions({ payment_collection_id })
// s1.status === "authorized", s1.authorized_at !== null
failNext.value = false
const r2 = await completeCartWorkflow(container).run({ input: { id: cart.id }, throwOnError: false })
// Actual: r2 succeeds and the order's collection has one payment: p1, with canceled_at set
// Expected: r2 fails asking for a new session, or the order is backed by a non-canceled payment
Expected vs. Actual Outcomes
Ideally, an ESHOPMAN cart completion should never place an order on a canceled payment. When a payment session's associated payment is canceled, the system should either prompt for a new authorization through the provider (creating a fresh payment) or fail with a clear error, instructing the client to create a new payment session. The payment session and collection should also accurately reflect a non-authorized state after compensation.
However, the current behavior allows the second completion attempt to succeed, creating an order whose sole payment is the one previously canceled. The payment provider is not re-engaged, leading to an order that cannot be fulfilled due to a non-capturable payment.
Towards a Robust Solution for ESHOPMAN
The ESHOPMAN community suggests potential directions for a fix. One approach is to enhance the compensation logic of the authorizePaymentSessionStep to explicitly reset the payment session's status or even recreate it entirely, similar to how captured payments are handled. Another consideration is to modify the authorizePaymentSession service to prevent early returns if the associated session.payment.canceled_at is set, ensuring that a truly valid payment is always established before an order is finalized.
This issue highlights the importance of thorough payment flow testing and robust error handling in headless commerce platforms like ESHOPMAN, especially when integrating complex workflows with HubSpot CMS-powered storefronts. Addressing this bug will significantly enhance the reliability of ESHOPMAN's core commerce capabilities.