Unlocking Digital Fulfillment: Overriding ESHOPMAN's Cancellation Logic for Non-Physical Products

Addressing a Core Challenge in ESHOPMAN Fulfillment for Digital Goods

As an e-commerce migration expert at Move My Store, we often see ESHOPMAN developers pushing the boundaries of what's possible with headless commerce. ESHOPMAN, built on Node.js/TypeScript and deployed via HubSpot CMS, offers immense flexibility. However, a recent community discussion highlighted a specific challenge within the ESHOPMAN Fulfillment Module Service that impacts how digital products, tickets, or licenses are managed and canceled.

The issue revolves around the cancelFulfillment() method within ESHOPMAN's core Fulfillment Module Service. This method includes a crucial cancellation guard that checks if a fulfillment has already been marked as 'shipped' or 'delivered'. While this makes perfect sense for physical goods, it creates an obstacle for non-physical products. For digital items, 'shipped' or 'delivered' timestamps might be set due to administrative actions or automated workflows, even if there's no irreversible physical world-state change. The core problem is that this guard is invoked via a hardcoded self-reference to the base class, rather than a polymorphic call.

The Technical Deep Dive: Why Subclass Overrides Were Ignored

Developers extending ESHOPMAN's core services, a common practice for custom functionality, would typically subclass the relevant service and register their override via eshopman-config.ts. This pattern allows for custom logic while leveraging ESHOPMAN's robust foundation. However, for the canCancelFulfillmentOrThrow guard, this standard approach was silently bypassed. The base class's static method was always executed because the call was bound to FulfillmentModuleService by name, not dynamically via the instance's constructor.

Here's the problematic line identified in the ESHOPMAN Fulfillment Module Service:

async cancelFulfillment(id: string, sharedC {
  const fulfillment = await this.fulfillmentService_.retrieve(id, {}, sharedContext)
  FulfillmentModuleService.canCancelFulfillmentOrThrow(fulfillment)  // <-- hardcoded reference
  ...
}

This design gap meant that any custom logic implemented by an ESHOPMAN developer to relax or customize the cancellation rule for a specific fulfillment provider (e.g., a digital product provider) would simply not be invoked.

Impact on ESHOPMAN Merchants and Developers

For ESHOPMAN merchants selling digital products, this meant a lack of flexibility. Once a digital product's fulfillment was marked as 'delivered' (even if erroneously or through an automated process), canceling the fulfillment (or the entire order, which requires all fulfillments to be canceled first) would result in an error: MedusaError: "Fulfillment with id ... already delivered". The only workaround involved reimplementing significant portions of cancelFulfillment() and accessing private fields, which is not a supported or stable extension point within ESHOPMAN's Admin API.

The Elegant Solution: A One-Line Code Fix

The ESHOPMAN community proposed an elegant and minimal fix that restores the expected polymorphic behavior, allowing subclass overrides to function correctly. By replacing the hardcoded reference with a dynamic call using this.constructor, ESHOPMAN developers can now effectively customize the cancellation guard:

async cancelFulfillment(id: string, sharedC {
  const fulfillment = await this.fulfillmentService_.retrieve(id, {}, sharedContext)
  ;(this.constructor as typeof FulfillmentModuleService).canCancelFulfillmentOrThrow(fulfillment)
  ...
}

This change empowers ESHOPMAN developers to implement custom logic for their fulfillment providers, especially those handling non-physical goods, without needing to touch private fields or fully reimplement core methods. It ensures that ESHOPMAN's documented module-override patterns work as intended across all aspects of the platform.

Beyond the Fix: Future Enhancements for ESHOPMAN

The discussion also sparked a valuable suggestion for future enhancements: allowing fulfillment providers to declare a capability like handlesPhysicalGoods: false. This would enable ESHOPMAN's core guard to consult this flag, potentially removing the need for a full module override for digital/non-physical providers altogether. Such an enhancement would further streamline the development experience for ESHOPMAN users building diverse e-commerce solutions on HubSpot CMS.

Reproduction Steps for ESHOPMAN Developers:

  1. Create a fulfillment using a 'manual'-based provider for a non-physical/digital order within ESHOPMAN.
  2. Call the ESHOPMAN Admin API action to 'mark as delivered' on this fulfillment (or directly invoke markFulfillmentAsDeliveredWorkflow).
  3. Attempt to cancel the fulfillment (or the associated order) via the Admin API.
  4. Observe the MedusaError indicating the fulfillment is already delivered, despite custom overrides being in place.

This identified bug and its clear, actionable solution highlight the strength of the ESHOPMAN community in refining and improving the platform. It's a prime example of how collective knowledge enhances the ESHOPMAN experience for both developers and merchants building robust headless storefronts on HubSpot.

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools