diff --git a/www/apps/docs/content/development/entities/repositories.md b/www/apps/docs/content/development/entities/repositories.md index 51562de383..78f9aa3215 100644 --- a/www/apps/docs/content/development/entities/repositories.md +++ b/www/apps/docs/content/development/entities/repositories.md @@ -135,9 +135,9 @@ export const GET = async ( You can learn more about API Route [here](../api-routes/overview.mdx). -### Services and Subscribers +### Services -As custom repositories are registered in the [dependency container](../fundamentals/dependency-injection.md#dependency-container-and-injection), they can be accessed through dependency injection in the constructor of a service or a subscriber. +As custom repositories are registered in the [dependency container](../fundamentals/dependency-injection.md#dependency-container-and-injection), they can be accessed through dependency injection in the constructor of a service. For example: @@ -167,6 +167,32 @@ class PostService extends TransactionBaseService { You can learn more about services [here](../services/overview.mdx). +### Subscribers + +A subscriber handler function can resolve a repository using the `container` property of its parameter. The `container` has a method `resolve` which accepts the registration name of the repository as a parameter. + +For example: + +```ts title=src/subscribers/post-handler.ts +import { + type SubscriberArgs, +} from "@medusajs/medusa" + +import { PostRepository } from "../repositories/post" + +export default async function postHandler({ + data, eventName, container, pluginOptions, +}: SubscriberArgs) { + const postRepository: PostRepository = container.resolve( + "postRepository" + ) + + // ... +} + +// ... +``` + ### Other Resources Resources that have access to the dependency container can access repositories just like any other resources. You can learn more about the dependency container and dependency injection in [this documentation](../fundamentals/dependency-injection.md). diff --git a/www/apps/docs/content/development/events/create-module.md b/www/apps/docs/content/development/events/create-module.md index c2ae905606..b07398fc6a 100644 --- a/www/apps/docs/content/development/events/create-module.md +++ b/www/apps/docs/content/development/events/create-module.md @@ -62,9 +62,9 @@ In the class you must implement the `emit` method. You can optionally implement ### Note About the eventToSubscribersMap Property -The `AbstractEventBusModuleService` implements two methods for handling subscription: `subscribe` and `unsubscribe`. In these methods, the subscribed handler methods are managed within a class property `eventToSubscribersMap`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler methods. +The `AbstractEventBusModuleService` implements two methods for handling subscription: `subscribe` and `unsubscribe`. In these methods, the subscribed handler functions are managed within a class property `eventToSubscribersMap`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions. -In your custom implementation, you can use this property to manage the subscribed handler methods. For example, you can get the subscribers of a method using the `get` method of the map: +In your custom implementation, you can use this property to manage the subscribed handler functions. For example, you can get the subscribers of a method using the `get` method of the map: ```ts const eventSubscribers = @@ -144,13 +144,13 @@ class CustomEventBus extends AbstractEventBusModuleService { As mentioned earlier, this method is already implemented in the `AbstractEventBusModuleService` class. This section explains how you can implement your custom subscribe logic if necessary. -The `subscribe` method attaches a handler method to the specified event, which is run when the event is triggered. It is typically used inside a subscriber class. +The `subscribe` method attaches a handler function to the specified event, which is run when the event is triggered. It is typically used inside a subscriber class. The `subscribe` method accepts three parameters: -1. The first parameter `eventName` is a required string. It indicates which event the handler method is subscribing to. +1. The first parameter `eventName` is a required string. It indicates which event the handler function is subscribing to. 2. The second parameter `subscriber` is a required function that performs an action when the event is triggered. -3. The third parameter `context` is an optional object that has the property `subscriberId`. Subscriber IDs are useful to differentiate between handler methods when retrying a failed method. It’s also useful for unsubscribing an event handler. Note that if you must implement the mechanism around assigning IDs to subscribers when you override the `subscribe` method. +3. The third parameter `context` is an optional object that has the property `subscriberId`. Subscriber IDs are useful to differentiate between handler functions when retrying a failed method. It’s also useful for unsubscribing an event handler. Note that if you must implement the mechanism around assigning IDs to subscribers when you override the `subscribe` method. The implementation of this method depends on the service you’re using for the event bus: @@ -170,11 +170,11 @@ class CustomEventBus extends AbstractEventBusModuleService { As mentioned earlier, this method is already implemented in the `AbstractEventBusModuleService` class. This section explains how you can implement your custom unsubscribe logic if necessary. -The `unsubscribe` method is used to unsubscribe a handler method from an event. +The `unsubscribe` method is used to unsubscribe a handler function from an event. The `unsubscribe` method accepts three parameters: -1. The first parameter `eventName` is a required string. It indicates which event the handler method is unsubscribing from. +1. The first parameter `eventName` is a required string. It indicates which event the handler function is unsubscribing from. 2. The second parameter `subscriber` is a required function that was initially subscribed to the event. 3. The third parameter `context` is an optional object that has the property `subscriberId`. It can be used to specify the ID of the subscriber to unsubscribe. diff --git a/www/apps/docs/content/development/events/create-subscriber-deprecated.md b/www/apps/docs/content/development/events/create-subscriber-deprecated.md new file mode 100644 index 0000000000..6157e7fba3 --- /dev/null +++ b/www/apps/docs/content/development/events/create-subscriber-deprecated.md @@ -0,0 +1,140 @@ +--- +description: 'Learn how to create a subscriber in Medusa. You can use subscribers to implement functionalities like sending an order confirmation email.' +addHowToData: true +--- + +# (Deprecated) How to Create a Subscriber + +In this document, you’ll learn how to create a [Subscriber](./subscribers.mdx) in Medusa that listens to events to perform an action. + +:::note + +Following v1.18 of `@medusajs/medusa`, the approach in this guide is deprecated. It's recommended to follow [this guide](./create-subscriber.md) instead. + +::: + +## Implementation + +A subscriber is a TypeScript or JavaScript file that is created under `src/subscribers`. Its file name, by convention, should be the class name of the subscriber without the word `Subscriber`. For example, if the subscriber is `HelloSubscriber`, the file name should be `hello.ts`. + +After creating the file under `src/subscribers`, in the constructor of your subscriber, listen to events using `eventBusService.subscribe` , where `eventBusService` is a service injected into your subscriber’s constructor. + +The `eventBusService.subscribe` method receives the name of the event as a first parameter and as a second parameter a method in your subscriber that will handle this event. + +For example, here is the `OrderNotifierSubscriber` class created in `src/subscribers/order-notifier.ts`: + +```ts title=src/subscribers/order-notifier.ts +class OrderNotifierSubscriber { + constructor({ eventBusService }) { + eventBusService.subscribe("order.placed", this.handleOrder) + } + + handleOrder = async (data) => { + console.log("New Order: " + data.id) + } +} + +export default OrderNotifierSubscriber +``` + +This subscriber registers the method `handleOrder` as one of the handlers of the `order.placed` event. The method `handleOrder` will be executed every time an order is placed. It receives the order ID in the `data` parameter. You can then use the order’s details to perform any kind of task you need. + +:::tip + +For the `order.placed` event, the `data` object won't contain other order data. Only the ID of the order. You can retrieve the order information using the `orderService`. + +::: + +### Subscriber ID + +The `subscribe` method of the `eventBusService` accepts a third optional parameter which is a context object. This object has a property `subscriberId` with its value being a string. This ID is useful when there is more than one handler method attached to a single event or if you have multiple Medusa backends running. This allows the events bus service to differentiate between handler methods when retrying a failed one. +If a subscriber ID is not passed on subscription, all handler methods are run again. This can lead to data inconsistencies or general unwanted behavior in your system. On the other hand, if you want all handler methods to run again when one of them fails, you can omit passing a subscriber ID. + +An example of using the subscribe method with the third parameter: + +```ts +eventBusService.subscribe("order.placed", this.handleOrder, { + subscriberId: "my-unique-subscriber", +}) +``` + +--- + +## Retrieve Medusa Configurations + +Within your subscriber, you may need to access the Medusa configuration exported from `medusa-config.js`. To do that, you can access `configModule` using dependency injection. + +For example: + +```ts +import { ConfigModule, EventBusService } from "@medusajs/medusa" + +type InjectedDependencies = { + eventBusService: EventBusService + configModule: ConfigModule +} + +class OrderNotifierSubscriber { + protected readonly configModule_: ConfigModule + + constructor({ + eventBusService, + configModule, + }: InjectedDependencies) { + this.configModule_ = configModule + eventBusService.subscribe("order.placed", this.handleOrder) + } + + // ... +} + +export default OrderNotifierSubscriber +``` + +--- + +## Using Services in Subscribers + +You can access any service through the dependencies injected to your subscriber’s constructor. + +For example: + +```ts title=src/subscribers/order-notifier.ts +class OrderNotifierSubscriber { + constructor({ productService, eventBusService }) { + this.productService = productService + + eventBusService.subscribe( + "order.placed", + this.handleOrder + ) + } + // ... +} +``` + +You can then use `this.productService` anywhere in your subscriber’s methods. For example: + +```ts title=src/subscribers/order-notifier.ts +class OrderNotifierSubscriber { + // ... + handleOrder = async (data) => { + // ... + const product = this.productService.list() + } +} +``` + +:::note + +When using attributes defined in the subscriber, such as the `productService` in the example above, you must use an arrow function to declare the method. Otherwise, the attribute will be undefined when used. + +::: + +--- + +## See Also + +- [Example: send order confirmation email](../../modules/orders/backend/send-order-confirmation.md) +- [Example: send registration confirmation email](../../modules/customers/backend/send-confirmation.md) +- [Create a Plugin](../plugins/create.mdx) \ No newline at end of file diff --git a/www/apps/docs/content/development/events/create-subscriber.md b/www/apps/docs/content/development/events/create-subscriber.md index 2e9032ff16..d788ad914a 100644 --- a/www/apps/docs/content/development/events/create-subscriber.md +++ b/www/apps/docs/content/development/events/create-subscriber.md @@ -7,51 +7,98 @@ addHowToData: true In this document, you’ll learn how to create a [Subscriber](./subscribers.mdx) in Medusa that listens to events to perform an action. -## Implementation - -A subscriber is a TypeScript or JavaScript file that is created under `src/subscribers`. Its file name, by convention, should be the class name of the subscriber without the word `Subscriber`. For example, if the subscriber is `HelloSubscriber`, the file name should be `hello.ts`. - -After creating the file under `src/subscribers`, in the constructor of your subscriber, listen to events using `eventBusService.subscribe` , where `eventBusService` is a service injected into your subscriber’s constructor. - -The `eventBusService.subscribe` method receives the name of the event as a first parameter and as a second parameter a method in your subscriber that will handle this event. - -For example, here is the `OrderNotifierSubscriber` class created in `src/subscribers/order-notifier.ts`: - -```ts title=src/subscribers/order-notifier.ts -class OrderNotifierSubscriber { - constructor({ eventBusService }) { - eventBusService.subscribe("order.placed", this.handleOrder) - } - - handleOrder = async (data) => { - console.log("New Order: " + data.id) - } -} - -export default OrderNotifierSubscriber -``` - -This subscriber registers the method `handleOrder` as one of the handlers of the `order.placed` event. The method `handleOrder` will be executed every time an order is placed. It receives the order ID in the `data` parameter. You can then use the order’s details to perform any kind of task you need. - :::tip -For the `order.placed` event, the `data` object won't contain other order data. Only the ID of the order. You can retrieve the order information using the `orderService`. +v1.18 of `@medusajs/medusa` introduced a new approach to create a subscriber. If you're looking for the old guide, you can find it [here](./create-subscriber-deprecated.md). However, it's highly recommended you follow this new approach, as the old one is deprecated. ::: -### Subscriber ID +## Implementation -The `subscribe` method of the `eventBusService` accepts a third optional parameter which is a context object. This object has a property `subscriberId` with its value being a string. This ID is useful when there is more than one handler method attached to a single event or if you have multiple Medusa backends running. This allows the events bus service to differentiate between handler methods when retrying a failed one. -If a subscriber ID is not passed on subscription, all handler methods are run again. This can lead to data inconsistencies or general unwanted behavior in your system. On the other hand, if you want all handler methods to run again when one of them fails, you can omit passing a subscriber ID. +A subscriber is a TypeScript or JavaScript file that is created under `src/subscribers`. It can be created under subdirectories of `src/subscribers` as well. For example, you can place all subscribers to product events under the `src/subscribers/products` directory. -An example of using the subscribe method with the third parameter: +The subscriber file exports a default handler function, and the subscriber's configurations. -```ts -eventBusService.subscribe("order.placed", this.handleOrder, { - subscriberId: "my-unique-subscriber", -}) +For example: + +```ts title=src/subscribers/product-update-handler.ts +import { + ProductService, + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" + +export default async function productUpdateHandler({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const productService: ProductService = container.resolve( + "productService" + ) + + const { id } = data + + const product = await productService.retrieve(id) + + // do something with the product... +} + +export const config: SubscriberConfig = { + event: ProductService.Events.UPDATED, + context: { + subscriberId: "product-update-handler", + }, +} ``` +### Subscriber Configuration + +The exported configuration object of type `SubscriberConfig` must include the following properties: + +- `event`: A string or an array of strings, each being the name of the event that the subscriber handler function listens to. +- `context`: An object that defines the context of the subscriber. It can accept any properties along with the `subscriberId` property. Learn more about the subscriber ID and the context object in [this section](#context-with-subscriber-id). + +### Subscriber Handler Function + +The default-export of the subscriber file is a handler function that is executed when the events specified in the exported configuration is triggerd. + +The function accepts a parameter of type `SubscriberArgs`, which has the following properties: + +- `data`: The data payload of the emitted event. Its type is different for each event. So, make sure to check the [events reference](./events-list.md) for the expected payload of the events your subscriber listens to. You can then pass the expected payload type as a type parameter to `SubscriberArgs`, for example, `Record`. +- `eventName`: A string indicating the name of the event. This is useful if your subscriber listens to more than one event and you want to differentiate between them. +- `container`: The [dependency container](../fundamentals/dependency-injection.md) that allows you to resolve Medusa resources, such as services. +- `pluginOptions`: When the subscriber is created within a plugin, this object holds the plugin's options defined in the [Medusa configurations](../backend/configurations.md). + +--- + +## Context with Subscriber ID + +The `context` property of the subscriber configuration object is passed to the `eventBusService`. You can pass the `subscriberId` and any custom data in it. + +:::note + +The subscriber ID is useful when there is more than one handler function attached to a single event or if you have multiple Medusa backends running. This allows the events bus service to differentiate between handler functions when retrying a failed one, avoiding retrying all subscribers which can lead to data inconsistencies or general unwanted behavior in your system. + +::: + +### Inferred Subscriber ID + +If you don't pass a subscriber ID to the subscriber configurations, the name of the subscriber function is used as the subscriber ID. If the subscriber function is an anonymous function, the name of the subscriber file is used instead. + +--- + +## Caveats for Local Event Bus + +If you use the `event-bus-local` as your event bus sevice, note the following: + +- The `subscriberId` passed in the context is overwritten to a random ID when using `event-bus-local`. So, setting the subscriber ID in the context won't have any effect in this case. +- The `eventName` passed to the handler function will be `undefined` when using `event-bus-local` as it doesn't pass the event name properly. + +:::note + +While the local event bus is a good option for development, it's highly recommended to use the [Redis Event Module](./modules/redis.md) in production. + +::: + --- ## Retrieve Medusa Configurations @@ -60,71 +107,32 @@ Within your subscriber, you may need to access the Medusa configuration exported For example: -```ts -import { ConfigModule, EventBusService } from "@medusajs/medusa" +```ts title=src/subscribers/product-update-handler.ts +import { + ProductService, + type SubscriberConfig, + type SubscriberArgs, + type ConfigModule, +} from "@medusajs/medusa" -type InjectedDependencies = { - eventBusService: EventBusService - configModule: ConfigModule -} - -class OrderNotifierSubscriber { - protected readonly configModule_: ConfigModule +export default async function productUpdateHandler({ + data, eventName, container, pluginOptions, +}: SubscriberArgs) { + const configModule: ConfigModule = container.resolve( + "configModule" + ) - constructor({ - eventBusService, - configModule, - }: InjectedDependencies) { - this.configModule_ = configModule - eventBusService.subscribe("order.placed", this.handleOrder) - } - // ... } -export default OrderNotifierSubscriber -``` - ---- - -## Using Services in Subscribers - -You can access any service through the dependencies injected to your subscriber’s constructor. - -For example: - -```ts title=src/subscribers/order-notifier.ts -class OrderNotifierSubscriber { - constructor({ productService, eventBusService }) { - this.productService = productService - - eventBusService.subscribe( - "order.placed", - this.handleOrder - ) - } - // ... +export const config: SubscriberConfig = { + event: ProductService.Events.UPDATED, + context: { + subscriberId: "product-update-handler", + }, } ``` -You can then use `this.productService` anywhere in your subscriber’s methods. For example: - -```ts title=src/subscribers/order-notifier.ts -class OrderNotifierSubscriber { - // ... - handleOrder = async (data) => { - // ... - const product = this.productService.list() - } -} -``` - -:::note - -When using attributes defined in the subscriber, such as the `productService` in the example above, you must use an arrow function to declare the method. Otherwise, the attribute will be undefined when used. - -::: - --- ## See Also diff --git a/www/apps/docs/content/development/events/events-list.md b/www/apps/docs/content/development/events/events-list.md index d4b7605450..498e2552dc 100644 --- a/www/apps/docs/content/development/events/events-list.md +++ b/www/apps/docs/content/development/events/events-list.md @@ -4,7 +4,7 @@ description: 'Learn about the available events and their data payloads in Medusa # Events Reference -This document details all events in Medusa, when they are triggered, and what data your handler method will receive when the event is triggered. +This document details all events in Medusa, when they are triggered, and what data your handler function will receive when the event is triggered. ## Prerequisites diff --git a/www/apps/docs/content/development/events/subscribers.mdx b/www/apps/docs/content/development/events/subscribers.mdx index 2b983bbbf5..6f3fe7b38b 100644 --- a/www/apps/docs/content/development/events/subscribers.mdx +++ b/www/apps/docs/content/development/events/subscribers.mdx @@ -15,9 +15,9 @@ Subscribers register handlers for an events and allows you to perform an action Natively in Medusa there are subscribers to handle different events. However, you can also create your own custom subscribers. -Custom subscribers are TypeScript or JavaScript files in your project's `src/subscribers` directory. Files here should export classes, which will be treated as subscribers by Medusa. By convention, the class name should end with `Subscriber` and the file name should be the camel-case version of the class name without `Subscriber`. For example, the `WelcomeSubscriber` class is in the file `src/subscribers/welcome.ts`. +Custom subscribers are TypeScript or JavaScript files in your project's `src/subscribers` directory. Subscriber files must default export a handler function and export a configuration object. -Whenever an event is emitted, the subscriber’s registered handler method is executed. The handler method receives as a parameter an object that holds data related to the event. For example, if an order is placed the `order.placed` event will be emitted and all the handlers will receive the order id in the parameter object. +Whenever an event is emitted, the subscriber’s handler function is executed. The handler function receives as a parameter an object that includes the data payload, among other parameters. For example, if an order is placed, the `order.placed` event is emitted and all the handlers receive the order ID in the `data` object. ### Example Use Cases diff --git a/www/apps/docs/content/development/fundamentals/architecture-overview.md b/www/apps/docs/content/development/fundamentals/architecture-overview.md index 90a74a85d6..6914e4fec5 100644 --- a/www/apps/docs/content/development/fundamentals/architecture-overview.md +++ b/www/apps/docs/content/development/fundamentals/architecture-overview.md @@ -20,7 +20,7 @@ The backend doesn't have any tightly-coupled frontend. Instead, it exposes [API Medusa also uses an [Events Architecture](../events/index.mdx) to trigger and handle events. Events are triggered when a specific action occurs, such as when an order is placed. To manage this events system, Medusa connects to a service that implements a pub/sub model, such as [Redis](https://redis.io/). -Events can be handled using [Subscribers](../events/subscribers.mdx). Subscribers are TypeScript or JavaScript classes that add their methods as handlers for specific events. These handler methods are only executed when an event is triggered. +Events can be handled using [Subscribers](../events/subscribers.mdx). Subscribers are TypeScript or JavaScript files that export a handler function that's executed asynchronously when an event is triggered, and a configuration object defining what events the subscriber listens to. You can create any of the resources in the backend’s architecture, such as entities, API Routes, services, and more, as part of your custom development without directly modifying the backend itself. The Medusa backend uses [loaders](../loaders/overview.mdx) to load the backend’s resources, as well as your custom resources and resources in [Plugins](../plugins/overview.mdx). diff --git a/www/apps/docs/content/development/fundamentals/dependency-injection.md b/www/apps/docs/content/development/fundamentals/dependency-injection.md index 0390aa5b0d..060f20df09 100644 --- a/www/apps/docs/content/development/fundamentals/dependency-injection.md +++ b/www/apps/docs/content/development/fundamentals/dependency-injection.md @@ -704,14 +704,42 @@ To resolve resources, such as services, in API Routes, use the `MedusaRequest` o For example: ```ts -const logger = req.scope.resolve("logger") +const productService: ProductService = req.scope.resolve( + "productService" +) ``` Please note that in API Routes some resources, such as repositories, aren't available. Refer to the [repositories](../entities/repositories.md#api-routes) documentation to learn how you can load them. +### In Subscribers + +To resolve resources, such as services, in subscriber handler functions, use the `container` property of the handler function's parameter. The `container` has a method `resolve` which accepts the registration name of a resource as a parameter. + +For example: + +```ts +import { + ProductService, + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" + +export default async function productUpdateHandler({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const productService: ProductService = container.resolve( + "productService" + ) + + // ... +} + +// ... +``` + ### In Classes -In classes such as services, strategies, or subscribers, you can load resources in the constructor function using dependency injection. The constructor receives an object of dependencies as a first parameter. Each dependency in the object should use the registration name of the resource that should be injected to the class. +In classes such as services or strategies, you can load resources in the constructor function using dependency injection. The constructor receives an object of dependencies as a first parameter. Each dependency in the object should use the registration name of the resource that should be injected to the class. For example: diff --git a/www/apps/docs/content/development/loaders/create.md b/www/apps/docs/content/development/loaders/create.md index a8fa251151..2d1abc2b26 100644 --- a/www/apps/docs/content/development/loaders/create.md +++ b/www/apps/docs/content/development/loaders/create.md @@ -23,14 +23,14 @@ The loader file must export a function. When the loader is defined in the Medusa backend or a plugin, the function receives the following parameters: -1. `container`: the first parameter, which is a `AwilixContainer` object. You can use the container to register custom resources into the dependency container or resolve resources from the dependency container. +1. `container`: the first parameter, which is a `MedusaContainer` object. You can use the container to register custom resources into the dependency container or resolve resources from the dependency container. 2. `config`: the second parameter, which is an object that holds the loader’s plugin options. If the loader is not created inside a plugin, the config object will be empty. ### Parameters of Loaders in Modules When the loader is defined in a module, it receives the following parameters: -1. `container`: the first parameter, which is a `AwilixContainer` object. You can use the container to register custom resources into the dependency container or resolve resources from the dependency container. +1. `container`: the first parameter, which is a `MedusaContainer` object. You can use the container to register custom resources into the dependency container or resolve resources from the dependency container. 2. `logger`: the second parameter, which is a `Logger` object. The logger can be used to log messages in the console. 3. `config`: the third parameter, which is an object that holds the loader’s module options. @@ -39,11 +39,14 @@ When the loader is defined in a module, it receives the following parameters: For example, this loader function resolves the `ProductService` and logs in the console the count of products in the Medusa backend: ```ts title=src/loaders/my-loader.ts -import { ProductService, ConfigModule } from "@medusajs/medusa" -import { AwilixContainer } from "awilix" +import { + ProductService, + ConfigModule, + MedusaContainer, +} from "@medusajs/medusa" export default async ( - container: AwilixContainer, + container: MedusaContainer, config: ConfigModule ): Promise => { console.info("Starting loader...") diff --git a/www/apps/docs/content/development/logging/index.md b/www/apps/docs/content/development/logging/index.md index 46d432f058..0180eaf260 100644 --- a/www/apps/docs/content/development/logging/index.md +++ b/www/apps/docs/content/development/logging/index.md @@ -21,11 +21,11 @@ import { ProductService, ConfigModule, Logger, + MedusaContainer, } from "@medusajs/medusa" -import { AwilixContainer } from "awilix" export default async ( - container: AwilixContainer, + container: MedusaContainer, config: ConfigModule ): Promise => { const logger = container.resolve("logger") @@ -90,11 +90,11 @@ import { ProductService, ConfigModule, Logger, + MedusaContainer, } from "@medusajs/medusa" -import { AwilixContainer } from "awilix" export default async ( - container: AwilixContainer, + container: MedusaContainer, config: ConfigModule ): Promise => { const logger = container.resolve("logger") diff --git a/www/apps/docs/content/development/notification/create-notification-provider.md b/www/apps/docs/content/development/notification/create-notification-provider.md index c1ee8036d4..fa363f1349 100644 --- a/www/apps/docs/content/development/notification/create-notification-provider.md +++ b/www/apps/docs/content/development/notification/create-notification-provider.md @@ -77,7 +77,7 @@ Notification Provider Services must have a static property `identifier`. The `NotificationProvider` entity has 2 properties: `identifier` and `is_installed`. The value of the `identifier` property in the Service class is used when the Notification Provider is created in the database. -The value of this property is also used later when you want to subscribe the Notification Provider to events in a Subscriber. +The value of this property is also used later when you want to subscribe the Notification Provider to events in a Loader. For example, in the class you created in the previous code snippet you can add the following property: @@ -257,39 +257,33 @@ The `to` and `data` properties are used in the `NotificationService` in Medusa --- -## Create a Subscriber +## Subscribe with Loaders -After creating your Notification Provider Service, you must create a Subscriber that registers this Service as a notification handler of events. +After creating your Notification Provider Service, you must create a [Loader](../loaders/overview.mdx) that registers this Service as a notification handler of events. -:::note +Following the previous example, to make sure the `email-sender` Notification Provider handles the `order.placed` event, create the file `src/loaders/notification.ts` with the following content: -This section will not cover the basics of Subscribers. You can read the [Subscribers](../events/create-subscriber.md) documentation to learn more about them and how to create them. +```ts title=src/loaders/notification.ts +import { + MedusaContainer, + NotificationService, +} from "@medusajs/medusa" -::: +export default async ( + container: MedusaContainer +): Promise => { + const notificationService = container.resolve< + NotificationService + >("notificationService") -Following the previous example, to make sure the `email-sender` Notification Provider handles the `order.placed` event, create the file `src/subscribers/notification.js` with the following content: - -```ts title=src/subscribers/notification.js -class NotificationSubscriber { - constructor({ notificationService }) { - notificationService.subscribe( - "order.placed", - "email-sender" - ) - } - // ... + notificationService.subscribe( + "order.placed", + "email-sender" + ) } - -export default NotificationSubscriber ``` -This subscriber accesses the `notificationService` using dependency injection. The `notificationService` contains a `subscribe` method that accepts 2 parameters. The first one is the name of the event to subscribe to, and the second is the identifier of the Notification Provider that is subscribing to that event. - -:::tip - -Notice that the value of the `identifier` static property defined in the `EmailSenderService` is used to register the Notification Provider to handle the `order.placed` event. - -::: +This loader accesses the `notificationService` through the [MedusaContainer](../fundamentals/dependency-injection.md). The `notificationService` has a `subscribe` method that accepts 2 parameters. The first one is the name of the event to subscribe to, and the second is the identifier of the Notification Provider that's subscribing to that event. --- diff --git a/www/apps/docs/content/development/scheduled-jobs/create-deprecated.md b/www/apps/docs/content/development/scheduled-jobs/create-deprecated.md new file mode 100644 index 0000000000..840e06f147 --- /dev/null +++ b/www/apps/docs/content/development/scheduled-jobs/create-deprecated.md @@ -0,0 +1,186 @@ +--- +description: 'Learn how to create a scheduled job in Medusa. The scheduled job in this example will simply change the status of draft products to published.' +addHowToData: true +--- + +# (Deprecated) How to Create a Scheduled Job + +In this document, you’ll learn how to create a scheduled job in Medusa. + +:::note + +Following v1.18 of `@medusajs/medusa`, the approach in this guide is deprecated. It's recommended to follow [this guide](./create.md) instead. + +::: + +## Overview + +Medusa allows you to create scheduled jobs that run at specific times during your backend's lifetime. For example, you can synchronize your inventory with an Enterprise Resource Planning (ERP) system once a day. + +This guide explains how to create a scheduled job on your Medusa backend. The scheduled job in this example will simply change the status of draft products to `published`. + +--- + +## Prerequisites + +### Medusa Components + +It is assumed that you already have a Medusa backend installed and set up. If not, you can follow the [quickstart guide](../backend/install.mdx) to get started. The Medusa backend must also have an event bus module installed, which is available when using the default Medusa backend starter. + +--- + +## 1. Create a File + +Each scheduled job should reside in a [loader](../loaders/overview.mdx), which is a TypeScript or JavaScript file located under the `src/loaders` directory. + +Start by creating the `src/loaders` directory. Then, inside that directory, create the JavaScript or TypeScript file that you’ll add the scheduled job in. You can use any name for the file. + +For the example in this tutorial, you can create the file `src/loaders/publish.ts`. + +--- + +## 2. Create Scheduled Job + +To create a scheduled job, add the following code in the file you created, which is `src/loaders/publish.ts` in this example: + +```ts title=src/loaders/publish.ts +import { MedusaContainer } from "@medusajs/medusa" + +const publishJob = async ( + container: MedusaContainer, + options: Record +) => { + const jobSchedulerService = + container.resolve("jobSchedulerService") + jobSchedulerService.create( + "publish-products", + {}, + "0 0 * * *", + async () => { + // job to execute + const productService = container.resolve("productService") + const draftProducts = await productService.list({ + status: "draft", + }) + + for (const product of draftProducts) { + await productService.update(product.id, { + status: "published", + }) + } + } + ) +} + +export default publishJob +``` + +:::info + +The service taking care of background jobs was renamed in v1.7.1. If you are running a previous version, use `eventBusService` instead of `jobSchedulerService`. + +::: + +This file should export a function that accepts a `container` and `options` parameters. `container` is the dependency container that you can use to resolve services, such as the JobSchedulerService. `options` are the plugin’s options if this scheduled job is created in a plugin. + +You then resolve the `JobSchedulerService` and use the `jobSchedulerService.create` method to create the scheduled job. This method accepts four parameters: + +- The first parameter is a unique name to give to the scheduled job. In the example above, you use the name `publish-products`. +- The second parameter is an object which can be used to [pass data to the job](#pass-data-to-the-scheduled-job). +- The third parameter is the scheduled job expression pattern. In this example, it will execute the scheduled job once a day at 12 AM. +- The fourth parameter is the function to execute. This is where you add the code to execute once the scheduled job runs. In this example, you retrieve the draft products using the [ProductService](../../references/services/classes/ProductService.mdx) and update the status of each of these products to `published`. + +:::tip + +You can see examples of scheduled job expression patterns on [crontab guru](https://crontab.guru/examples.html). + +::: + +### Scheduled Job Name + +As mentioned earlier, the first parameter of the `create` method is the name of the scheduled job. By default, if another scheduled job has the same name, your custom scheduled job will replace it. + +If you want to ensure both scheduled jobs are registered and used, you can pass as a fifth parameter an options object with a `keepExisting` property set to `true`. For example: + +```ts +jobSchedulerService.create( + "publish-products", + {}, + "0 0 * * *", + async () => { + // ... + }, + { + keepExisting: true, + } +) +``` + +### Pass Data to the Scheduled Job + +To pass data to your scheduled job, add the data to the object passed as a second parameter under the `data` property. This is helpful if you use one function to handle multiple scheduled jobs. + +For example: + +```ts +jobSchedulerService.create("publish-products", { + data: { + productId, + }, + }, "0 0 * * *", async (job) => { + console.log(job.data) // {productId: 'prod_124...'} + // ... +}) +``` + +--- + +## 3. Run Medusa Backend + +:::info + +Scheduled Jobs only run while the Medusa backend is running. + +::: + +Before you run the Medusa backend, make sure to build your code that's under the `src` directory into the `dist` directory with the following command: + +```bash npm2yarn +npm run build +``` + +Then, run the following command to start your Medusa backend: + +```bash npm2yarn +npx medusa develop +``` + +If the scheduled job was registered successfully, you should see a message similar to this logged on your Medusa backend: + +```bash +Registering publish-products +``` + +Where `publish-products` is the unique name you provided to the scheduled job. + +Once it is time to run your scheduled job based on the scheduled job expression pattern, the scheduled job will run and you can see it logged on your Medusa backend. + +For example, the above scheduled job will run at 12 AM and, when it runs, you can see the following logged on your Medusa backend: + +```bash noReport +info: Processing scheduled job: publish-products +``` + +If you log anything in the scheduled job, for example using `console.log`, or if any errors are thrown, it’ll also be logged on your Medusa backend. + +:::tip + +To test the previous example out instantly, you can change the scheduled job expression pattern passed as the third parameter to `jobSchedulerService.create` to `* * * * *`. This will run the scheduled job every minute. + +::: + +--- + +## See Also + +- [Create a Plugin](../plugins/create.mdx) diff --git a/www/apps/docs/content/development/scheduled-jobs/create.md b/www/apps/docs/content/development/scheduled-jobs/create.md index 25d276e5ec..e6fe574b33 100644 --- a/www/apps/docs/content/development/scheduled-jobs/create.md +++ b/www/apps/docs/content/development/scheduled-jobs/create.md @@ -7,6 +7,12 @@ addHowToData: true In this document, you’ll learn how to create a scheduled job in Medusa. +:::tip + +v1.18 of `@medusajs/medusa` introduced a new approach to create a scheduled job. If you're looking for the old guide, you can find it [here](./create-deprecated.md). However, it's highly recommended you follow this new approach, as the old one is deprecated. + +::: + ## Overview Medusa allows you to create scheduled jobs that run at specific times during your backend's lifetime. For example, you can synchronize your inventory with an Enterprise Resource Planning (ERP) system once a day. @@ -17,72 +23,59 @@ This guide explains how to create a scheduled job on your Medusa backend. The sc ## Prerequisites -### Medusa Components - -It is assumed that you already have a Medusa backend installed and set up. If not, you can follow the [quickstart guide](../backend/install.mdx) to get started. The Medusa backend must also have an event bus module installed, which is available when using the default Medusa backend starter. +To use scheduled jobs, you must configure Redis in your Medusa backend. Learn more in the [Configurations documentation](../backend/configurations.md#redis_url). --- -## 1. Create a File +## Create Scheduled Job -Each scheduled job should reside in a [loader](../loaders/overview.mdx), which is a TypeScript or JavaScript file located under the `src/loaders` directory. +Scheduled jobs are TypeScript or JavaScript guides placed under the `src/jobs` directory. It can be created under subdirectories of `src/job` as well. -Start by creating the `src/loaders` directory. Then, inside that directory, create the JavaScript or TypeScript file that you’ll add the scheduled job in. You can use any name for the file. +The scheduled job file exports a default handler function, and the scheduled job's configurations. -For the example in this tutorial, you can create the file `src/loaders/publish.ts`. - ---- - -## 2. Create Scheduled Job - -To create a scheduled job, add the following code in the file you created, which is `src/loaders/publish.ts` in this example: +For example: ```ts title=src/loaders/publish.ts -import { AwilixContainer } from "awilix" +import { + type ProductService, + type ScheduledJobConfig, + type ScheduledJobArgs, + ProductStatus, +} from "@medusajs/medusa" -const publishJob = async ( - container: AwilixContainer, - options: Record -) => { - const jobSchedulerService = - container.resolve("jobSchedulerService") - jobSchedulerService.create( - "publish-products", - {}, - "0 0 * * *", - async () => { - // job to execute - const productService = container.resolve("productService") - const draftProducts = await productService.list({ - status: "draft", - }) - - for (const product of draftProducts) { - await productService.update(product.id, { - status: "published", - }) - } - } +export default async function handler({ + container, + data, + pluginOptions, +}: ScheduledJobArgs) { + const productService: ProductService = container.resolve( + "productService" ) + const draftProducts = await productService.list({ + status: ProductStatus.DRAFT, + }) + + for (const product of draftProducts) { + await productService.update(product.id, { + status: ProductStatus.PUBLISHED, + }) + } } -export default publishJob +export const config: ScheduledJobConfig = { + name: "publish-once-a-day", + schedule: "0 0 * * *", + data: {}, +} ``` -:::info +### Scheduled Job Configuration -The service taking care of background jobs was renamed in v1.7.1. If you are running a previous version, use `eventBusService` instead of `jobSchedulerService`. +The exported configuration object of type `ScheduledJobConfig` must include the following properties: -::: - -This file should export a function that accepts a `container` and `options` parameters. `container` is the dependency container that you can use to resolve services, such as the JobSchedulerService. `options` are the plugin’s options if this scheduled job is created in a plugin. - -You then resolve the `JobSchedulerService` and use the `jobSchedulerService.create` method to create the scheduled job. This method accepts four parameters: - -- The first parameter is a unique name to give to the scheduled job. In the example above, you use the name `publish-products`. -- The second parameter is an object which can be used to [pass data to the job](#pass-data-to-the-scheduled-job). -- The third parameter is the scheduled job expression pattern. In this example, it will execute the scheduled job once a day at 12 AM. -- The fourth parameter is the function to execute. This is where you add the code to execute once the scheduled job runs. In this example, you retrieve the draft products using the [ProductService](../../references/services/classes/ProductService.mdx) and update the status of each of these products to `published`. +- `name`: a string indicating a unique name to give to the scheduled job. If another scheduled job has the same name, your custom scheduled job will replace it. +- `schedule`: a string indicating a [cron schedule](https://crontab.guru/#0_0_*_*_*) that determines when the job should run. In this example, it will execute the scheduled job once a day at 12 AM. +- `data`: Optional data you want to pass to the job. :::tip @@ -90,46 +83,19 @@ You can see examples of scheduled job expression patterns on [crontab guru](http ::: -### Scheduled Job Name +### Scheduled Job Handler Function -As mentioned earlier, the first parameter of the `create` method is the name of the scheduled job. By default, if another scheduled job has the same name, your custom scheduled job will replace it. +The default-export of the scheduled job file is a handler function that is executed when the events specified in the exported configuration is triggerd. -If you want to ensure both scheduled jobs are registered and used, you can pass as a fifth parameter an options object with a `keepExisting` property set to `true`. For example: +The function accepts a parameter of type `ScheduledJobArgs`, which has the following properties: -```ts -jobSchedulerService.create( - "publish-products", - {}, - "0 0 * * *", - async () => { - // ... - }, - { - keepExisting: true, - } -) -``` - -### Pass Data to the Scheduled Job - -To pass data to your scheduled job, add the data to the object passed as a second parameter under the `data` property. This is helpful if you use one function to handle multiple scheduled jobs. - -For example: - -```ts -jobSchedulerService.create("publish-products", { - data: { - productId, - }, - }, "0 0 * * *", async (job) => { - console.log(job.data) // {productId: 'prod_124...'} - // ... -}) -``` +- `container`: The [dependency container](../fundamentals/dependency-injection.md) that allows you to resolve Medusa resources, such as services. +- `data`: The data passed in the [configuration object](#scheduled-job-configuration). +- `pluginOptions`: When the scheduled job is created within a plugin, this object holds the plugin's options defined in the [Medusa configurations](../backend/configurations.md). --- -## 3. Run Medusa Backend +## Test it Out :::info @@ -149,27 +115,27 @@ Then, run the following command to start your Medusa backend: npx medusa develop ``` -If the scheduled job was registered successfully, you should see a message similar to this logged on your Medusa backend: +If the scheduled job is registered successfully, you should see a message similar to this logged on your Medusa backend: ```bash -Registering publish-products +Registering publish-once-a-day ``` -Where `publish-products` is the unique name you provided to the scheduled job. +Where `publish-once-a-day` is the unique name you provided to the scheduled job. -Once it is time to run your scheduled job based on the scheduled job expression pattern, the scheduled job will run and you can see it logged on your Medusa backend. +Once it's time to run your scheduled job based on the `schedule` configuration, the scheduled job will run and you can see it logged on your Medusa backend. For example, the above scheduled job will run at 12 AM and, when it runs, you can see the following logged on your Medusa backend: ```bash noReport -info: Processing scheduled job: publish-products +info: Processing scheduled job: publish-once-a-day ``` If you log anything in the scheduled job, for example using `console.log`, or if any errors are thrown, it’ll also be logged on your Medusa backend. :::tip -To test the previous example out instantly, you can change the scheduled job expression pattern passed as the third parameter to `jobSchedulerService.create` to `* * * * *`. This will run the scheduled job every minute. +To test the previous example out instantly, you can change the value of `schedule` in the exported configuration object to `* * * * *`. This will run the scheduled job every minute. ::: diff --git a/www/apps/docs/content/development/services/create-service.mdx b/www/apps/docs/content/development/services/create-service.mdx index 2ae0862197..c106008734 100644 --- a/www/apps/docs/content/development/services/create-service.mdx +++ b/www/apps/docs/content/development/services/create-service.mdx @@ -391,15 +391,29 @@ res.json({ ### In a Subscriber -To use your custom service in a subscriber, you can have easy access to it in the subscriber’s dependencies injected to the constructor of your subscriber: +To resolve your custom service in subscriber handler functions, use the `container` property of the handler function's parameter. The `container` has a method `resolve` which accepts the registration name of the service as a parameter. + +For example: ```ts -class MySubscriber { - constructor({ postService, eventBusService }) { - this.postService = postService - } +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" + +import { PostService } from "../services/post.ts" + +export default async function postHandler({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const postService: PostService = container.resolve( + "postService" + ) + // ... } + +// ... ``` --- diff --git a/www/apps/docs/content/modules/customers/backend/send-confirmation.md b/www/apps/docs/content/modules/customers/backend/send-confirmation.md index d251b05699..95c81304ef 100644 --- a/www/apps/docs/content/modules/customers/backend/send-confirmation.md +++ b/www/apps/docs/content/modules/customers/backend/send-confirmation.md @@ -9,7 +9,9 @@ In this document, you’ll learn how to send a confirmation email to the custome ## Overview -When a customer registers, the event `customer.created` is triggered. You can then listen to this event in a subscriber to perform an action, such as send the customer a confirmation email. +When a customer registers, the event `customer.created` is triggered. You can then listen to this event in a subscriber to perform an action, such as send the customer a confirmation email. + +Alternatively, you can use subscribe a Notification Provider to the event, if the Notification Provider Service implements the logic to handle the event. This guide will explain how to create the subscriber and how to use SendGrid to send the confirmation email. SendGrid is only used to illustrate how the process works, but you’re free to use any other notification service. @@ -31,155 +33,73 @@ You can also find other available Notification provider plugins in the [Plugins --- -## Step 1: Create the Subscriber +## Method 1: Using a Subscriber -To subscribe to and handle an event, you must create a subscriber. - -:::note - -You can learn more about subscribers in the [Subscribers documentation](../../../development/events/subscribers.mdx). - -::: +To subscribe to an event, you must create a [subscriber](../../../development/events/subscribers.mdx). Create the file `src/subscribers/customer-confirmation.ts` with the following content: ```ts title=src/subscribers/customer-confirmation.ts -type InjectedDependencies = { - // TODO add necessary dependencies +import { + type SubscriberConfig, + type SubscriberArgs, + CustomerService, +} from "@medusajs/medusa" + +export default async function handleCustomerCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + // TODO: handle event } -class CustomerConfirmationSubscriber { - constructor(container: InjectedDependencies) { - // TODO subscribe to event - } +export const config: SubscriberConfig = { + event: CustomerService.Events.CREATED, + context: { + subscriberId: "customer-created-handler", + }, } - -export default CustomerConfirmationSubscriber ``` -You’ll be adding in the next step the necessary dependencies to the subscriber. +In this file, you export a configuration object indicating that the subscriber is listening to the `CustomerService.Events.CREATED` (or `customer.created`) event. -:::note - -You can learn more about dependency injection in [this documentation](../../../development/fundamentals/dependency-injection.md). - -::: - ---- - -## Step 2: Subscribe to the Event - -In this step, you’ll subscribe to the `customer.created` event to send the customer a confirmation email. - -There are two ways to do this: - -### Method 1: Using the NotificationService - -If the notification provider you’re using already implements the logic to handle this event, you can subscribe to the event using the `NotificationService`: - -```ts title=src/subscribers/customer-confirmation.ts -import { NotificationService } from "@medusajs/medusa" - -type InjectedDependencies = { - notificationService: NotificationService -} - -class CustomerConfirmationSubscriber { - constructor({ notificationService }: InjectedDependencies) { - notificationService.subscribe( - "customer.created", - "" - ) - } -} - -export default CustomerConfirmationSubscriber -``` - -Where `` is the identifier for your notification provider. - -:::note - -You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). - -::: - -### Method 2: Using the EventBusService - -If the notification provider you’re using isn’t configured to handle this event, or you want to implement some other custom logic, you can subscribe to the event using the `EventBusService`: - -```ts title=src/subscribers/customer-confirmation.ts -import { Customer, EventBusService } from "@medusajs/medusa" - -type InjectedDependencies = { - eventBusService: EventBusService -} - -class CustomerConfirmationSubscriber { - constructor({ eventBusService }: InjectedDependencies) { - eventBusService.subscribe( - "customer.created", - this.handleCustomerConfirmation - ) - } - - handleCustomerConfirmation = async (data: Customer) => { - // TODO: handle event - } -} - -export default CustomerConfirmationSubscriber -``` - -When using this method, you’ll have to handle the logic of sending the confirmation email to the customer inside the handler function, which in this case is `handleCustomerConfirmation`. - -## Step 3: Handle the Event - -The `handleCustomerConfirmation` method receives a `data` object as a parameter which is a payload emitted when the event was triggered. This object is the entire customer object. So, you can find in it fields like `first_name`, `last_name`, `email`, and more. +You also export a handler function `handleCustomerConfirmation`. In the parameter it receives, the `data` object is the payload emitted when the event was triggered, which is the entire customer object. So, you can find in it fields like `first_name`, `last_name`, `email`, and more. In this method, you should typically send an email to the customer. You can place any content in the email, such as welcoming them to your store or thanking them for registering. ### Example: Using SendGrid -For example, you can implement this subscriber to send emails using SendGrid: +For example, you can implement this subscriber to send emails using [SendGrid](../../../plugins/notifications/sendgrid.mdx): ```ts title=src/subscribers/customer-confirmation.ts -import { Customer, EventBusService } from "@medusajs/medusa" +import { + type SubscriberConfig, + type SubscriberArgs, + CustomerService, +} from "@medusajs/medusa" -type InjectedDependencies = { - eventBusService: EventBusService, - sendgridService: any +export default async function handleCustomerCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const sendGridService = container.resolve("sendgridService") + + sendGridService.sendEmail({ + templateId: "customer-confirmation", + from: "hello@medusajs.com", + to: data.email, + dynamic_template_data: { + // any data necessary for your template... + first_name: data.first_name, + last_name: data.last_name, + }, + }) } -class CustomerConfirmationSubscriber { - protected sendGridService: any - - constructor({ - eventBusService, - sendgridService, - }: InjectedDependencies) { - this.sendGridService = sendgridService - eventBusService.subscribe( - "customer.created", - this.handleCustomerConfirmation - ) - } - - handleCustomerConfirmation = async (data: Customer) => { - this.sendGridService.sendEmail({ - templateId: "customer-confirmation", - from: "hello@medusajs.com", - to: data.email, - dynamic_template_data: { - // any data necessary for your template... - first_name: data.first_name, - last_name: data.last_name, - }, - }) - } +export const config: SubscriberConfig = { + event: CustomerService.Events.CREATED, + context: { + subscriberId: "customer-created-handler", + }, } - -export default CustomerConfirmationSubscriber ``` Notice that you should replace the values in the object passed to the `sendEmail` method: @@ -188,3 +108,39 @@ Notice that you should replace the values in the object passed to the `sendEmail - `from`: Should be the from email. - `to`: Should be the customer’s email. - `data`: Should be an object holding any data that should be passed to your SendGrid email template. + +--- + +## Method 2: Using the NotificationService + +If the notification provider you’re using already implements the logic to handle this event, you can create a [Loader](../../../development/loaders/overview.mdx) to subscribe the Notification provider to the `customer.created` event. + +For example: + +```ts title=src/loaders/customer-confirmation.ts +import { + MedusaContainer, + NotificationService, +} from "@medusajs/medusa" + +export default async ( + container: MedusaContainer +): Promise => { + const notificationService = container.resolve< + NotificationService + >("notificationService") + + notificationService.subscribe( + "customer.created", + "" + ) +} +``` + +Where `` is the identifier for your notification provider. For example, `sendgrid`. + +:::note + +You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). + +::: diff --git a/www/apps/docs/content/modules/customers/storefront/implement-customer-profiles.mdx b/www/apps/docs/content/modules/customers/storefront/implement-customer-profiles.mdx index 476bea9065..8433135a8d 100644 --- a/www/apps/docs/content/modules/customers/storefront/implement-customer-profiles.mdx +++ b/www/apps/docs/content/modules/customers/storefront/implement-customer-profiles.mdx @@ -287,7 +287,7 @@ If the request has been processed successfully, it returns a `204` status code i :::note -If the customer doesn’t receive an email after this request, make sure that you’ve set up a Notification provider like [SendGrid](../../../plugins/notifications/sendgrid.mdx) successfully. You also need to add a subscriber that handles the [customer.password_reset](../../../development/events/events-list.md#customer-events) event and sends the email. +If the customer doesn’t receive an email after this request, make sure that you’ve set up a Notification provider like [SendGrid](../../../plugins/notifications/sendgrid.mdx) successfully. You also need to add a [subscriber](../../../development/events/create-subscriber.md) that handles the [customer.password_reset](../../../development/events/events-list.md#customer-events) event and sends the email. ::: diff --git a/www/apps/docs/content/modules/gift-cards/backend/send-gift-card-to-customer.md b/www/apps/docs/content/modules/gift-cards/backend/send-gift-card-to-customer.md index b63899150b..4c604773dd 100644 --- a/www/apps/docs/content/modules/gift-cards/backend/send-gift-card-to-customer.md +++ b/www/apps/docs/content/modules/gift-cards/backend/send-gift-card-to-customer.md @@ -13,7 +13,7 @@ Once the customer purchases a gift card, they should receive the code of the gif Typically, the code would be sent by email, however, you’re free to choose how you deliver the gift card code to the customer. -This document shows you how to track when a gift card has been purchased so that you can send its code to the customer. +This document shows you how to track when a gift card is purchased so that you can send its code to the customer. :::tip @@ -31,121 +31,124 @@ It's assumed that you already have a Medusa backend installed and set up. If not ### Notification Provider -To send an email or another type of notification method, you must have a notification provider installed or configured. You can either install an existing plugin or [create your own](../../../development/notification/create-notification-provider.md). +To send an email or another type of notification method, you must have a notification provider installed or configured. You can either install an [existing plugin](../../../plugins/notifications/index.mdx) or [create your own](../../../development/notification/create-notification-provider.md). --- -## Step 1: Create a Subscriber +## Methed 1: Using a Subscriber -To subscribe to and handle an event, you must create a subscriber. - -:::info - -You can learn more about subscribers in the [Subscribers](../../../development/events/subscribers.mdx) documentation. - -::: +To subscribe to and handle an event, you must create a [subscriber](../../../development/events/subscribers.mdx). Create the file `src/subscribers/gift-card.ts` with the following content: -```ts title=src/subscribers/gift-card.ts -type InjectedDependencies = { - // TODO add necessary dependencies -} - -class GiftCardSubscriber { - constructor(container: InjectedDependencies) { - // TODO subscribe to event - } -} - -export default GiftCardSubscriber -``` - -You’ll be adding in the next step the necessary dependencies to the subscriber. - -:::info - -You can learn more about [dependency injection](../../../development/fundamentals/dependency-injection.md) in this documentation. - -::: - ---- - -## Step 2: Subscribe to the Event - -In this step, you’ll subscribe to the event `gift_card.created` to send the customer a notification about their gift card. - -There are two ways to do this: - -### Method 1: Using the NotificationService - -If the notification provider you’re using already implements the logic to handle this event, you can subscribe to the event using the `NotificationService`: - -```ts title=src/subscribers/gift-card.ts -import { NotificationService } from "@medusajs/medusa" - -type InjectedDependencies = { - notificationService: NotificationService -} - -class GiftCardSubscriber { - constructor({ notificationService }: InjectedDependencies) { - notificationService.subscribe( - "gift_card.created", - "" - ) - } -} - -export default GiftCardSubscriber -``` - -Where `` is the identifier for your notification provider. For example, if you’re using SendGrid, the identifier is `sendgrid`. - -:::info - -You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). - -::: - -### Method 2: Using the EventBusService - -If the notification provider you’re using isn’t configured to handle this event, or you want to implement some other custom logic, you can subscribe to the event using the `EventBusService`: - ```ts title=src/subscribers/gift-card.ts import { - EventBusService, + type SubscriberConfig, + type SubscriberArgs, GiftCardService, } from "@medusajs/medusa" -type InjectedDependencies = { - eventBusService: EventBusService - giftCardService: GiftCardService +export default async function handleGiftCardCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + // TODO: handle event } -class GiftCardSubscriber { - giftCardService: GiftCardService - - constructor({ - eventBusService, - giftCardService, - }: InjectedDependencies) { - this.giftCardService = giftCardService - eventBusService.subscribe( - "gift_card.created", this.handleGiftCard) - } - - handleGiftCard = async (data) => { - const giftCard = await this.giftCardService.retrieve( - data.id - ) - // TODO send customer the gift card code - } +export const config: SubscriberConfig = { + event: GiftCardService.Events.CREATED, + context: { + subscriberId: "gift-card-created-handler", + }, } - -export default GiftCardSubscriber ``` -When using this method, you’ll have to handle the logic of sending the code to the customer inside the handler function, which in this case is `handleGiftCard`. +In this file, you export a configuration object indicating that the subscriber is listening to the `GiftCardService.Events.CREATED` (or `gift_card.created`) event. -The `handleGiftCard` event receives a `data` object as a parameter. This object holds the `id` property which is the ID of the gift card. You can retrieve the full gift card object using the [GiftCardService](../../../references/services/classes/GiftCardService.mdx) \ No newline at end of file +You also export a handler function `handleGiftCardCreated`. In the parameter it receives, the `data` object is the payload emitted when the event was triggered, which is an object containing the ID of the gift card in the `id` property. + +In this method, you should typically send an email to the customer. You can place any content in the email, such as the code of the gift card. + +### Example: Using SendGrid + +For example, you can implement this subscriber to send emails using [SendGrid](../../../plugins/notifications/sendgrid.mdx): + +```ts title=src/subscribers/gift-card.ts +import { + type SubscriberConfig, + type SubscriberArgs, + GiftCardService, +} from "@medusajs/medusa" + +export default async function handleGiftCardCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const sendGridService = container.resolve("sendgridService") + const giftCardService: GiftCardService = container.resolve( + "giftCardService" + ) + + const giftCard = await giftCardService.retrieve(data.id, { + relations: ["order"], + }) + + sendGridService.sendEmail({ + templateId: "gift-card-created", + from: "hello@medusajs.com", + to: giftCard.order.email, + dynamic_template_data: { + // any data necessary for your template... + code: giftCard.code, + }, + }) +} + +export const config: SubscriberConfig = { + event: GiftCardService.Events.CREATED, + context: { + subscriberId: "gift-card-created-handler", + }, +} +``` + +Notice that you should replace the values in the object passed to the `sendEmail` method: + +- `templateId`: Should be the ID of your confirmation email template in SendGrid. +- `from`: Should be the from email. +- `to`: Should be the customer’s email. +- `data`: Should be an object holding any data that should be passed to your SendGrid email template. + +--- + +## Method 2: Using the NotificationService + +If the notification provider you’re using already implements the logic to handle this event, you can create a [Loader](../../../development/loaders/overview.mdx) to subscribe the Notification provider to the `gift_card.created` event. + +For example: + +```ts title=src/loaders/gift-card-event.ts +import { + MedusaContainer, + NotificationService, +} from "@medusajs/medusa" + +export default async ( + container: MedusaContainer +): Promise => { + const notificationService = container.resolve< + NotificationService + >("notificationService") + + notificationService.subscribe( + "gift_card.created", + "" + ) +} +``` + +Where `` is the identifier for your notification provider. For example, `sendgrid`. + +:::note + +You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). + +::: diff --git a/www/apps/docs/content/modules/orders/backend/handle-order-claim-event.md b/www/apps/docs/content/modules/orders/backend/handle-order-claim-event.md index 1f3b07110e..cb0f7fba8d 100644 --- a/www/apps/docs/content/modules/orders/backend/handle-order-claim-event.md +++ b/www/apps/docs/content/modules/orders/backend/handle-order-claim-event.md @@ -9,14 +9,12 @@ In this document, you’ll learn how to handle the order claim event and send a ## Overview -When a guest customer places an order, the order is not associated with a customer. It is associated with an email address. +When a guest customer places an order, the order isn't associated with a customer. It's associated with an email address. After the customer registers, later on, they can claim that order by providing the order’s ID. When the customer requests to claim the order, the event `order-update-token.created` is triggered on the Medusa backend. This event should be used to send the customer a confirmation email. -### What You’ll Learn - In this document, you’ll learn how to handle the `order-update-token.created` event on the backend to send the customer a confirmation email. --- @@ -35,170 +33,132 @@ This document has an example using the [SendGrid](../../../plugins/notifications --- -## Step 1: Create a Subscriber +## Method 1: Using a Subscriber -To subscribe to and handle an event, you must create a subscriber. +To subscribe to an event, you must create a subscriber. -:::tip +:::note -You can learn more about subscribers in the [Subscribers](../../../development/events/subscribers.mdx) documentation. +You can learn more about subscribers in the [Subscribers documentation](../../../development/events/subscribers.mdx). ::: -Create the file `src/subscribers/claim-order.ts` with the following content: +Create the file `src/subscribers/order-claim.ts` with the following content: -```ts title=src/subscribers/claim-order.ts -type InjectedDependencies = { - // TODO add necessary dependencies +```ts title=src/subscribers/order-claim.ts +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" + +export default async function handleOrderClaim({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + // TODO: handle event } -class ClaimOrderSubscriber { - constructor(container: InjectedDependencies) { - // TODO subscribe to event - } +export const config: SubscriberConfig = { + event: "order-update-token.created", + context: { + subscriberId: "customer-created-handler", + }, } - -export default ClaimOrderSubscriber ``` -You’ll be adding in the next step the necessary dependencies to the subscriber. +In this file, you export a configuration object indicating that the subscriber is listening to the `order-update-token.created` event. -:::info +You also export a handler function `handleOrderClaim`. In the parameter it receives, the `data` object is the payload emitted when the event was triggered, which is an object of the following format: -You can learn more about [dependency injection](../../../development/fundamentals/dependency-injection.md) in this documentation. - -::: - ---- - -## Step 2: Subscribe to the Event - -In this step, you’ll subscribe to the `order-update-token.created` event to send the customer a notification about their order edit. - -There are two ways to do this: - -### Method 1: Using the NotificationService - -If the notification provider you’re using already implements the logic to handle this event, you can subscribe to the event using the `NotificationService`: - -```ts title=src/subscribers/claim-order.ts -import { NotificationService } from "@medusajs/medusa" - -type InjectedDependencies = { - notificationService: NotificationService +```ts +data = { + // string - email of order + old_email, + // string - ID of customer + new_customer_id, + // array of string - IDs of orders + orders, + // string - token used for verification + token, } - -class ClaimOrderSubscriber { - constructor({ notificationService }: InjectedDependencies) { - notificationService.subscribe( - "order-update-token.created", - "" - ) - } -} - -export default ClaimOrderSubscriber ``` -Where `` is the identifier for your notification provider. +In this method, you should typically send an email to the customer. You can place any content in the email, but should mainly include the link to confirm claiming the order. -:::info - -You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). - -::: - -### Method 2: Using the EventBusService - -If the notification provider you’re using isn’t configured to handle this event, or you want to implement some other custom logic, you can subscribe to the event using the `EventBusService`: - -```ts title=src/subscribers/claim-order.ts -import { EventBusService } from "@medusajs/medusa" - -type InjectedDependencies = { - eventBusService: EventBusService -} - -class ClaimOrderSubscriber { - constructor({ eventBusService }: InjectedDependencies) { - eventBusService.subscribe( - "order-update-token.created", - this.handleRequestClaimOrder - ) - } - - handleRequestClaimOrder = async (data) => { - // TODO: handle event - } -} - -export default ClaimOrderSubscriber -``` - -When using this method, you’ll have to handle the logic of sending the confirmation email to the customer inside the handler function, which in this case is `handleRequestClaimOrder`. - -The `handleRequestClaimOrder` event receives a `data` object as a parameter. This object holds the following properties: - -1. `old_email`: The email associated with the orders. -2. `new_customer_id`: The ID of the customer claiming the orders. -3. `orders`: An array of the order IDs that the customer is requesting to claim. -4. `token`: A verification token. This token is used to later verify the claim request and associate the order with the customer. - -In this method, you should typically send an email to the customer’s old email. In the email, you should link to a page in your storefront and pass the `token` as a parameter. - -The page would then send a request to the backend to verify that the `token` is valid and associate the order with the customer. You can read more about how to implement this in your storefront in [this documentation](../storefront/implement-claim-order.mdx). - ---- - -## Example: Using SendGrid +### Example: Using SendGrid For example, you can implement this subscriber to send emails using SendGrid: - +```ts title=src/subscribers/order-claim.ts +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" -```ts title=src/subscribers/claim-order.ts -import { EventBusService } from "@medusajs/medusa" +export default async function handleOrderClaim({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const sendGridService = container.resolve("sendgridService") -type InjectedDependencies = { - eventBusService: EventBusService, - sendgridService: any + sendGridService.sendEmail({ + templateId: "order-claim-confirmation", + from: "hello@medusajs.com", + to: data.old_email, + dynamic_template_data: { + link: + `http://example.com/confirm-order-claim/${data.token}`, + // other data... + }, + }) } -class ClaimOrderSubscriber { - protected sendGridService: any - - constructor({ - eventBusService, - sendgridService, - }: InjectedDependencies) { - this.sendGridService = sendgridService - eventBusService.subscribe( - "order-update-token.created", - this.handleRequestClaimOrder - ) - } - - - handleRequestClaimOrder = async (data) => { - this.sendGridService.sendEmail({ - templateId: "order-claim-confirmation", - from: "hello@medusajs.com", - to: data.old_email, - dynamic_template_data: { - link: `http://example.com/confirm-order-claim/${data.token}`, - // other data... - }, - }) - } +export const config: SubscriberConfig = { + event: "order-update-token.created", + context: { + subscriberId: "customer-created-handler", + }, } - -export default ClaimOrderSubscriber ``` Notice how the `token` is passed to the storefront link as a parameter. --- +## Method 2: Using the NotificationService + +If the notification provider you’re using already implements the logic to handle this event, you can create a [Loader](../../../development/loaders/overview.mdx) to subscribe the Notification provider to the `order-update-token.created` event. + +For example: + +```ts title=src/loaders/order-claim.ts +import { + MedusaContainer, + NotificationService, +} from "@medusajs/medusa" + +export default async ( + container: MedusaContainer +): Promise => { + const notificationService = container.resolve< + NotificationService + >("notificationService") + + notificationService.subscribe( + "order-update-token.created", + "" + ) +} +``` + +Where `` is the identifier for your notification provider. For example, `sendgrid`. + +:::note + +You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). + +::: + +--- + ## See Also - [Implement claim-order flow in your storefront](../storefront/implement-claim-order.mdx) diff --git a/www/apps/docs/content/modules/orders/backend/send-order-confirmation.md b/www/apps/docs/content/modules/orders/backend/send-order-confirmation.md index 6daba82aa3..4400afd026 100644 --- a/www/apps/docs/content/modules/orders/backend/send-order-confirmation.md +++ b/www/apps/docs/content/modules/orders/backend/send-order-confirmation.md @@ -11,22 +11,12 @@ In this document, you’ll learn how to send an order confirmation email to the When an order is placed, the `order.placed` event is triggered. You can listen to this event in a subscriber to perform an action, such as send the customer an order confirmation email. -This guide explains how to create the subscriber and how to use SendGrid to send the confirmation email. SendGrid is only used to illustrate how the process works, but you’re free to use any other notification service. - -:::note - -SendGrid is already configured to send emails when an order has been placed. So, by installing and configuring the plugin, you don't need to actually handle sending the order confirmation email. It's used as an example here to illustrate the process only. - -::: +This guide explains how you can listen to the `order.placed` event to send an email to the customer. --- ## Prerequisites -### Medusa Backend - -It’s assumed you already have the Medusa backend installed. If not, you can either use the [create-medusa-app command](../../../create-medusa-app.mdx) to install different Medusa tools, including the backend, or [install the backend only](../../../development/backend/install.mdx). - ### Event Bus Module The event bus module trigger the event to the listening subscribers. So, it’s required to have an event bus module installed and configured on your Medusa backend. @@ -41,221 +31,128 @@ You can also find other available Notification provider plugins in the [Plugins --- -## Step 1: Create the Subscriber +## Method 1: Using a Subscriber -To subscribe to and handle an event, you must create a subscriber. +To subscribe to an event, you must create a [subscriber](../../../development/events/subscribers.mdx). -:::note +Create the file `src/subscribers/order-placed.ts` with the following content: -You can learn more about subscribers in the [Subscribers documentation](../../../development/events/subscribers.mdx). +```ts title=src/subscribers/order-placed.ts +import { + type SubscriberConfig, + type SubscriberArgs, + OrderService, +} from "@medusajs/medusa" -::: - -Create the file `src/subscribers/order-confirmation.ts` with the following content: - -```ts title=src/subscribers/order-confirmation.ts -type InjectedDependencies = { - // TODO add necessary dependencies +export default async function handleOrderPlaced({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + // TODO: handle event } -class OrderConfirmationSubscriber { - constructor(container: InjectedDependencies) { - // TODO subscribe to event - } +export const config: SubscriberConfig = { + event: OrderService.Events.PLACED, + context: { + subscriberId: "order-placed-handler", + }, } - -export default OrderConfirmationSubscriber ``` -You’ll be adding in the next step the necessary dependencies to the subscriber. +In this file, you export a configuration object indicating that the subscriber is listening to the `OrderService.Events.PLACED` (or `order.placed`) event. + +You also export a handler function `handleCustomerConfirmation`. In the parameter it receives, the `data` object is the payload emitted when the event was triggered, which is an object that includes the ID of the order in the `id` property. + +In this method, you should typically send an email to the customer. You can place any content in the email, such as the order's items and total. + +### Example: Using SendGrid :::note -You can learn more about dependency injection in [this documentation](../../../development/fundamentals/dependency-injection.md). +This example is only used to illustrate how the functionality can be implemented. The SendGrid plugin automatically handles sending an email when an order is placed once you install and configure the plugin in your backend. ::: +For example, you can implement this subscriber to send emails using [SendGrid](../../../plugins/notifications/sendgrid.mdx): + +```ts title=src/subscribers/order-placed.ts +import { + type SubscriberConfig, + type SubscriberArgs, + OrderService, +} from "@medusajs/medusa" + +export default async function handleOrderPlaced({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const sendGridService = container.resolve("sendgridService") + const orderService: OrderService = container.resolve( + "orderService" + ) + + const order = await orderService.retrieve(data.id, { + // you can include other relations as well + relations: ["items"], + }) + + sendGridService.sendEmail({ + templateId: "order-confirmation", + from: "hello@medusajs.com", + to: order.email, + dynamic_template_data: { + // any data necessary for your template... + items: order.items, + status: order.status, + }, + }) +} + +export const config: SubscriberConfig = { + event: OrderService.Events.PLACED, + context: { + subscriberId: "order-placed-handler", + }, +} +``` + +Notice that you should replace the values in the object passed to the `sendEmail` method: + +- `templateId`: Should be the ID of your confirmation email template in SendGrid. +- `from`: Should be the from email. +- `to`: Should be the customer’s email. +- `data`: Should be an object holding any data that should be passed to your SendGrid email template. + --- -## Step 2: Subscribe to the Event +## Method 2: Using the NotificationService -In this step, you’ll subscribe to the `order.placed` event to send the customer an order confirmation email. +If the notification provider you’re using already implements the logic to handle this event, you can create a [Loader](../../../development/loaders/overview.mdx) to subscribe the Notification provider to the `order.placed` event. -There are two ways to do this: +For example: -### Method 1: Using the NotificationService +```ts title=src/loaders/customer-confirmation.ts +import { + MedusaContainer, + NotificationService, +} from "@medusajs/medusa" -If the notification provider you’re using already implements the logic to handle this event, you can subscribe to the event using the `NotificationService`: +export default async ( + container: MedusaContainer +): Promise => { + const notificationService = container.resolve< + NotificationService + >("notificationService") -```ts title=src/subscribers/order-confirmation.ts -import { NotificationService } from "@medusajs/medusa" - -type InjectedDependencies = { - notificationService: NotificationService + notificationService.subscribe( + "order.placed", + "" + ) } - -class OrderConfirmationSubscriber { - constructor({ notificationService }: InjectedDependencies) { - notificationService.subscribe( - "order.placed", - "" - ) - } -} - -export default OrderConfirmationSubscriber ``` -Where `` is the identifier for your notification provider. +Where `` is the identifier for your notification provider. For example, `sendgrid`. :::note You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). ::: - -### Method 2: Using the EventBusService - -If the notification provider you’re using isn’t configured to handle this event, or you want to implement some other custom logic, you can subscribe to the event using the `EventBusService`: - -```ts title=src/subscribers/order-confirmation.ts -import { EventBusService } from "@medusajs/medusa" - -type InjectedDependencies = { - eventBusService: EventBusService -} - -class OrderConfirmationSubscriber { - constructor({ eventBusService }: InjectedDependencies) { - eventBusService.subscribe( - "order.placed", - this.handleOrderConfirmation - ) - } - - handleOrderConfirmation = async ( - data: Record - ) => { - // TODO: handle event - } -} - -export default OrderConfirmationSubscriber -``` - -When using this method, you’ll have to handle the logic of sending the order confirmation email to the customer inside the handler function, which in this case is `handleOrderConfirmation`. - -## Step 3: Handle the Event - -The `handleOrderConfirmation` event receives a `data` object as a parameter. This object holds two properties: - -- `id`: the ID of the order that was placed. -- `no_notification`: a boolean value indicating whether the customer should receive notifications about the order or not. - -In this method, you should typically send an email to the customer if `no_notification` is enabled. - -To retrieve the order's details, you can add the `OrderService` into `InjectedDependencies` and use it within `handleOrderConfirmation`. For example: - -```ts title=src/subscribers/order-confirmation.ts -import { EventBusService, OrderService } from "@medusajs/medusa" - -type InjectedDependencies = { - eventBusService: EventBusService - orderService: OrderService -} - -class OrderConfirmationSubscriber { - protected readonly orderService_: OrderService - - constructor({ - eventBusService, - orderService, - }: InjectedDependencies) { - this.orderService_ = orderService - eventBusService.subscribe( - "order.placed", - this.handleOrderConfirmation - ) - } - - handleOrderConfirmation = async ( - data: Record - ) => { - const order = await this.orderService_.retrieve(data.id, { - // you can include other relations as well - relations: ["items"], - }) - // TODO: handle event - } -} - -export default OrderConfirmationSubscriber -``` - -After retrieving the order, you can add the logic necessary to send the email. In the email, you can include any content you want. For example, you can show the order's items or the order's status. - -### Example: Using SendGrid - -:::note - -This example is only used to illustrate how the functionality can be implemented. As mentioned in the introduction, there's actually no need to implement this subscriber if you have the SendGrid plugin installed and configured, as it will automatically handle it. - -::: - -For example, you can implement this subscriber to send emails using SendGrid: - -```ts title=src/subscribers/order-confirmation.ts -import { EventBusService, OrderService } from "@medusajs/medusa" - -type InjectedDependencies = { - eventBusService: EventBusService - orderService: OrderService - sendgridService: any -} - -class OrderConfirmationSubscriber { - protected readonly orderService_: OrderService - protected readonly sendgridService_: any - - constructor({ - eventBusService, - orderService, - sendgridService, - }: InjectedDependencies) { - this.orderService_ = orderService - this.sendgridService_ = sendgridService - eventBusService.subscribe( - "order.placed", - this.handleOrderConfirmation - ) - } - - handleOrderConfirmation = async ( - data: Record - ) => { - const order = await this.orderService_.retrieve(data.id, { - // you can include other relations as well - relations: ["items"], - }) - this.sendgridService_.sendEmail({ - templateId: "order-confirmation", - from: "hello@medusajs.com", - to: order.email, - dynamic_template_data: { - // any data necessary for your template... - items: order.items, - status: order.status, - }, - }) - } -} - -export default OrderConfirmationSubscriber -``` - -Notice that you should replace the values in the object passed to the `sendEmail` method: - -- `templateId`: Should be the ID of your order confirmation email template in SendGrid. -- `from`: Should be the from email. -- `to`: Should be the email associated with the order. -- `data`: Should be an object holding any data that should be passed to your SendGrid email template. diff --git a/www/apps/docs/content/modules/users/backend/send-invite.md b/www/apps/docs/content/modules/users/backend/send-invite.md index 70bd9fb724..59cc566af7 100644 --- a/www/apps/docs/content/modules/users/backend/send-invite.md +++ b/www/apps/docs/content/modules/users/backend/send-invite.md @@ -31,160 +31,81 @@ You can also find other available Notification provider plugins in the [Plugins --- -## Step 1: Create the Subscriber +## Method 1: Using a Subscriber -To subscribe to and handle an event, you must create a subscriber. +To subscribe to an event, you must create a [subscriber](../../../development/events/subscribers.mdx). -:::tip +Create the file `src/subscribers/invite-created.ts` with the following content: -You can learn more about subscribers in the [Subscribers documentation](../../../development/events/subscribers.mdx). +```ts title=src/subscribers/invite-created.ts +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" -::: - -Create the file `src/subscribers/invite.ts` with the following content: - -```ts title=src/subscribers/invite.ts -type InjectedDependencies = { - // TODO add necessary dependencies +export default async function handleInviteCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + // TODO: handle event } -class InviteSubscriber { - constructor(container: InjectedDependencies) { - // TODO subscribe to event - } +export const config: SubscriberConfig = { + event: "invite.created", + context: { + subscriberId: "invite-created-handler", + }, } - -export default InviteSubscriber ``` -You’ll be adding in the next step the necessary dependencies to the subscriber. +In this file, you export a configuration object indicating that the subscriber is listening to the `invite.created` event. -:::tip +You also export a handler function `handleInviteCreated`. In the parameter it receives, the `data` object is the payload emitted when the event was triggered, which is an object of the following format: -You can learn more about dependency injection in [this documentation](../../../development/fundamentals/dependency-injection.md). - -::: - ---- - -## Step 2: Subscribe to the Event - -In this step, you’ll subscribe to the `invite.created` event to send the user the invitation email. - -There are two ways to do this: - -### Method 1: Using the NotificationService - -If the notification provider you’re using already implements the logic to handle this event, you can subscribe to the event using the `NotificationService`: - -```ts title=src/subscribers/invite.ts -import { NotificationService } from "@medusajs/medusa" - -type InjectedDependencies = { - notificationService: NotificationService +```ts +{ + // string - ID of invite + id + // string - token generated to validate the invited user + token, + // string - email of invited user + user_email } - -class InviteSubscriber { - constructor({ notificationService }: InjectedDependencies) { - notificationService.subscribe( - "invite.created", - "" - ) - } -} - -export default InviteSubscriber ``` -Where `` is the identifier for your notification provider. - -:::tip - -You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). - -::: - -### Method 2: Using the EventBusService - -If the notification provider you’re using isn’t configured to handle this event, or you want to implement some other custom logic, you can subscribe to the event using the `EventBusService`: - -```ts title=src/subscribers/invite.ts -import { EventBusService } from "@medusajs/medusa" - -type InjectedDependencies = { - eventBusService: EventBusService -} - -class InviteSubscriber { - constructor({ eventBusService }: InjectedDependencies) { - eventBusService.subscribe( - "invite.created", - this.handleInvite - ) - } - - handleInvite = async (data: Record) => { - // TODO: handle event - } -} - -export default InviteSubscriber -``` - -When using this method, you’ll have to handle the logic of sending the invitation email inside the handler function, which in this case is `handleInvite`. - ---- - -## Step 3: Handle the Event - -The `handleInvite` method receives a `data` object as a parameter which is a payload emitted when the event was triggered. This object has the following properties: - -- `id`: a string indicating the ID of the invite. -- `token`: a string indicating the token of the invite. This token is useful to pass along to a frontend link that can be used to accept the invite. -- `user_email`: a string indicating the email of the invited user. - -In this method, you should typically send an email to the invited user. You can place any content in the email, but typically you would include a link to your frontend that allows the invited user to enter their details and accept the invite. +In this method, you should typically send an email to the user. You can place any content in the email, but should mainly include the invite token. ### Example: Using SendGrid For example, you can implement this subscriber to send emails using SendGrid: ```ts title=src/subscribers/invite.ts -import { EventBusService } from "@medusajs/medusa" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/medusa" -type InjectedDependencies = { - eventBusService: EventBusService - sendgridService: any +export default async function handleInviteCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const sendGridService = container.resolve("sendgridService") + + sendGridService.sendEmail({ + templateId: "send-invite", + from: "hello@medusajs.com", + to: data.user_email, + dynamic_template_data: { + // any data necessary for your template... + token: data.token, + }, + }) } -class InviteSubscriber { - protected sendGridService: any - - constructor({ - eventBusService, - sendgridService, - }: InjectedDependencies) { - this.sendGridService = sendgridService - eventBusService.subscribe( - "invite.created", - this.handleInvite - ) - } - - handleInvite = async (data: Record) => { - this.sendGridService.sendEmail({ - templateId: "send-invite", - from: "hello@medusajs.com", - to: data.user_email, - dynamic_template_data: { - // any data necessary for your template... - token: data.token, - }, - }) - } +export const config: SubscriberConfig = { + event: "invite.created", + context: { + subscriberId: "invite-created-handler", + }, } - -export default InviteSubscriber ``` Notice that you should replace the values in the object passed to the `sendEmail` method: @@ -193,3 +114,39 @@ Notice that you should replace the values in the object passed to the `sendEmai - `from`: Should be the from email. - `to`: Should be the invited user’s email. - `data`: Should be an object holding any data that should be passed to your SendGrid email template. In the example above, you pass the token, which you can use in the SendGrid template to format the frontend link (for example, `/invite?token={{token}}`, where `` is your frontend’s hostname.) + +--- + +## Method 2: Using the NotificationService + +If the notification provider you’re using already implements the logic to handle this event, you can create a [Loader](../../../development/loaders/overview.mdx) to subscribe the Notification provider to the `invite.created` event. + +For example: + +```ts title=src/loaders/customer-confirmation.ts +import { + MedusaContainer, + NotificationService, +} from "@medusajs/medusa" + +export default async ( + container: MedusaContainer +): Promise => { + const notificationService = container.resolve< + NotificationService + >("notificationService") + + notificationService.subscribe( + "invite.created", + "" + ) +} +``` + +Where `` is the identifier for your notification provider. For example, `sendgrid`. + +:::note + +You can learn more about handling events with the Notification Service using [this documentation](../../../development/notification/create-notification-provider.md). + +::: diff --git a/www/apps/docs/content/plugins/analytics/segment.md b/www/apps/docs/content/plugins/analytics/segment.md index 4f9c44f666..5dde673c20 100644 --- a/www/apps/docs/content/plugins/analytics/segment.md +++ b/www/apps/docs/content/plugins/analytics/segment.md @@ -153,43 +153,41 @@ If the data is not appearing on the destination, the issue is related to your co In some cases, you might want to track more events or custom events. You can do that using the `SegmentService` provided by the Segment Plugin. -For example, you can add the following subscriber to listen to the `customer.created` event and add tracking for every customer created: +For example, you can add the following [subscriber](../../development/events/subscribers.mdx) to listen to the `customer.created` event and add tracking for every customer created: -```jsx title=src/subscribers/customer.ts -class CustomerSubscriber { - constructor({ segmentService, eventBusService }) { - this.segmentService = segmentService +```ts title=src/subscribers/customer.ts +import { + type SubscriberConfig, + type SubscriberArgs, + CustomerService, +} from "@medusajs/medusa" - eventBusService.subscribe( - "customer.created", - this.handleCustomer - ) - } +export default async function handleCustomerCreated({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const segmentService = container.resolve("segmentService") - handleCustomer = async (data) => { - const customerData = data - delete customerData["password_hash"] + const customerData = data + delete customerData["password_hash"] - this.segmentService.track({ - event: "Customer Created", - userId: data.id, - properties: customerData, - }) - } + this.segmentService.track({ + event: "Customer Created", + userId: data.id, + properties: customerData, + }) } -export default CustomerSubscriber +export const config: SubscriberConfig = { + event: CustomerService.Events.CREATED, + context: { + subscriberId: "customer-created-handler", + }, +} ``` -You resolve the `SegmentService` using dependency injection. Then, when the `customer.created` event is triggered, you use the `track` method available in the `SegmentService` to send tracking data to Segment. +In the handler function, you resolve the `SegmentService` using the `container` of type [MedusaContainer](../../development/fundamentals/dependency-injection.md). Then, you use the `track` method of the `SegmentService` to send tracking data to Segment. -:::info - -Services can be resolved and used in Subscribers, API Routes, and other Services. Learn [how to resolve services in the Services documentation](../../development/services/create-service.mdx#using-your-custom-service). - -::: - -`track` accepts an object of data, where the keys `event` and `userId` are required. Instead of `userId`, you can use `anonymousId` to pass an anonymous user ID. +The `track` method accepts an object of data, where the keys `event` and `userId` are required. Instead of `userId`, you can use `anonymousId` to pass an anonymous user ID. If you want to pass additional data to Segment, pass them under the `properties` object key. @@ -199,8 +197,6 @@ The `SegmentService` also provides the method `identify` to tie a user to their After adding the above subscriber, run your backend again if it isn’t running and create a customer using the REST APIs or one of the Medusa storefronts. If you check the Debugger in your Segment source, you should see a new event “Customer Created” tracked. If you click on it, you’ll see the data you passed to the `track` method. -![The customer created event is recorded on the Segment source](https://res.cloudinary.com/dza7lstvk/image/upload/v1668000759/Medusa%20Docs/Segment/4LD41xE_qungdw.png) - --- ## See Also diff --git a/www/apps/docs/content/plugins/cms/contentful.mdx b/www/apps/docs/content/plugins/cms/contentful.mdx index fb0a20cf08..8808994f55 100644 --- a/www/apps/docs/content/plugins/cms/contentful.mdx +++ b/www/apps/docs/content/plugins/cms/contentful.mdx @@ -405,8 +405,11 @@ npm install --save-dev contentful-migration Finally, create a [loader](../../development/loaders/overview.mdx) at `src/loaders/index.ts` with the following content: ```ts title=src/loaders/index.ts -import { ConfigModule, StoreService } from "@medusajs/medusa" -import { AwilixContainer } from "awilix" +import { + ConfigModule, + StoreService, + MedusaContainer, +} from "@medusajs/medusa" import { runMigration } from "contentful-migration" import { productMigration, @@ -434,7 +437,7 @@ type ContentfulPluginType = { } export default async ( - container: AwilixContainer, + container: MedusaContainer, config: ConfigModule ): Promise => { // ensure that migration only runs once diff --git a/www/apps/docs/content/plugins/notifications/slack.md b/www/apps/docs/content/plugins/notifications/slack.md index 1b93b29ad4..9ce5b66db0 100644 --- a/www/apps/docs/content/plugins/notifications/slack.md +++ b/www/apps/docs/content/plugins/notifications/slack.md @@ -18,7 +18,7 @@ The notification contains details about the order including: - Order totals including Tax amount. - Promotion details if there are any (this is optional and can be turned off). -The plugin registers a subscriber to the `order.placed` event. When an order is placed, the subscriber handler method uses the ID of the order to retrieve order details mentioned above. +The plugin registers a subscriber to the `order.placed` event. When an order is placed, the subscriber handler function uses the ID of the order to retrieve order details mentioned above. Then, the order notification is sent to Slack using Webhooks. So, you'll need to create a Slack App, add it into your workspace, and activate Incoming Webhooks. diff --git a/www/apps/docs/content/plugins/notifications/twilio-sms.md b/www/apps/docs/content/plugins/notifications/twilio-sms.md index a46eddb6db..1dad43e1df 100644 --- a/www/apps/docs/content/plugins/notifications/twilio-sms.md +++ b/www/apps/docs/content/plugins/notifications/twilio-sms.md @@ -73,63 +73,56 @@ const plugins = [ ## Example Usage of the Plugin -This plugin adds the service `twilioSmsService` to your Medusa backend. To send SMS using it, all you have to do is resolve it in your file as explained in the [Services](../../development/services/create-service.mdx#using-your-custom-service) documentation. +This plugin adds the service `twilioSmsService` to your Medusa backend. To send SMS using it, all you have to do is resolve it in your file as explained in the [dependency injection](../../development/fundamentals/dependency-injection.md) documentation. -In this example, you’ll create a subscriber that listens to the `order.placed` event and sends an SMS to the customer to confirm their order. +In this example, you’ll create a [subscriber](../../development/events/subscribers.mdx) that listens to the `order.placed` event and sends an SMS to the customer to confirm their order. :::tip -For this example to work, you'll need to have an event bus module installed and configured, which should be available by default. +For this example to work, you'll need to have an [event bus module](../../development/events/index.mdx) installed and configured, which should be available by default. ::: -Create the file `src/services/sms.js` in your Medusa backend with the following content: +Create the file `src/subscriber/sms.ts` in your Medusa backend with the following content: -```js title=src/services/sms.js -class SmsSubscriber { - constructor({ - twilioSmsService, - orderService, - eventBusService, - }) { - this.twilioSmsService_ = twilioSmsService - this.orderService = orderService +```ts title=src/subscriber/sms.ts +import { + type SubscriberConfig, + type SubscriberArgs, + OrderService, +} from "@medusajs/medusa" - eventBusService.subscribe("order.placed", this.sendSMS) - } +export default async function handleOrderPlaced({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const twilioSmsService = container.resolve("twilioSmsService") + const orderService: OrderService = container.resolve( + "orderService" + ) - sendSMS = async (data) => { - const order = await this.orderService.retrieve(data.id, { - relations: ["shipping_address"], + const order = await orderService.retrieve(data.id, { + relations: ["shipping_address"], + }) + + if (order.shipping_address.phone) { + twilioSmsService.sendSms({ + to: order.shipping_address.phone, + body: "We have received your order #" + data.id, }) - - if (order.shipping_address.phone) { - this.twilioSmsService_.sendSms({ - to: order.shipping_address.phone, - body: "We have received your order #" + data.id, - }) - } } } -export default SmsSubscriber +export const config: SubscriberConfig = { + event: OrderService.Events.PLACED, + context: { + subscriberId: "order-placed-handler", + }, +} ``` -In the `constructor`, you resolve the `twilioSmsService` and `orderService` using dependency injection to use it later in the `sendSMS` method. +In the handler function, you resolve the `twilioSmsService` and `orderService` using `container` of type [MedusaContainer](../../development/fundamentals/dependency-injection.md). You then retrieve the order's details, and send an SMS to the customer based on the phone number in their shipping address. -You also subscribe to the event `order.placed` and sets the event handler to be `sendSMS`. - -In `sendSMS`, you first retrieve the order with its relation to `shipping_address` which contains a `phone` field. If the phone is set, you send an SMS to the customer using the method `sendSms` in the `twilioSmsService`. - -This method accepts an object of parameters. These parameters are based on Twilio’s SMS APIs. You can check their [API documentation](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource) for more fields that you can add. - -If you create an order now on your storefront, you should receive a message from Twilio on the phone number you entered in the shipping address. - -:::tip - -If you don’t have a storefront set up yet, you can install the [Next.js Starter Template](../../starters/nextjs-medusa-starter.mdx). - -::: +The `sendSms` method of the Twilio service accepts an object of parameters. These parameters are based on Twilio’s SMS APIs. You can check their [API documentation](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource) for more fields that you can add. :::caution @@ -137,8 +130,6 @@ If you’re on a Twilio trial make sure that the phone number you entered on che ::: -![Twilio Dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1668001219/Medusa%20Docs/Stripe/MXtQMiL_kb7kxe.png) - --- ## See Also diff --git a/www/apps/docs/content/plugins/other/restock-notifications.md b/www/apps/docs/content/plugins/other/restock-notifications.md index 8b6eb0e780..e61ba06f33 100644 --- a/www/apps/docs/content/plugins/other/restock-notifications.md +++ b/www/apps/docs/content/plugins/other/restock-notifications.md @@ -28,7 +28,7 @@ Before you follow this guide, you must have a Medusa backend installed. If not, ### Event-Bus Module -To trigger events to the subscribed handler methods, you must have an event-bus module installed. For development purposes, you can use the [Local module](../../development/events/modules/local.md) which should be enabled by default in your Medusa backend. +To trigger events to the subscribed handler functions, you must have an event-bus module installed. For development purposes, you can use the [Local module](../../development/events/modules/local.md) which should be enabled by default in your Medusa backend. For production, it's recommended to use the [Redis module](../../development/events/modules/redis.md). @@ -114,64 +114,49 @@ The SendGrid plugin already listens to and handles the `restock-notification.res ::: -Here's an example of a subscriber that listens to the `restock-notification.restocked` event and uses the [SendGrid plugin](../notifications/sendgrid.mdx) to send the subscribed customers an email: +Here's an example of a [subscriber](../../development/events/subscribers.mdx) that listens to the `restock-notification.restocked` event and uses the [SendGrid plugin](../notifications/sendgrid.mdx) to send the subscribed customers an email: ```ts title=src/subscribers/restock-notification.ts import { - EventBusService, + type SubscriberConfig, + type SubscriberArgs, ProductVariantService, } from "@medusajs/medusa" -type InjectedDependencies = { - eventBusService: EventBusService, - sendgridService: any - productVariantService: ProductVariantService +export default async function handleRestockNotification({ + data, eventName, container, pluginOptions, +}: SubscriberArgs>) { + const sendgridService = container.resolve("sendgridService") + const productVariantService: ProductVariantService = + container.resolve("productVariantService") + + // retrieve variant + const variant = await this.productVariantService_.retrieve( + data.variant_id + ) + + this.sendGridService_.sendEmail({ + templateId: "restock-notification", + from: "hello@medusajs.com", + to: data.emails, + dynamic_template_data: { + // any data necessary for your template... + variant, + }, + }) } -class RestockNotificationSubscriber { - protected sendGridService_: any - protected productVariantService_: ProductVariantService - - constructor({ - eventBusService, - sendgridService, - productVariantService, - }: InjectedDependencies) { - this.sendGridService_ = sendgridService - this.productVariantService_ = productVariantService - eventBusService.subscribe( - "restock-notification.restocked", - this.handleRestockNotification - ) - } - - handleRestockNotification = async ({ - variant_id, - emails, - }) => { - // retrieve variant - const variant = await this.productVariantService_.retrieve( - variant_id - ) - - this.sendGridService_.sendEmail({ - templateId: "restock-notification", - from: "hello@medusajs.com", - to: emails, - dynamic_template_data: { - // any data necessary for your template... - variant, - }, - }) - } +export const config: SubscriberConfig = { + event: "restock-notification.restocked", + context: { + subscriberId: "restock-handler", + }, } - -export default RestockNotificationSubscriber ``` -Handler methods subscribed to the `restock-notification.restocked` event, which in this case is the `handleRestockNotification` method, receive the following object data payload as a parameter: +The handler function receives in the `data` property of the first parameter the following properties: - `variant_id`: The ID of the variant that has been restocked. - `emails`: An array of strings indicating the email addresses subscribed to the restocked variant. Here, you pass it along to the SendGrid plugin directly to send the email to everyone subscribed. If necessary, you can also retrieve the customer of that email using the `CustomerService`'s [retrieveByEmail](../../references/services/classes/CustomerService.mdx#retrievebyemail) method. -In the method, you retrieve the variant by its ID using the `ProductVariantService`, then send the email using the SendGrid plugins' `SendGridService`. +In the handler function, you retrieve the variant by its ID using the `ProductVariantService`, then send the email using the SendGrid plugins' `SendGridService`. diff --git a/www/apps/docs/content/recipes/commerce-automation.mdx b/www/apps/docs/content/recipes/commerce-automation.mdx index 32522bd9b4..bbd5ed3255 100644 --- a/www/apps/docs/content/recipes/commerce-automation.mdx +++ b/www/apps/docs/content/recipes/commerce-automation.mdx @@ -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>) { + 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", + }, + } ``` @@ -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 ): Promise => { const logger = container.resolve("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>) { + 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", + }, + } ``` @@ -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>) { + 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", + }, + } ``` diff --git a/www/apps/docs/content/recipes/digital-products.mdx b/www/apps/docs/content/recipes/digital-products.mdx index b1326b13c0..7bb1979483 100644 --- a/www/apps/docs/content/recipes/digital-products.mdx +++ b/www/apps/docs/content/recipes/digital-products.mdx @@ -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>) { + 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 - ) => { - 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`. diff --git a/www/apps/docs/content/recipes/marketplace.mdx b/www/apps/docs/content/recipes/marketplace.mdx index 3bab9d1e5a..290595550b 100644 --- a/www/apps/docs/content/recipes/marketplace.mdx +++ b/www/apps/docs/content/recipes/marketplace.mdx @@ -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. { - // TODO perform functionality - } + export default async function handleOrderPlaced({ + data, eventName, container, pluginOptions, + }: SubscriberArgs>) { + // 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. diff --git a/www/apps/docs/content/recipes/oms.mdx b/www/apps/docs/content/recipes/oms.mdx index 701fc81143..8ac6d6d1dd 100644 --- a/www/apps/docs/content/recipes/oms.mdx +++ b/www/apps/docs/content/recipes/oms.mdx @@ -75,7 +75,7 @@ Medusa uses the fulfillment service whenever a fulfillment action is performed, ![Fulfilling orders with Medusa OMS](https://res.cloudinary.com/dza7lstvk/image/upload/v1695115456/Medusa%20Docs/Diagrams/oms-fulfillment_kfi1iz.jpg) -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. )} + {customProps?.sidebar_badge && ( + + )} diff --git a/www/apps/docs/src/theme/DocSidebarItem/Html/index.tsx b/www/apps/docs/src/theme/DocSidebarItem/Html/index.tsx index 3ecf710556..57a1542acc 100644 --- a/www/apps/docs/src/theme/DocSidebarItem/Html/index.tsx +++ b/www/apps/docs/src/theme/DocSidebarItem/Html/index.tsx @@ -29,8 +29,9 @@ export default function DocSidebarItemHtml({ customProps?.sidebar_is_group_divider && "sidebar-group-divider", customProps?.sidebar_is_divider_line && "sidebar-divider-line", customProps?.sidebar_is_back_link && "sidebar-back-link", - customProps?.sidebar_is_soon && - "sidebar-soon-link sidebar-badge-wrapper", + (customProps?.sidebar_is_soon || customProps?.sidebar_badge) && + "sidebar-badge-wrapper", + customProps?.sidebar_is_soon && "sidebar-soon-link", !customProps?.sidebar_is_title && "[&_.sidebar-item-icon]:w-[20px] [&_.sidebar-item-icon]:h-[20px]", !customProps?.sidebar_is_title && @@ -55,6 +56,12 @@ export default function DocSidebarItemHtml({ Soon )} + {customProps?.sidebar_badge && ( + + )} ) } diff --git a/www/apps/docs/src/theme/DocSidebarItem/Link/index.tsx b/www/apps/docs/src/theme/DocSidebarItem/Link/index.tsx index 6040964fb4..8d8e239212 100644 --- a/www/apps/docs/src/theme/DocSidebarItem/Link/index.tsx +++ b/www/apps/docs/src/theme/DocSidebarItem/Link/index.tsx @@ -67,6 +67,12 @@ export default function DocSidebarItemLink({ )} {label} {!isInternalLink && } + {customProps?.sidebar_badge && ( + + )} {customProps?.sidebar_is_soon && ( diff --git a/www/apps/docs/src/types/index.d.ts b/www/apps/docs/src/types/index.d.ts index 863bf6c163..c5ff6da4ea 100644 --- a/www/apps/docs/src/types/index.d.ts +++ b/www/apps/docs/src/types/index.d.ts @@ -26,6 +26,8 @@ declare module "@medusajs/docs" { PropSidebarItemCategory, PropSidebarItemLink, PropSidebarItemHtml, + SidebarCategoriesShorthand, + SidebarItemConfig, } from "@docusaurus/plugin-content-docs" import { BadgeProps, ButtonType, ButtonVariants } from "docs-ui" import { IconProps } from "@medusajs/icons/dist/types" @@ -55,6 +57,7 @@ declare module "@medusajs/docs" { sidebar_is_group_divider?: boolean sidebar_is_divider_line?: boolean sidebar_is_back_link?: boolean + sidebar_badge?: BadgeProps } }