Skip to content

Commit

Permalink
docs: added "Integrate Your Ecommerce Stack" recipe (#5774)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahednasser authored Nov 30, 2023
1 parent 4e9d954 commit c808317
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 9 deletions.
307 changes: 307 additions & 0 deletions www/apps/docs/content/recipes/integrate-ecommerce-stack.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
---
addHowToData: true
---

import DocCard from '@theme/DocCard';
import Icons from '@theme/Icon';
import LearningPath from '@site/src/components/LearningPath';

# Integrate Ecommerce Stack Recipe

This document guides you through integrating 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.

<DocCard item={{
type: 'link',
href: '/development/services/create-service',
label: 'Create a Service',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create a service in Medusa.',
}
}} />

<Details>
<Summary>Example: Create a service for an ERP system</Summary>

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.

:::tip

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

:::

</Details>


---

## Build Flows Across Systems

With Medusas workflows, you can build flows with steps that may perform actions on different systems. For example, you can create a workflow that updates the products 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](../development/events/subscribers.mdx) 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/v1701340569/Medusa%20Docs/Diagrams/workflow-recipe-example_wmwmo5.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 the compensation function.

<DocCard item={{
type: 'link',
href: '/development/workflows',
label: 'Workflows',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn more about Workflows and how to create them.',
}
}} />

<Details>
<Summary>Example: Update products across systems with workflows</Summary>

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. Webhook listeners can be created in Medusa using API Routes.
For example, suppose an administrator changes the product data in the ERP system. In that case, the system sends a request to the webhook you define in Medusa, which updates the product data in Medusa.
<DocCard item={{
type: 'link',
href: '/development/api-routes/create',
label: 'Create an API Route',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create an API Route in Medusa.',
}
}} />
<Details>
<Summary>Example: Create a webhook listener for ERP changes</Summary>
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
You can find other resources for your ecommerce development in the [Medusa Development section](../development/overview.mdx) of this documentation.
8 changes: 8 additions & 0 deletions www/apps/docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ module.exports = {
iconName: "credit-card-solid",
},
},
{
type: "doc",
id: "recipes/integrate-ecommerce-stack",
label: "Integrate Ecommerce Stack",
customProps: {
iconName: "puzzle-solid",
},
},
{
type: "doc",
id: "recipes/commerce-automation",
Expand Down
18 changes: 9 additions & 9 deletions www/apps/docs/src/components/LearningPath/Steps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const LearningPathSteps: React.FC<LearningPathStepsProps> = ({ ...rest }) => {
)}
key={index}
>
<div className={clsx("flex items-center gap-1")}>
<div className={clsx("flex items-center gap-1 relative")}>
<div className="w-2 flex-none flex items-center justify-center">
{index === currentStep && (
<IconCircleDottedLine
Expand All @@ -102,6 +102,14 @@ const LearningPathSteps: React.FC<LearningPathStepsProps> = ({ ...rest }) => {
>
{step.title}
</span>
<Link
href={step.path}
className={clsx("absolute top-0 left-0 w-full h-full")}
onClick={(e) => {
e.preventDefault()
goToStep(index)
}}
/>
</div>
{index === currentStep && (
<div className={clsx("flex items-center gap-1")}>
Expand All @@ -113,14 +121,6 @@ const LearningPathSteps: React.FC<LearningPathStepsProps> = ({ ...rest }) => {
</div>
</div>
)}
<Link
href={step.path}
className={clsx("absolute top-0 left-0 w-full h-full")}
onClick={(e) => {
e.preventDefault()
goToStep(index)
}}
/>
</div>
))}
</div>
Expand Down
Loading

3 comments on commit c808317

@vercel
Copy link

@vercel vercel bot commented on c808317 Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs-ui – ./www/apps/ui

docs-ui-git-develop-medusajs.vercel.app
docs-ui-medusajs.vercel.app
docs-ui.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c808317 Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

api-reference – ./www/apps/api-reference

docs.medusajs.com
api-reference-git-develop-medusajs.vercel.app
api-reference-medusajs.vercel.app
api-reference-delta.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c808317 Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

medusa-docs – ./www/apps/docs

medusa-docs.vercel.app
medusa-docs-git-develop-medusajs.vercel.app
medusa-docs-medusajs.vercel.app

Please sign in to comment.