Files
medusa-store/www/apps/resources/app/recipes/commerce-automation/page.mdx
T
Shahed Nasser 0a37675f0e docs: add routing page (#9550)
- 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
2024-10-18 08:24:34 +00:00

951 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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">
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
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>