- Add a new homepage to `book` project for the routing page - Move all main doc pages to be under `/v2/learn` (and added redirects + fixed links across docs) - Other: add admin components to resources dropdown + fixes to search on mobile. Closes DX-955 Preview: https://docs-v2-git-docs-router-page-medusajs.vercel.app/v2
951 lines
32 KiB
Plaintext
951 lines
32 KiB
Plaintext
import { AcademicCapSolid, BoltSolid } from "@medusajs/icons"
|
||
import { LearningPath, BetaBadge } from "docs-ui"
|
||
|
||
export const metadata = {
|
||
title: `Commerce Automation Recipe`,
|
||
}
|
||
|
||
# {metadata.title} <BetaBadge text="Soon" tooltipText="This recipe is a work in progress, as some features are not ready yet in Medusa V2." />
|
||
|
||
This recipe provides the general steps to implement a B2B store with Medusa.
|
||
|
||
## Overview
|
||
|
||
Commerce automation is essential for businesses to save costs, provide a better user experience, and avoid manual, repetitive tasks that lead to human errors. Businesses utilize automation in different domains, including marketing, customer support, and order management.
|
||
|
||
Medusa provides the necessary architecture and tools to implement commerce automation for order management, customer service, and more. You can perform an asynchronous action when an event is triggered, schedule a job that runs at a specified interval, and more.
|
||
|
||
---
|
||
|
||
## Re-Stock Notifications
|
||
|
||
Customers may be interested in a product that is currently out of stock.
|
||
|
||
To implement sending restock notifications, you can:
|
||
|
||
- Create a module that manages the customers subscribed to a variant's restock notification.
|
||
- Create relationships to the Product and Sales Channel modules. A variant's inventory is managed by the sales channel's associated stock locations.
|
||
- Create an API route that allows customers to subscribe to a variant's restock notification.
|
||
- Create a subscriber that listens to the `inventory-item.updated` event and sends a notification to the subscribed customers if the variant's quantity is more than `0`.
|
||
|
||
<Note type="soon">
|
||
|
||
The `inventory-item.updated` event is currently not emitted.
|
||
|
||
</Note>
|
||
|
||
<CardList items={[
|
||
{
|
||
href: "!docs!/learn/basics/modules",
|
||
title: "Create a Module",
|
||
text: "Learn how to create a module in Medusa.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "!docs!/learn/basics/modules#1-create-data-model",
|
||
title: "Create a Data Model",
|
||
text: "Learn how to create a data model.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
]} />
|
||
|
||
<CardList items={[
|
||
{
|
||
href: "!docs!/learn/basics/api-routes",
|
||
title: "Create an API Route",
|
||
text: "Learn how to create an API route in Medusa.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "!docs!/learn/basics/events-and-subscribers",
|
||
title: "Create a Subscriber",
|
||
text: "Learn how to create a subscriber in Medusa.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
]} className="mt-1" />
|
||
|
||
<Details summaryContent="Example">
|
||
|
||
In this example, you'll create a Restock Notification Module with the features explained above.
|
||
|
||
### Create Restock Notification Module
|
||
|
||
Start by creating the `src/modules/restock-notification` directory.
|
||
|
||
Then, create the file `src/modules/restock-notification/models/restock-notification.ts` with the following content:
|
||
|
||
export const restockModelHighlights = [
|
||
["5", "email", "The email of the customer to send the notification to when the item is restocked."],
|
||
["6", "variant_id", "The ID of the variant the customer is subscribed to."],
|
||
["7", "sales_channel_id", "The ID of the sales channel the customer is viewing the product variant from."]
|
||
]
|
||
|
||
```ts title="src/modules/restock-notification/models/restock-notification.ts" highlights={restockModelHighlights}
|
||
import { model } from "@medusajs/framework/utils"
|
||
|
||
const RestockNotification = model.define("restock_notification", {
|
||
id: model.id().primaryKey(),
|
||
email: model.text(),
|
||
variant_id: model.text(),
|
||
sales_channel_id: model.text(),
|
||
})
|
||
|
||
export default RestockNotification
|
||
```
|
||
|
||
This creates a `RestockNotification` data model with the following properties:
|
||
|
||
- `id`: An automatically generated ID.
|
||
- `email`: The email of the customer to send the notification to when the item is restocked.
|
||
- `variant_id`: The ID of the variant the customer is subscribed to. This will later be used to form a relationship with the `ProductVariant` data model of the Product Module.
|
||
- `sales_channel_id`: The ID of the sales channel the customer is viewing the product variant from. This will later be used to form a relationship with the `SalesChannel` data model of the Sales Channel Module.
|
||
|
||
Since a variant's inventory is managed based on the locations of each sales channel, you have to specify which sales channel to check stock quantity in.
|
||
|
||
Next, create the file `src/modules/restock-notification/migrations/Migration20240516140616.ts` with the following content:
|
||
|
||
```ts title="src/modules/restock-notification/migrations/Migration20240516140616.ts"
|
||
import { Migration } from "@mikro-orm/migrations"
|
||
|
||
export class Migration20240516140616 extends Migration {
|
||
|
||
async up(): Promise<void> {
|
||
this.addSql("create table if not exists \"restock_notification\" (\"id\" text not null, \"email\" text not null, \"variant_id\" text not null, \"sales_channel_id\" text not null, constraint \"restock_notification_pkey\" primary key (\"id\"));")
|
||
}
|
||
|
||
async down(): Promise<void> {
|
||
this.addSql("drop table if exists \"restock_notification\" cascade;")
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
You'll run the migration to reflect the changes on the database after finishing the module's definition.
|
||
|
||
Then, create the module's main service at `src/modules/restock-notification/service.ts` with the following content:
|
||
|
||
```ts title="src/modules/restock-notification/service.ts"
|
||
import { MedusaService } from "@medusajs/framework/utils"
|
||
import RestockNotification from "./models/restock-notification"
|
||
|
||
class RestockNotificationModuleService extends MedusaService({
|
||
RestockNotification,
|
||
}){
|
||
// TODO add custom methods
|
||
}
|
||
|
||
export default RestockNotificationModuleService
|
||
```
|
||
|
||
The module's main service extends the service factory which generates basic management features for the `RestockNotification` data model.
|
||
|
||
Next, create the module's definition file `src/modules/restock-notification/index.ts` with the following content:
|
||
|
||
```ts title="src/modules/restock-notification/index.ts"
|
||
import RestockNotificationModuleService from "./service"
|
||
import { Module } from "@medusajs/framework/utils"
|
||
|
||
export default Module("restock-notification", {
|
||
service: RestockNotificationModuleService,
|
||
})
|
||
```
|
||
|
||
Finally, add the module to the `modules` object in `medusa-config.ts`:
|
||
|
||
```ts title="medusa-config.ts"
|
||
module.exports = defineConfig({
|
||
// ...
|
||
modules: [
|
||
{
|
||
resolve: "./src/modules/restock-notification",
|
||
},
|
||
]
|
||
})
|
||
```
|
||
|
||
You can now run the migrations with the following command:
|
||
|
||
```bash npm2yarn
|
||
npx medusa db:migrate
|
||
```
|
||
|
||
### Create Restock Notification API Route
|
||
|
||
Create the file `src/api/store/restock-notification/route.ts` with the following content:
|
||
|
||
```ts title="src/api/store/restock-notification/route.ts" collapsibleLines="1-13" expandButtonLabel="Show Imports"
|
||
import type {
|
||
MedusaRequest,
|
||
MedusaResponse,
|
||
} from "@medusajs/framework/http"
|
||
import RestockNotificationModuleService
|
||
from "../../../modules/restock-notification/service"
|
||
|
||
type RestockNotificationReq = {
|
||
email: string
|
||
variant_id: string
|
||
sales_channel_id: string
|
||
}
|
||
|
||
export async function POST(
|
||
req: MedusaRequest<RestockNotificationReq>,
|
||
res: MedusaResponse
|
||
) {
|
||
const restockNotificationModuleService:
|
||
RestockNotificationModuleService = req.scope.resolve(
|
||
"restockNotificationModuleService"
|
||
)
|
||
|
||
await restockNotificationModuleService.createRestockNotifications(
|
||
req.body
|
||
)
|
||
|
||
res.json({
|
||
success: true,
|
||
})
|
||
}
|
||
```
|
||
|
||
This creates a `POST` API route at `/store/restock-notification`. It accepts the `email`, `variant_id`, and `sales_channel_id` request body parameters and creates a restock notification.
|
||
|
||
### Create Inventory Item Updated Subscriber
|
||
|
||
To handle the sending of the restock notifications, create a subscriber that listens to the `inventory-item.updated` event, then sends a notification using the Notification Module to subscribed emails.
|
||
|
||
<Note type="soon">
|
||
|
||
The `inventory-item.updated` event is currently not emitted.
|
||
|
||
</Note>
|
||
|
||
{/* To handle the sending of the restock notifications, create the file `src/subscribers/inventory-item-update.ts` with the following content: */}
|
||
|
||
export const subscriberHighlights = [
|
||
["48", "inventoryVariantLinkService", "Retrieve an instance of the link service for the product-variant-inventory-item link module."],
|
||
["55", "inventoryVariantItems", "Retrieve the variants linked to the updated inventory item."],
|
||
["68", "restockQuery", "Assemble the query to retrieve the restock notifications with their associated variants."],
|
||
["81", "restockNotifications", "Retrieve the restock notifications using the query."],
|
||
["84", "salesChannelLocationService", "Retrieve an instance of the link service for the sales-channel-stock-location link module."],
|
||
["93", "salesChannelLocations", "Retrieve the stock locations linked to the restock notification's sales channel."],
|
||
["107", "availableQuantity", "Retrieve the available quantity of the variant in the retrieved stock locations."],
|
||
["116", "continue", "Only send the notification if the available quantity is greater than `0`"],
|
||
["119", "createNotifications", "Send the notification to the customer using the Notification Module."],
|
||
["122", '"test_template"', "Replace with the actual template used for sending the email."],
|
||
["123", "data", "The data to send along to the third-party service sending the notification."],
|
||
["131", "deleteRestockNotifications", "Delete the restock notification to not send the notification again."]
|
||
]
|
||
|
||
{/* ```ts title="src/subscribers/inventory-item-update.ts" highlights={subscriberHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports"
|
||
import type {
|
||
SubscriberArgs,
|
||
SubscriberConfig,
|
||
} from "@medusajs/framework"
|
||
import {
|
||
IInventoryService,
|
||
INotificationModuleService,
|
||
RemoteQueryFunction,
|
||
} from "@medusajs/framework/types"
|
||
import {
|
||
ContainerRegistrationKeys,
|
||
Modules,
|
||
remoteQueryObjectFromString,
|
||
} from "@medusajs/framework/utils"
|
||
import {
|
||
RemoteLink,
|
||
} from "@medusajs/framework/modules-sdk"
|
||
import RestockNotificationModuleService
|
||
from "../modules/restock-notification/service"
|
||
|
||
// subscriber function
|
||
export default async function inventoryItemUpdateHandler({
|
||
data,
|
||
container,
|
||
}: SubscriberArgs<{ id: string }>) {
|
||
const remoteQuery: RemoteQueryFunction = container.resolve(
|
||
ContainerRegistrationKeys.REMOTE_QUERY
|
||
)
|
||
const remoteLink: RemoteLink = container.resolve(
|
||
ContainerRegistrationKeys.REMOTE_LINK
|
||
)
|
||
const restockNotificationModuleService:
|
||
RestockNotificationModuleService = container.resolve(
|
||
"restockNotificationModuleService"
|
||
)
|
||
const inventoryModuleService: IInventoryService =
|
||
container.resolve(Modules.INVENTORY)
|
||
const notificationModuleService: INotificationModuleService =
|
||
container.resolve(
|
||
Modules.NOTIFICATION
|
||
)
|
||
|
||
const inventoryItemId = data.data.id
|
||
|
||
const inventoryVariantLinkService = remoteLink.getLinkModule(
|
||
Modules.PRODUCT,
|
||
"variant_id",
|
||
Modules.INVENTORY,
|
||
"inventory_item_id"
|
||
)
|
||
|
||
const inventoryVariantItems =
|
||
await inventoryVariantLinkService.list({
|
||
inventory_item_id: [inventoryItemId],
|
||
}) as {
|
||
variant_id: string,
|
||
inventory_item_id: string
|
||
}[]
|
||
|
||
if (!inventoryVariantItems.length) {
|
||
console.log("no inventory variant items")
|
||
return
|
||
}
|
||
|
||
const restockQuery = remoteQueryObjectFromString({
|
||
entryPoint: "restock_notification",
|
||
fields: [
|
||
"email",
|
||
"variant.name",
|
||
],
|
||
variables: {
|
||
filters: {
|
||
variant_id: inventoryVariantItems[0].variant_id,
|
||
},
|
||
},
|
||
})
|
||
|
||
const restockNotifications =
|
||
await remoteQuery(restockQuery)
|
||
|
||
const salesChannelLocationService = remoteLink.getLinkModule(
|
||
Modules.SALES_CHANNEL,
|
||
"sales_channel_id",
|
||
Modules.STOCK_LOCATION,
|
||
"stock_location_id"
|
||
)
|
||
|
||
for (const restockNotification of restockNotifications) {
|
||
const salesChannelLocations =
|
||
await salesChannelLocationService.list({
|
||
sales_channel_id: [
|
||
restockNotification.sales_channel_id,
|
||
],
|
||
}) as {
|
||
stock_location_id: string
|
||
sales_channel_id: string
|
||
}[]
|
||
|
||
if (!salesChannelLocations.length) {
|
||
continue
|
||
}
|
||
|
||
const availableQuantity = await inventoryModuleService
|
||
.retrieveAvailableQuantity(
|
||
inventoryItemId,
|
||
salesChannelLocations.map(
|
||
(salesChannelLocation) =>
|
||
salesChannelLocation.stock_location_id
|
||
)
|
||
)
|
||
|
||
if (availableQuantity === 0) {
|
||
continue
|
||
}
|
||
|
||
notificationModuleService.createNotifications({
|
||
to: restockNotification.email,
|
||
channel: "email",
|
||
template: "test_template",
|
||
data: {
|
||
variant_id: restockNotification.variant_id,
|
||
variant_name: restockNotification.variant.title,
|
||
// other data...
|
||
},
|
||
})
|
||
|
||
// delete the restock notification
|
||
await restockNotificationModuleService
|
||
.deleteRestockNotifications(restockNotification.id)
|
||
}
|
||
}
|
||
|
||
// subscriber config
|
||
export const config: SubscriberConfig = {
|
||
event: "inventory-item.updated",
|
||
}
|
||
```
|
||
|
||
This adds a subscriber to the `inventory-item.updated` event. In the subscriber handler function, you:
|
||
|
||
- Retrieve an instance of the link service for the product-variant-inventory-item link module.
|
||
- Retrieve the variants linked to the updated inventory item.
|
||
- Retrieve the restock notifications of those variants.
|
||
- For each restock notification, you:
|
||
- Retrieve its quantity based on the stock location associated with the restock notification's sales channel.
|
||
- If the quantity is greater than `0`, you send a notification using the Notification Module and delete the restock notification. */}
|
||
|
||
</Details>
|
||
|
||
---
|
||
|
||
## Automated Customer Support
|
||
|
||
Customer support is essential to build a store's brand and customer loyalty. This can include integrating with third-party services or automating notifications sent to customers when changes happen related to their orders, returns, exchanges, and more.
|
||
|
||
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">
|
||
|
||
The [Events reference](../../events-reference/page.mdx) shows an extensive list of events triggered by the each commerce module.
|
||
|
||
</Note> */}
|
||
|
||
Medusa also provides Notification Module Providers that integrate with third-party services, such as SendGrid.
|
||
|
||
<CardList items={[
|
||
{
|
||
href: "/architectural-modules/notification",
|
||
title: "Notification Module",
|
||
text: "Learn about the Notification Module.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "!docs!/learn/basics/events-and-subscribers",
|
||
title: "Create Subscriber",
|
||
text: "Learn how to create a subscriber to handle events.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
]} />
|
||
|
||
---
|
||
|
||
## Automatic Data Synchronization
|
||
|
||
As your commerce store grows, you'll likely need to synchronize data across different systems. For example, you need to synchronize data with an ERP system or a data warehouse.
|
||
|
||
To implement that:
|
||
|
||
- Create a workflow that implements the synchronization steps, along with retry and rollback logic.
|
||
- Create a scheduled job that executes the workflow automatically at the specified time pattern.
|
||
|
||
<Card
|
||
href="!docs!/learn/basics/scheduled-jobs"
|
||
title="Create a Scheduled Job"
|
||
text="Learn how to create a scheduled job in Medusa."
|
||
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
|
||
|
||
Using Medusa's architecture and commerce features, you can automate a large amount of order management functionalities.
|
||
|
||
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/basics/events-and-subscribers"
|
||
title="Create a Subscriber"
|
||
text="Learn how to create a subscriber in Medusa."
|
||
icon={AcademicCapSolid}
|
||
/>
|
||
|
||
{/* <CardList items={[
|
||
{
|
||
href: "!docs!/learn/basics/events-and-subscribers",
|
||
title: "Create a Subscriber",
|
||
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,
|
||
},
|
||
]} /> */}
|
||
|
||
---
|
||
|
||
## Automated RMA Flow
|
||
|
||
Businesses must optimize their Return Merchandise Authorization (RMA) flow to ensure a good customer experience and service. By automating the flow, customers request to return their received items, and businesses quickly support them.
|
||
|
||
Medusa's commerce features are geared towards automating RMA flows and ensuring a good customer experience:
|
||
|
||
- Customers can create order returns from the storefront. Merchants then receive a notification and handle the return from the Medusa Admin.
|
||
- Merchants can make order changes and request the customer's approval for them. The customer can also send any additional payment if necessary.
|
||
- Every order-related action triggers an event, which you can listen to with a subscriber. This allows you to handle order events to automate actions.
|
||
|
||
<CardList items={[
|
||
{
|
||
href: "/commerce-modules/orders",
|
||
title: "Order Module",
|
||
text: "Learn about the Order Module and its features.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "!docs!/learn/basics/events-and-subscribers",
|
||
title: "Create a Subscriber",
|
||
text: "Learn how to create a subscriber in Medusa.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
]} />
|
||
|
||
---
|
||
|
||
## Customer Segmentation
|
||
|
||
Businesses use customer segmentation to organize customers into different groups and then apply different price rules to these groups.
|
||
|
||
Medusa's commerce modules provide the necessary features to implement this use case:
|
||
|
||
- The Customer Module provides a customer groups feature to organize customers into customer groups.
|
||
- The Pricing Module provides the features to specify prices based on a condition, such as the group of the customer.
|
||
|
||
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",
|
||
title: "Customer Module",
|
||
text: "Learn about the Customer Module and its features.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "/commerce-modules/pricing",
|
||
title: "Pricing Module",
|
||
text: "Learn about the Pricing Module and its features.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "!docs!/learn/basics/events-and-subscribers",
|
||
title: "Create a Subscriber",
|
||
text: "Learn how to create a subscriber in Medusa.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
]} />
|
||
|
||
<Details summaryContent="Example: Add customer to VIP group">
|
||
|
||
Here’s 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
|
||
|
||
In your commerce store, you may utilize marketing strategies that encourage customers to make purchases. For example, you send a newsletter when new products are added to your store.
|
||
|
||
To do that, create a subscriber that listens to the `product.created`, and send an email to subscribed customers with tools like SendGrid or Mailchimp.
|
||
|
||
You can also create a scheduled job that checks whether the number of new products has exceeded a set threshold, then sends out the newsletter.
|
||
|
||
<CardList items={[
|
||
{
|
||
href: "!docs!/learn/basics/events-and-subscribers",
|
||
title: "Create a Subscriber",
|
||
text: "Learn how to create a subscriber in Medusa.",
|
||
icon: AcademicCapSolid,
|
||
},
|
||
{
|
||
href: "!docs!/learn/basics/scheduled-jobs",
|
||
title: "Scheduled Jobs",
|
||
text: "Learn how to create a scheduled job in Medusa.",
|
||
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>
|