docs: add notes + missing links for user guide (#11621)

* docs: add notes + missing links for user guide

* fix build errors

* fixes
This commit is contained in:
Shahed Nasser
2025-02-26 15:28:18 +02:00
committed by GitHub
parent 322d108c03
commit 95eef899f7
76 changed files with 736 additions and 794 deletions
@@ -1,11 +1,10 @@
import { AcademicCapSolid, BoltSolid } from "@medusajs/icons"
import { BetaBadge } from "docs-ui"
export const metadata = {
title: `Commerce Automation Recipe`,
}
# {metadata.title} <BetaBadge text="Soon" tooltipText="This recipe is a work in progress." />
# {metadata.title}
This recipe provides the general steps to implement commerce automation with Medusa.
@@ -36,11 +35,11 @@ Customer support is essential to build a store's brand and customer loyalty. Thi
You can use the Notification Module to send notifications when an action is triggered, such as when a customer or their order is updated.
{/* <Note title="Tip">
<Note title="Tip">
The [Events reference](../../events-reference/page.mdx) shows an extensive list of events triggered by the each commerce module.
The [Events reference](../../events-reference/page.mdx) shows an extensive list of events triggered for each commerce module.
</Note> */}
</Note>
Medusa also provides Notification Module Providers that integrate with third-party services, such as SendGrid.
@@ -77,211 +76,6 @@ To implement that:
icon={AcademicCapSolid}
/>
<Details summaryContent="Example: Synchronizing products with a third-party service">
For example, create the file `src/workflows/sync-products.ts` with the following content:
export const syncProductsWorkflowHighlight = [
["20", "retrieveStoreStep", "A step that retrieves the store by its ID."],
["36", "retrieveProductsToUpdateStep", "A step that retrieves the products to update based on a last update date."],
["56", "syncProductsStep", "A step to sync the product with a third-party service."],
["59", "productSyncModuleService", "Assuming this is a custom module's main service that provides connection to the third-party service."],
["63", "productsBeforeSync", "Retrieve old product data from third-party service for compensation function."],
["68", "sync", "Sync the product data in the third-party service."],
["72", "", "Pass products data before sync to compensation function."],
["75", "", "A compensation function to revert the sync when an error occurs."],
["81", "sync", "Revert the product's data in the third-party service to its old data before the synchronization."],
["90", "updateStoreLastSyncStep", "A step to update the `last_sync_data` of the store."],
["96", "prevLastSyncDate", "Retrieve the previous value of `last_sync_date` to pass it to compensation function."],
["98", "update", "Update the `last_sync_date` of the store."],
["106", "", "Pass previous last sync date to compensation function."],
["109", "", "A compensation function to revert the update of `last_sync_data` if an error occurs."],
["125", "syncProductsWorkflow", "Define the workflow that uses the above steps."]
]
```ts title="src/workflows/sync-products.ts" highlights={syncProductsWorkflowHighlight} collapsibleLines="1-16" expandButtonLabel="Show Imports"
import {
Modules,
} from "@medusajs/framework/utils"
import {
IProductModuleService,
IStoreModuleService,
ProductDTO,
StoreDTO,
} from "@medusajs/framework/types"
import {
StepResponse,
createStep,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
type RetrieveStoreStepInput = {
id: string
}
const retrieveStoreStep = createStep(
"retrieve-store-step",
async ({ id }: RetrieveStoreStepInput, { container }) => {
const storeModuleService: IStoreModuleService =
container.resolve(Modules.STORE)
const store = await storeModuleService.retrieveStore(id)
return new StepResponse({ store })
}
)
type RetrieveProductsToUpdateStepInput = {
last_sync_date?: string
}
const retrieveProductsToUpdateStep = createStep(
"retrieve-products-to-update-step",
async ({ last_sync_date }: RetrieveProductsToUpdateStepInput, { container }) => {
const productModuleService: IProductModuleService =
container.resolve(Modules.PRODUCT)
const products = await productModuleService.listProducts({
updated_at: {
$gt: last_sync_date,
},
})
return new StepResponse({ products })
}
)
type SyncProductsStepInput = {
products: ProductDTO[]
}
const syncProductsStep = createStep(
"sync-products-step",
async ({ products }: SyncProductsStepInput, { container }) => {
const productSyncModuleService = container.resolve(
"productSyncModuleService"
)
const productsBeforeSync = await productSyncModuleService.listProductSyncs({
id: products.map((product) => product.id),
})
for (const product of products) {
await productSyncModuleService.sync(product)
}
return new StepResponse({}, {
products: productsBeforeSync,
})
},
async ({ products }, { container }) => {
const productSyncModuleService = container.resolve(
"productSyncModuleService"
)
for (const product of products) {
await productSyncModuleService.sync(product)
}
}
)
type UpdateStoreLastSyncStepInput = {
store: StoreDTO
}
const updateStoreLastSyncStep = createStep(
"update-store-last-sync-step",
async ({ store }: UpdateStoreLastSyncStepInput, { container }) => {
const storeModuleService: IStoreModuleService =
container.resolve(Modules.STORE)
const prevLastSyncDate = store.metadata.last_sync_date
await storeModuleService.updateStores(store.id, {
metadata: {
last_sync_date: (new Date()).toString(),
},
})
return new StepResponse({}, {
id: store.id,
last_sync_date: prevLastSyncDate,
})
},
async ({ id, last_sync_date }, { container }) => {
const storeModuleService: IStoreModuleService =
container.resolve(Modules.STORE)
await storeModuleService.updateStores(id, {
metadata: {
last_sync_date,
},
})
}
)
type SyncProductsWorkflowInput = {
store_id: string
}
export const syncProductsWorkflow = createWorkflow(
"sync-products-workflow",
function ({ store_id }: SyncProductsWorkflowInput) {
const { store } = retrieveStoreStep({
id: store_id,
})
const { products } = retrieveProductsToUpdateStep({
last_sync_date: store.metadata.last_sync_date,
})
syncProductsStep({
products,
})
updateStoreLastSyncStep({
store,
})
}
)
```
This creates a workflow with the following steps:
1. Retrieve the store by its ID.
2. Retrieve products to update based on the last date and time the products were synced. The last sync date is retrieved from the store's `metadata` property.
3. Sync the retrieved products with a third-party service. It's assumed that the connection to the third-party service is implemented within a custom module's main service.
4. Update the last sync date of the store to the current date.
Then, create a scheduled job at `src/jobs/sync-products.ts` that executes the workflow at the specified interval:
```ts
import { MedusaContainer } from "@medusajs/framework/types"
import {
syncProductsWorkflow,
} from "../workflows/sync-products"
export default async function syncProductsJob(
container: MedusaContainer
) {
await syncProductsWorkflow(container)
.run({
input: {
name: "John",
},
})
}
export const config = {
name: "sync-products",
// execute every minute
schedule: "0 0 * * *",
numberOfExecutions: 3,
}
```
</Details>
---
## Order Management Automation
@@ -290,20 +84,7 @@ Using Medusa's architecture and commerce features, you can automate a large amou
To handle events within an order flow and automate actions, create a subscriber. For example, create a subscriber that listens to the `order.placed` event and automatically creates a fulfillment if predefined conditions are met.
<Note type="soon">
The `order.placed` event is currently not emitted.
</Note>
<Card
href="!docs!/learn/fundamentals/events-and-subscribers"
title="Create a Subscriber"
text="Learn how to create a subscriber in Medusa."
icon={AcademicCapSolid}
/>
{/* <CardList items={[
<CardList items={[
{
href: "!docs!/learn/fundamentals/events-and-subscribers",
title: "Create a Subscriber",
@@ -316,7 +97,7 @@ The `order.placed` event is currently not emitted.
text: "Check out triggered events by each commerce module.",
icon: AcademicCapSolid,
},
]} /> */}
]} />
---
@@ -361,12 +142,6 @@ For example, to group customers with over twenty orders:
1. Create a subscriber that listens to the `order.placed` event.
2. If the customer has more than 20 orders, add them to the VIP customer group.
<Note type="soon">
The `order.placed` event is currently not emitted.
</Note>
<CardList items={[
{
href: "/commerce-modules/customer",
@@ -386,91 +161,14 @@ The `order.placed` event is currently not emitted.
text: "Learn how to create a subscriber in Medusa.",
icon: AcademicCapSolid,
},
{
href: "/events-reference",
title: "Events Reference",
text: "Check out triggered events by each commerce module.",
icon: AcademicCapSolid,
},
]} />
<Details summaryContent="Example: Add customer to VIP group">
Heres an example of a subscriber that listens to the `order.placed` event and checks whether the customer should be added to the VIP customer group based on their number of orders:
<Note type="soon">
The `order.placed` event is currently not emitted.
</Note>
```ts title="src/subscribers/add-custom-to-vip.ts" collapsibleLines="1-12" expandButtonLabel="Show Imports"
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
import {
Modules,
} from "@medusajs/framework/utils"
import {
ICustomerModuleService,
IOrderModuleService,
} from "@medusajs/framework/types"
export default async function orderCreatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const orderId = data.id
const orderModuleService: IOrderModuleService = container
.resolve(Modules.ORDER)
const customerModuleService:
ICustomerModuleService = container.resolve(
Modules.CUSTOMER
)
// check if VIP group exists
const vipGroup = await customerModuleService
.listCustomerGroups({
name: "VIP",
}, {
relations: ["customers"],
})
if (!vipGroup.length) {
return
}
// retrieve the order
const order = await orderModuleService.retrieveOrder(orderId)
if (!order ||
!order.customer_id ||
vipGroup[0].customers.find(
(customer) => customer.id === order.customer_id
) !== undefined) {
return
}
const [, count] = await orderModuleService.listAndCountOrders({
customer_id: order.customer_id,
})
if (count < 20) {
return
}
// add customer to VIP group
await customerModuleService.addCustomerToGroup({
customer_group_id: vipGroup[0].id,
customer_id: order.customer_id,
})
}
export const config: SubscriberConfig = {
event: "order.placed",
}
```
</Details>
---
## Marketing Automation
@@ -495,99 +193,3 @@ You can also create a scheduled job that checks whether the number of new produc
icon: AcademicCapSolid,
},
]} />
<Details summaryContent="Example: Sending a newsletter email after adding ten products">
For example, create the file `src/subscribers/send-products-newsletter.ts` with the following content:
export const newsletterHighlights = [
["32", "store", "Retrieve the first store in the application."],
["34", "products", "Retrieve the products created since the last newsletter send date."],
["40", "", "Check whether more than 10 products have been created before proceeding."],
["44", "customers", "Retrieve all customers, assuming they're considered subscribed."],
["46", "createNotifications", "Send a notification (newsletter) to each customer using the Notification Module."],
["49", '"email"', "Send the notification through the email channel."],
["50", '"newsletter_template"', "Specify the template name in the third-party service (for example, SendGrid)."],
["52", "products", "Pass the created products to the template."],
["57", "updateStores", "Update the store's `last_newsletter_send_date` property with the current date."]
]
```ts title="src/subscribers/send-products-newsletter.ts" highlights={newsletterHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
import {
Modules,
} from "@medusajs/framework/utils"
import {
ICustomerModuleService,
IProductModuleService,
IStoreModuleService,
INotificationModuleService,
} from "@medusajs/framework/types"
export default async function productCreateHandler({
container,
}: SubscriberArgs<{ id: string }>) {
const productModuleService: IProductModuleService =
container.resolve(Modules.PRODUCT)
const storeModuleService: IStoreModuleService =
container.resolve(Modules.STORE)
const customerModuleService: ICustomerModuleService =
container.resolve(Modules.CUSTOMER)
const notificationModuleService:
INotificationModuleService = container.resolve(
Modules.NOTIFICATION
)
const store = (await storeModuleService.listStores())[0]
const products = await productModuleService.listProducts({
created_at: {
$gt: store.metadata.last_newsletter_send_date,
},
})
if (products.length < 10) {
return
}
const customers = await customerModuleService.listCustomers()
await notificationModuleService.createNotifications(
customers.map((customer) => ({
to: customer.email,
channel: "email",
template: "newsletter_template",
data: {
products,
},
}))
)
await storeModuleService.updateStores(store.id, {
metadata: {
last_newsletter_send_date: (new Date()).toString(),
},
})
}
export const config: SubscriberConfig = {
event: "product.created",
}
```
In the subscriber function, you:
1. Retrieve the first store in our application.
2. Retrieve products created since the last time a newsletter is sent. The last send date is stored in the store's `metadata` property.
3. If the count of last created products is less than 10, stop execution.
4. Retrieve all customers. Here, it's assumed that all customers are considered subscribed for simplicity.
5. Use the Notification Module to send a notification to the customer. This uses the Notification Module Provider configured for the `email` channel.
6. Update the store's `last_newsletter_send_date` with the current date.
</Details>