docs: documentation for v1.18 (#5652)
* docs: documentation for v.17.5 * fix links * updated version number
This commit is contained in:
1 parent
adc60e519c
commit
9c7f95c3d5
36 files changed
+1499
-1336
No files matched your search
@@ -106,74 +106,63 @@ Medusa also provides official notification plugins that integrate with third-par
|
||||
Here’s an example of a subscriber that uses the SendGrid plugin to send an email to the customer when the order has a new note:
|
||||
|
||||
```ts title=src/subscribers/new-note.ts
|
||||
import {
|
||||
EventBusService,
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
ProductVariantService,
|
||||
NoteService,
|
||||
OrderService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
sendgridService: any
|
||||
noteService: NoteService
|
||||
orderService: OrderService
|
||||
}
|
||||
export default async function handleNoteCreated({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const noteService: NoteService = container.resolve(
|
||||
"noteService"
|
||||
)
|
||||
const orderService: OrderService = container.resolve(
|
||||
"orderService"
|
||||
)
|
||||
const sendgridService = container.resolve("sendgridService")
|
||||
|
||||
class NewNoteSubscriber {
|
||||
protected sendGridService_: any
|
||||
protected noteService_: NoteService
|
||||
protected orderService_: OrderService
|
||||
// retrieve note by id
|
||||
const note = await noteService.retrieve(data.id, {
|
||||
relations: ["author"],
|
||||
})
|
||||
|
||||
constructor({
|
||||
eventBusService,
|
||||
sendgridService,
|
||||
noteService,
|
||||
orderService,
|
||||
}: InjectedDependencies) {
|
||||
this.noteService_ = noteService
|
||||
this.orderService_ = orderService
|
||||
this.sendGridService_ = sendgridService
|
||||
eventBusService.subscribe(
|
||||
"note.created",
|
||||
this.handleNoteCreated
|
||||
)
|
||||
if (!note || note.resource_type !== "order") {
|
||||
return
|
||||
}
|
||||
|
||||
handleNoteCreated = async (data) => {
|
||||
// retrieve note by id
|
||||
const note = await this.noteService_.retrieve(data.id, {
|
||||
relations: ["author"],
|
||||
})
|
||||
// retrieve note's order
|
||||
const order = await orderService.retrieve(
|
||||
note.resource_id
|
||||
)
|
||||
|
||||
if (!note || note.resource_type !== "order") {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve note's order
|
||||
const order = await this.orderService_.retrieve(
|
||||
note.resource_id
|
||||
)
|
||||
|
||||
if (!order) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sendGridService_.sendEmail({
|
||||
templateId: "order-update",
|
||||
from: "hello@medusajs.com",
|
||||
to: order.email,
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
note_text: note.value,
|
||||
note_author: note.author.first_name,
|
||||
note_date: note.created_at,
|
||||
order_id: order.display_id,
|
||||
},
|
||||
})
|
||||
if (!order) {
|
||||
return
|
||||
}
|
||||
|
||||
sendGridService.sendEmail({
|
||||
templateId: "order-update",
|
||||
from: "hello@medusajs.com",
|
||||
to: order.email,
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
note_text: note.value,
|
||||
note_author: note.author.first_name,
|
||||
note_date: note.created_at,
|
||||
order_id: order.display_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default NewNoteSubscriber
|
||||
export const config: SubscriberConfig = {
|
||||
event: NoteService.Events.CREATED,
|
||||
context: {
|
||||
subscriberId: "note-created-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
@@ -206,14 +195,14 @@ You can implement automatic synchronization in Medusa using scheduled jobs. A sc
|
||||
Logger,
|
||||
ProductService,
|
||||
StoreService,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ProductSelector,
|
||||
} from "@medusajs/medusa/dist/types/product"
|
||||
import { AwilixContainer } from "awilix"
|
||||
|
||||
export default async (
|
||||
container: AwilixContainer,
|
||||
container: MedusaContainer,
|
||||
config: Record<string, unknown>
|
||||
): Promise<void> => {
|
||||
const logger = container.resolve<Logger>("logger")
|
||||
@@ -422,76 +411,65 @@ For example, if you're grouping customers with over twenty orders, you can use a
|
||||
|
||||
```ts title=src/subscribers/add-custom-to-vip.ts
|
||||
import {
|
||||
CustomerGroupService,
|
||||
CustomerService,
|
||||
EventBusService,
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
OrderService,
|
||||
CustomerService,
|
||||
CustomerGroupService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
type InjectedDependencies = {
|
||||
orderService: OrderService
|
||||
customerService: CustomerService
|
||||
customerGroupService: CustomerGroupService
|
||||
eventBusService: EventBusService
|
||||
}
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const orderService: OrderService = container.resolve(
|
||||
"orderService"
|
||||
)
|
||||
const customerService: CustomerService = container.resolve(
|
||||
"customerService"
|
||||
)
|
||||
const customerGroupService: CustomerGroupService =
|
||||
container.resolve("customerGroupService")
|
||||
|
||||
class AddCustomerToVipSubscriber {
|
||||
protected orderService_: OrderService
|
||||
protected customerService_: CustomerService
|
||||
protected customerGroupService_: CustomerGroupService
|
||||
// check if VIP group exists
|
||||
const vipGroup = await customerGroupService.list({
|
||||
name: "VIP",
|
||||
}, {
|
||||
relations: ["customers"],
|
||||
})
|
||||
if (!vipGroup.length) {
|
||||
return
|
||||
}
|
||||
|
||||
constructor({
|
||||
orderService,
|
||||
customerService,
|
||||
customerGroupService,
|
||||
eventBusService,
|
||||
}: InjectedDependencies) {
|
||||
this.orderService_ = orderService
|
||||
this.customerService_ = customerService
|
||||
this.customerGroupService_ = customerGroupService
|
||||
eventBusService.subscribe(
|
||||
"order.placed",
|
||||
this.handleOrderPlaced
|
||||
// retrieve order and its customer
|
||||
const order = await orderService.retrieve(data.id)
|
||||
|
||||
if (!order || !order.customer_id ||
|
||||
vipGroup[0].customers.find(
|
||||
(customer) => customer.id === order.customer_id
|
||||
) !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve orders of this customer
|
||||
const [, count] = await orderService.listAndCount({
|
||||
customer_id: order.customer_id,
|
||||
})
|
||||
|
||||
if (count >= 20) {
|
||||
// add customer to VIP group
|
||||
customerGroupService.addCustomers(
|
||||
vipGroup[0].id,
|
||||
order.customer_id
|
||||
)
|
||||
}
|
||||
|
||||
handleOrderPlaced = async ({ id }) => {
|
||||
// check if VIP group exists
|
||||
const vipGroup = await this.customerGroupService_.list({
|
||||
name: "VIP",
|
||||
}, {
|
||||
relations: ["customers"],
|
||||
})
|
||||
if (!vipGroup.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve order and its customer
|
||||
const order = await this.orderService_.retrieve(id)
|
||||
|
||||
if (!order || !order.customer_id ||
|
||||
vipGroup[0].customers.find(
|
||||
(customer) => customer.id === order.customer_id
|
||||
) !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve orders of this customer
|
||||
const [, count] = await this.orderService_.listAndCount({
|
||||
customer_id: order.customer_id,
|
||||
})
|
||||
|
||||
if (count >= 20) {
|
||||
// add customer to VIP group
|
||||
this.customerGroupService_.addCustomers(
|
||||
vipGroup[0].id,
|
||||
order.customer_id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AddCustomerToVipSubscriber
|
||||
export const config: SubscriberConfig = {
|
||||
event: OrderService.Events.PLACED,
|
||||
context: {
|
||||
subscriberId: "order-placed-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
@@ -544,93 +522,78 @@ You can alternatively have a Scheduled Job that checks if the number of new prod
|
||||
|
||||
```ts title=src/subscribers/send-products-newsletter.ts
|
||||
import {
|
||||
CustomerService,
|
||||
EventBusService,
|
||||
NoteService,
|
||||
OrderService,
|
||||
ProductService,
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
ProductService,
|
||||
StoreService,
|
||||
CustomerService,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ProductSelector,
|
||||
} from "@medusajs/medusa/dist/types/product"
|
||||
|
||||
type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
sendgridService: any
|
||||
productService: ProductService
|
||||
storeService: StoreService
|
||||
customerService: CustomerService
|
||||
}
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const productService: ProductService = container.resolve(
|
||||
"productService"
|
||||
)
|
||||
const storeService: StoreService = container.resolve(
|
||||
"storeService"
|
||||
)
|
||||
const customerService: CustomerService = container.resolve(
|
||||
"customerService"
|
||||
)
|
||||
const sendgridService = container.resolve("sendgridService")
|
||||
|
||||
class SendProductsNewsletterSubscriber {
|
||||
protected sendGridService_: any
|
||||
protected productService_: ProductService
|
||||
protected storeService_: StoreService
|
||||
protected customerService_: CustomerService
|
||||
|
||||
constructor({
|
||||
eventBusService,
|
||||
sendgridService,
|
||||
productService,
|
||||
storeService,
|
||||
customerService,
|
||||
}: InjectedDependencies) {
|
||||
this.productService_ = productService
|
||||
this.sendGridService_ = sendgridService
|
||||
this.storeService_ = storeService
|
||||
this.customerService_ = customerService
|
||||
eventBusService.subscribe(
|
||||
"product.created",
|
||||
this.handleProductCreated
|
||||
)
|
||||
// retrieve store to have access to last send date
|
||||
const store = await storeService.retrieve()
|
||||
|
||||
const productFilters: ProductSelector = {}
|
||||
if (store.metadata.last_send_date) {
|
||||
productFilters.created_at = {
|
||||
gt: new Date(store.metadata.last_send_date as string),
|
||||
}
|
||||
}
|
||||
|
||||
handleProductCreated = async ({ id }) => {
|
||||
// retrieve store to have access to last send date
|
||||
const store = await this.storeService_.retrieve()
|
||||
|
||||
const productFilters: ProductSelector = {}
|
||||
if (store.metadata.last_send_date) {
|
||||
productFilters.created_at = {
|
||||
gt: new Date(store.metadata.last_send_date as string),
|
||||
}
|
||||
}
|
||||
const products = await productService.list(
|
||||
productFilters
|
||||
)
|
||||
|
||||
const products = await this.productService_.list(
|
||||
productFilters
|
||||
)
|
||||
if (products.length > 10) {
|
||||
// get subscribed customers
|
||||
const customers = await customerService.list({
|
||||
metadata: {
|
||||
is_subscribed: true,
|
||||
},
|
||||
})
|
||||
sendgridService.sendEmail({
|
||||
templateId: "product-newsletter",
|
||||
from: "hello@medusajs.com",
|
||||
to: customers.map((customer) => ({
|
||||
name: customer.first_name,
|
||||
email: customer.email,
|
||||
})),
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
products,
|
||||
},
|
||||
})
|
||||
|
||||
if (products.length > 10) {
|
||||
// get subscribed customers
|
||||
const customers = await this.customerService_.list({
|
||||
metadata: {
|
||||
is_subscribed: true,
|
||||
},
|
||||
})
|
||||
this.sendGridService_.sendEmail({
|
||||
templateId: "product-newsletter",
|
||||
from: "hello@medusajs.com",
|
||||
to: customers.map((customer) => ({
|
||||
name: customer.first_name,
|
||||
email: customer.email,
|
||||
})),
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
products,
|
||||
},
|
||||
})
|
||||
|
||||
await this.storeService_.update({
|
||||
metadata: {
|
||||
last_send_date: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
await storeService.update({
|
||||
metadata: {
|
||||
last_send_date: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default SendProductsNewsletterSubscriber
|
||||
export const config: SubscriberConfig = {
|
||||
event: ProductService.Events.CREATED,
|
||||
context: {
|
||||
subscriberId: "product-create-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
@@ -921,7 +921,7 @@ To add an interface that allows the admin user to upload digital products, you c
|
||||
|
||||
When a customer purchases a digital product, they should receive a link to download it.
|
||||
|
||||
To implement that, you first need to listen to the `order.placed` event using a Subscriber. Then, in the handler method of the subscriber, you check for the digital products in the order and obtain the download URLs using the file service's `getPresignedDownloadUrl` method.
|
||||
To implement that, you first need to listen to the `order.placed` event using a Subscriber. Then, in the subscriber handler function, you check for the digital products in the order and obtain the download URLs using the file service's `getPresignedDownloadUrl` method.
|
||||
|
||||
:::note
|
||||
|
||||
@@ -954,88 +954,74 @@ Finally, you can send a notification, such as an email, to the customer using th
|
||||
|
||||
```ts title=src/subscribers/handle-order.ts
|
||||
import {
|
||||
AbstractFileService,
|
||||
EventBusService,
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
OrderService,
|
||||
AbstractFileService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
orderService: OrderService
|
||||
sendgridService: any
|
||||
fileService: AbstractFileService
|
||||
}
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const orderService: OrderService = container.resolve(
|
||||
"orderService"
|
||||
)
|
||||
const fileService: AbstractFileService = container.resolve(
|
||||
"fileService"
|
||||
)
|
||||
const sendgridService = container.resolve("sendgridService")
|
||||
|
||||
class HandleOrderSubscribers {
|
||||
protected readonly orderService_: OrderService
|
||||
protected readonly sendgridService_: any
|
||||
protected readonly fileService_: AbstractFileService
|
||||
const order = await orderService.retrieve(data.id, {
|
||||
relations: [
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product_medias",
|
||||
],
|
||||
})
|
||||
|
||||
constructor({
|
||||
eventBusService,
|
||||
orderService,
|
||||
sendgridService,
|
||||
fileService,
|
||||
}: InjectedDependencies) {
|
||||
this.orderService_ = orderService
|
||||
this.sendgridService_ = sendgridService
|
||||
this.fileService_ = fileService
|
||||
eventBusService.subscribe(
|
||||
"order.placed",
|
||||
this.handleOrderPlaced
|
||||
)
|
||||
}
|
||||
|
||||
handleOrderPlaced = async (
|
||||
data: Record<string, any>
|
||||
) => {
|
||||
const order = await this.orderService_.retrieve(data.id, {
|
||||
relations: [
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product_medias",
|
||||
],
|
||||
})
|
||||
|
||||
// find product medias in the order
|
||||
const urls = []
|
||||
for (const item of order.items) {
|
||||
if (!item.variant.product_medias.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
item.variant.product_medias.forEach(
|
||||
async (productMedia) => {
|
||||
// get the download URL from the file service
|
||||
const downloadUrl = await
|
||||
this.fileService_.getPresignedDownloadUrl({
|
||||
fileKey: productMedia.file_key,
|
||||
isPrivate: true,
|
||||
})
|
||||
|
||||
urls.push(downloadUrl)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
if (!urls.length) {
|
||||
// find product medias in the order
|
||||
const urls = []
|
||||
for (const item of order.items) {
|
||||
if (!item.variant.product_medias.length) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sendgridService_.sendEmail({
|
||||
templateId: "digital-download",
|
||||
from: "hello@medusajs.com",
|
||||
to: order.email,
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
digital_download_urls: urls,
|
||||
},
|
||||
})
|
||||
await Promise.all([
|
||||
item.variant.product_medias.forEach(
|
||||
async (productMedia) => {
|
||||
// get the download URL from the file service
|
||||
const downloadUrl = await
|
||||
fileService.getPresignedDownloadUrl({
|
||||
fileKey: productMedia.file_key,
|
||||
isPrivate: true,
|
||||
})
|
||||
|
||||
urls.push(downloadUrl)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
if (!urls.length) {
|
||||
return
|
||||
}
|
||||
|
||||
sendgridService.sendEmail({
|
||||
templateId: "digital-download",
|
||||
from: "hello@medusajs.com",
|
||||
to: order.email,
|
||||
dynamic_template_data: {
|
||||
// any data necessary for your template...
|
||||
digital_download_urls: urls,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default HandleOrderSubscribers
|
||||
export const config: SubscriberConfig = {
|
||||
event: OrderService.Events.PLACED,
|
||||
context: {
|
||||
subscriberId: "order-placed-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notice that regardless of what file service you have installed, you can access it using dependency injection under the name `fileService`.
|
||||
|
||||
@@ -256,7 +256,7 @@ You can also extend services if you need to customize a functionality implemente
|
||||
|
||||
While implementing your marketplace, you'll typically need to listen to certain events then perform actions asynchronously. For example, you can listen to the `order.placed` event and, when triggered, create child orders of the order, separating ordered items by their associated store.
|
||||
|
||||
To listen to events, you need to create Subscribers that subscribe a handler method to an event. In that handler method, you can implement the desired functionality.
|
||||
To listen to events, you need to create Subscribers that subscribe a handler function to an event. In that handler function, you can implement the desired functionality.
|
||||
|
||||
<DocCard item={{
|
||||
type: 'link',
|
||||
@@ -274,17 +274,24 @@ To listen to events, you need to create Subscribers that subscribe a handler met
|
||||
To listen to the `order.placed` event, create the file `src/subscribers/orderNotifier.ts` with the following content:
|
||||
|
||||
```ts title=src/subscribers/orderNotifier.ts
|
||||
class OrderNotifierSubscriber {
|
||||
constructor({ eventBusService }) {
|
||||
eventBusService.subscribe("order.placed", this.handleOrder)
|
||||
}
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
OrderService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
handleOrder = async (data) => {
|
||||
// TODO perform functionality
|
||||
}
|
||||
export default async function handleOrderPlaced({
|
||||
data, eventName, container, pluginOptions,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
// TODO perform functionality
|
||||
}
|
||||
|
||||
export default OrderNotifierSubscriber
|
||||
export const config: SubscriberConfig = {
|
||||
event: OrderService.Events.PLACED,
|
||||
context: {
|
||||
subscriberId: "order-placed-handler",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This subscribes the `handleOrder` method to be executed whenever the `order.placed` event is emitted.
|
||||
|
||||
@@ -75,7 +75,7 @@ Medusa uses the fulfillment service whenever a fulfillment action is performed,
|
||||
|
||||

|
||||
|
||||
In addition, Medusa has an event bus that allows you to listen to events and perform a task asynchronously when that event is triggered. So, you can listen to fulfillment-related events, such as the `order.fulfillment_created` event, and handle that event in the subscribed handler method.
|
||||
In addition, Medusa has an event bus that allows you to listen to events and perform a task asynchronously when that event is triggered. So, you can listen to fulfillment-related events, such as the `order.fulfillment_created` event, and handle that event in the subscribed handler function.
|
||||
|
||||
<DocCardList colSize={6} items={[
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ By implementing the subscription logic within your backend, you can have full co
|
||||
|
||||
Implementing the logic depends on your use case, but you'll mainly need to do two things:
|
||||
|
||||
1. Perform an action when an order is placed, such as saving subscription details. This can be done using subscribers, which register handler methods to be triggered when an event is emitted. When an order is placed, the `order.placed` event is emitted.
|
||||
1. Perform an action when an order is placed, such as saving subscription details. This can be done using subscribers, which register handler functions to be triggered when an event is emitted. When an order is placed, the `order.placed` event is emitted.
|
||||
2. Check daily for subscriptions that need renewal. This can be done using a scheduled job, which is a cron job that can be executed on a defined interval. Within that job, you can define your renewal logic.
|
||||
|
||||
<DocCardList colSize={6} items={[
|
||||
|
||||
Reference in new issue
Block a user