import { AcademicCapSolid } from "@medusajs/icons"
import { LearningPath } from "docs-ui"

export const metadata = {
  title: `Integrate Ecommerce Stack Recipe`,
}

# {metadata.title}

In this recipe, you'll learn how to integrate your systems in your ecommerce stack with Medusa.

## Overview

Integrating third-party systems, such as ERP or a CMS, into your ecommerce stack can be challenging. It requires:

- Establishing connections with the different systems based on each of their APIs.
- Building flows that span across multiple systems.
- Maintaining data consistency and syncing between your systems.

Medusa’s architecture and functionalities allow you to integrate third-party systems and build flows around them. It also provides error-handling mechanisms and webhook capabilities that prevent data inconsistency between your systems.

<LearningPath pathName="integrate-ecommerce-stack" />

---

## Connect to External Systems with Services

Medusa’s Services let you implement a client that connects and performs functionalities with your third-party system. You can then use the service to connect to your third-party system in other resources, such as a Workflow or an API Route.

This increases the maintainability of your integration as it’s all implemented within the service. If the external system makes any changes to its APIs, you only need to make changes within the service.

<Card
  href="#"
  title="Create a Service"
  text="Learn how to create a service in Medusa."
  startIcon={<AcademicCapSolid />}
  showLinkIcon={false}
/>

<Details summaryContent="Example: Create a service for an ERP system">

    Here’s an example of a service created for an external ERP system:

    ```ts title="src/services/erp.ts"
    import { ProductService, TransactionBaseService } from "@medusajs/medusa";
    import { MedusaError } from "@medusajs/utils"
    import axios, { AxiosInstance } from "axios"
    
    type InjectedDependencies = {
      productService: ProductService
    }
    
    class ErpService extends TransactionBaseService {
      private client_: AxiosInstance
      private productService_: ProductService
    
      constructor(container: InjectedDependencies) {
        super(container)
    
        this.productService_ = container.productService
        this.client_ = axios.create({
          baseURL: `https://api.erp-example.com`,
          headers: {
            Authorization: `Bearer ${process.env.ERP_TOKEN}`
          }
        })
      }
    
      async getProductERPDetails (id: string) {
        const product = await this.productService_.retrieve(id, {
          select: ["external_id"]
        })
    
        if (!product) {
          throw new MedusaError(MedusaError.Types.NOT_FOUND, `Product with id ${id} was not found.`)
        }
    
        const erpProduct = await this.client_.get(`/product/${product.external_id}`)
    
        return erpProduct
      }
    
      async updateProduct(product: any) {
        // do stuff
      }
    }
    
    export default ErpService
    ```

    In this example, you create an `ErpService` that is used to interact with the third-party system. In the service, you create a client using [axios](https://axios-http.com/) to send requests to the ERP system. If the system you’re integrating provides a JavaScript SDK, you can use that instead.
    
    Then, in the service, you create methods for each ERP functionality. For example, the `getProductErpDetails` method retrieves the details of a product in the ERP system.
    
    <Note title="Tip">

    Products have an `external_id` attribute that can be used to store the product's ID in an external system.

    </Note>

</Details>
    

---

## Build Flows Across Systems

With Medusa’s workflows, you can build flows with steps that may perform actions on different systems. For example, you can create a workflow that updates the product’s details in integrated systems like ERPs, WMSs, and CMSs.

Workflows can be executed from anywhere. So, taking the workflow described in the above example, you can listen to the `product.updated` event using a Subscriber and execute the workflow whenever the event is triggered.

![A flowchart of how the workflow is executed when the product.updated event is triggered](https://res.cloudinary.com/dza7lstvk/image/upload/v1709025778/Medusa%20Book/workflow-recipe-example_yfwfuy.jpg)

Workflows guarantee data consistency through their compensation feature. You can provide a compensation function to steps that roll back the actions of that step. Then, if an error occurs in any step, the actions of previous steps are rolled back using their compensation function.

<Card
  href="#"
  title="Workflows"
  text="Learn more about Workflows and how to create them."
  startIcon={<AcademicCapSolid />}
  showLinkIcon={false}
/>

<Details summaryContent="Example: Update products across systems with workflows">

    For example, create the following workflow in `src/workflows/update-product.ts`:

    ```ts title="src/workflows/update-product.ts"
    import { Product, ProductService } from "@medusajs/medusa"
    import {
      createStep,
      StepResponse,
      createWorkflow
    } from "@medusajs/workflows-sdk"
    import ErpService from "../services/erp"
    
    type WorkflowInput = {
      productId: string
    }
    
    const updateInErpStep = createStep("update-in-erp", async ({ productId }: WorkflowInput, context) => {
      const erpService: ErpService = context.container.resolve("erpService")
      const productService: ProductService = context.container.resolve("productService")
      
      const updatedProductData = await productService.retrieve(productId)
    
      const oldProductData = await erpService.getProductERPDetails(productId)
    
      await erpService.updateProduct(updatedProductData)
      
      return new StepResponse({}, {
    		// provide the old product data as a parameter
    		// to the compensation function in case an error
    		// occurs.
        oldProductData
      })
    }, async ({
      oldProductData
    }, context) => {
      const erpService: ErpService = context.container.resolve("erpService")
    
      await erpService.updateProduct(oldProductData)
    })
    
    const updateInCmsStep = createStep("update-in-cms", async({ productId }: WorkflowInput, context) => {
      // update product in CMS...
    })
    
    const updateInWmsStep = createStep("update-in-wms", async({ productId }: WorkflowInput, context) => {
      // update product in WMS...
    })
    
    const updateProductWorkflow = createWorkflow<
      WorkflowInput, void
    >("update-product-in-systems", 
    	function (input) {
      updateInErpStep(input)
    
      updateInCmsStep(input)
    
      updateInWmsStep(input)
    })
    
    export default updateProductWorkflow
    ```

    In this workflow, you create three steps: one to update the product in an ERP, another in a CMS, and another in a WMS.
    
    In the `update-in-erp` step, you resolve the ERP’s service that you created and use it to update the product in the ERP. You also provide a compensation function, which is passed as a third parameter to the `createStep` function. The compensation function reverts the update in the ERP system in case an error occurs.
    
    You then create the workflow `update-product-in-systems` which uses the three steps in its constructor function. The workflow’s constructor function doesn’t run until it’s executed.
    
    Then, create the subscriber at `src/subscribers/update-product.ts`:

    ```ts title="src/subscribers/update-product.ts"
    import { 
      type SubscriberConfig, 
      type SubscriberArgs,
      ProductService,
    } from "@medusajs/medusa"
    import updateProductWorkflow from "../workflows/update-product"
    
    export default async function handleProductUpdate({ 
      data, eventName, container, pluginOptions 
    }: SubscriberArgs<Record<string, any>>) {
      updateProductWorkflow(container)
        .run({
          input: {
            productId: data.id
          }
        })
        .then(() => {
          console.log("Updated product across systems.")
        })
    }
    
    export const config: SubscriberConfig = {
      event: ProductService.Events.UPDATED,
      context: {
        subscriberId: "product-update"
      }
    }
    ```

    The subscriber executes the workflow whenever the `product.updated` event is triggered, passing it the ID of the updated product.

</Details>

---

## Create Webhook Listeners

You can provide webhook listeners that your external systems call when their data is updated. This lets you synchronize data between your systems. To create webhook listeners, create an API route.

For example, suppose an administrator changes the product data in the ERP system. The system then sends a request to the webhook you define in your Medusa application, which updates the product data in the application.

<Card
  href="#"
  title="Create an API Route"
  text="Learn how to create an API Route in Medusa."
  startIcon={<AcademicCapSolid />}
  showLinkIcon={false}
/>

<Details summaryContent="Example: Create a webhook listener for ERP changes">

    For example, create the file `src/api/webhooks/erp/update/route.ts` with the following content:

    ```ts title="src/api/webhooks/erp/update/route.ts"
    import { 
    	MedusaRequest, 
    	MedusaResponse, 
    	ProductService
    } from "@medusajs/medusa"
    
    export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
      const { id, updatedData} = req.body
    
      const productService: ProductService = req.scope.resolve(
    		"productService"
    	)
    
      await productService.update(id, updatedData)
    
      res.status(200)
    }
    ```
    
    This creates a webhook listener for an ERP system. It receives the ID of a product and its updated data, assuming that’s how your ERP system sends the data.
    
    Then, create the file `src/api/middlewares.ts` with the following content:

    ```ts title="src/api/middlewares.ts"
    import { MiddlewaresConfig } from "@medusajs/medusa"
    import { raw } from "body-parser"
    
    export const config: MiddlewaresConfig = {
      routes: [
        {
          method: ["POST", "PUT"],
          matcher: "/webhooks/*",
          bodyParser: false,
          middlewares: [raw({ type: "application/json" })],
        },
      ],
    }
    ```

    This replaces the default JSON middleware with the raw middleware, which is useful for webhook routes.

</Details>

---

## Additional Development

Refer to other guides in the Medusa Resources or the Medusa Book for additional guidance during your development.
