docs: describes usage of plugin
This commit is contained in:
@@ -1,24 +1,89 @@
|
|||||||
# medusa-plugin-restock-notification
|
# medusa-plugin-restock-notification
|
||||||
|
|
||||||
Twilio SMS / Messaging plugin.
|
|
||||||
|
|
||||||
## Plugin Options
|
## Usage
|
||||||
|
|
||||||
```
|
Install the plugin:
|
||||||
{
|
|
||||||
account_sid: [twilio messaging account sid] (required),
|
`$ yarn add medusa-plugin-restock-notification`
|
||||||
auth_token: [twilio massaging authentication token] (required),
|
|
||||||
from_number: [the number used as sender SMS],
|
```js
|
||||||
|
// medusa-config.js
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...,
|
||||||
|
plugins: [
|
||||||
|
...,
|
||||||
|
`medusa-plugin-restock-notification`
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dynamic usage
|
The plugin will migrate your database to include the RestockNotification entity, which consists of a variant id of a sold out item and a jsonb list of arrays that wish to be notified about restocks for the item.
|
||||||
|
|
||||||
You can resolve the Twilio SMS service to dynamically send SMS's via Twilio.
|
|
||||||
|
|
||||||
Example:
|
## API endpoint
|
||||||
|
|
||||||
|
The plugin exposes an endpoint to sign emails up for restock notifications:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /restock-notifications/variants/:variant_id
|
||||||
|
|
||||||
|
Body
|
||||||
|
{
|
||||||
|
"email": "seb@test.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The endpoint responds with `200 OK` on succesful signups. If a signup for an already in stock item is attempted the endpoint will have a 400 response code.
|
||||||
|
|
||||||
|
|
||||||
|
## Restock events
|
||||||
|
|
||||||
|
The plugin listens for the `product-variant.updated` call and emits a `restock-notification.restocked` event when a variant with restock signups become available.
|
||||||
|
|
||||||
|
The data sent with the `restock-notification.restocked` event is:
|
||||||
|
```
|
||||||
|
variant_id: The id of the variant to listen for restock events for.
|
||||||
|
emails: An array of emails that are to be notified of restocks.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
{
|
||||||
|
"variant_id": "variant_1234567890",
|
||||||
|
"emails": ["seb@test.com", "oli@test.com"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note: This plugin does not send any communication to the customer, communication logic must be implemented or provided through a communication plugin.*
|
||||||
|
|
||||||
|
You may use `medusa-plugin-sendgrid` to orchestrate transactional emails.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage with medusa-plugin-sendgrid
|
||||||
|
|
||||||
|
Install the plugins:
|
||||||
|
`$ yarn add medusa-plugin-restock-notification medusa-plugin-sendgrid`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const twilioSmsService = scope.resolve("twilioSmsService")
|
// medusa-config.js
|
||||||
twilioSmsService.sendSms({ to: "+4500112233", body: "Hello world!" })
|
|
||||||
|
module.exports = {
|
||||||
|
...,
|
||||||
|
plugins: [
|
||||||
|
...,
|
||||||
|
`medusa-plugin-restock-notification`,
|
||||||
|
{
|
||||||
|
resolve: `medusa-plugin-sendgrid`,
|
||||||
|
options: {
|
||||||
|
from: SENDGRID_FROM,
|
||||||
|
api_key: SENDGRID_API_KEY,
|
||||||
|
medusa_restock_template: `d-13141234123412342314`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You should set up a dynamic template in SendGrid which will be send for each of the emails in the restock notification.
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default async (req, res) => {
|
|||||||
"restockNotificationService"
|
"restockNotificationService"
|
||||||
)
|
)
|
||||||
await restockNotificationService.addEmail(variant_id, value.email)
|
await restockNotificationService.addEmail(variant_id, value.email)
|
||||||
res.sendStatus(200)
|
res.sendStatus(201)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ message: err.message })
|
res.status(400).json({ message: err.message })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,3 +30,25 @@ export class RestockNotification {
|
|||||||
@UpdateDateColumn({ type: "timestamptz" })
|
@UpdateDateColumn({ type: "timestamptz" })
|
||||||
updated_at: Date
|
updated_at: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema restock_notification
|
||||||
|
* title: "Restock Notification"
|
||||||
|
* description: "Holds a list of emails that wish to be notifed when an item is restocked."
|
||||||
|
* x-resourceId: restock_notification
|
||||||
|
* properties:
|
||||||
|
* variant_id:
|
||||||
|
* type: string
|
||||||
|
* description: "The id of the variant that customers have signed up to be notified about,"
|
||||||
|
* emails:
|
||||||
|
* type: string[]
|
||||||
|
* description: "The emails of customers who wish to be notified about restocks."
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: "The date time at which the first restock signup was made."
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: "The date time at which the first last signup was made."
|
||||||
|
*/
|
||||||
|
|||||||
+2
-8
@@ -88,10 +88,7 @@ describe("RestockNotificationService", () => {
|
|||||||
it("successfully adds email to non-existing noti", async () => {
|
it("successfully adds email to non-existing noti", async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
const result = await restockNotiService.addEmail(
|
await restockNotiService.addEmail("variant_test", "seb@med-test.com")
|
||||||
"variant_test",
|
|
||||||
"seb@med-test.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(RestockNotificationModel.create).toHaveBeenCalledTimes(1)
|
expect(RestockNotificationModel.create).toHaveBeenCalledTimes(1)
|
||||||
expect(RestockNotificationModel.create).toHaveBeenCalledWith({
|
expect(RestockNotificationModel.create).toHaveBeenCalledWith({
|
||||||
@@ -105,10 +102,7 @@ describe("RestockNotificationService", () => {
|
|||||||
it("successfully adds email to existing noti", async () => {
|
it("successfully adds email to existing noti", async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
|
||||||
const result = await restockNotiService.addEmail(
|
await restockNotiService.addEmail("variant_1234", "seb@med-test.com")
|
||||||
"variant_1234",
|
|
||||||
"seb@med-test.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(RestockNotificationModel.save).toHaveBeenCalledTimes(1)
|
expect(RestockNotificationModel.save).toHaveBeenCalledTimes(1)
|
||||||
expect(RestockNotificationModel.save).toHaveBeenCalledWith({
|
expect(RestockNotificationModel.save).toHaveBeenCalledWith({
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restock notifications can be used to keep track of customers who wish to be
|
||||||
|
* notified when a certain item is restocked. Restock notifications can only
|
||||||
|
* apply to sold out items and will be deleted once items are restocked.
|
||||||
|
*/
|
||||||
class RestockNotificationService extends BaseService {
|
class RestockNotificationService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
@@ -41,6 +46,12 @@ class RestockNotificationService extends BaseService {
|
|||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a restock notification by a given variant id.
|
||||||
|
* @param {string} variantId - the variant id to retrieve restock notification
|
||||||
|
* for
|
||||||
|
* @return {Promise<RestockNotification>} The restock notification
|
||||||
|
*/
|
||||||
async retrieve(variantId) {
|
async retrieve(variantId) {
|
||||||
const restockRepo = this.manager_.getRepository(
|
const restockRepo = this.manager_.getRepository(
|
||||||
this.restockNotificationModel_
|
this.restockNotificationModel_
|
||||||
@@ -48,6 +59,13 @@ class RestockNotificationService extends BaseService {
|
|||||||
return await restockRepo.findOne({ where: { variant_id: variantId } })
|
return await restockRepo.findOne({ where: { variant_id: variantId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an email to be notified when a certain variant is restocked. Throws if
|
||||||
|
* the variant is not sold out.
|
||||||
|
* @param {string} variantId - the variant id to sign up for notifications for
|
||||||
|
* @param {string} email - the email to signup
|
||||||
|
* @return {Promise<RestockNotification>} The resulting restock notification
|
||||||
|
*/
|
||||||
async addEmail(variantId, email) {
|
async addEmail(variantId, email) {
|
||||||
return this.atomicPhase_(async (manager) => {
|
return this.atomicPhase_(async (manager) => {
|
||||||
const restockRepo = manager.getRepository(this.restockNotificationModel_)
|
const restockRepo = manager.getRepository(this.restockNotificationModel_)
|
||||||
@@ -81,6 +99,13 @@ class RestockNotificationService extends BaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if anyone has signed up for restock notifications on a given variant
|
||||||
|
* and emits a restocked event to the event bus. After successful emission the
|
||||||
|
* restock notification is deleted.
|
||||||
|
* @param {string} variantId - the variant id to trigger restock for
|
||||||
|
* @return {Promise<RestockNotification>} The resulting restock notification
|
||||||
|
*/
|
||||||
async triggerRestock(variantId) {
|
async triggerRestock(variantId) {
|
||||||
return this.atomicPhase_(async (manager) => {
|
return this.atomicPhase_(async (manager) => {
|
||||||
const restockRepo = manager.getRepository(this.restockNotificationModel_)
|
const restockRepo = manager.getRepository(this.restockNotificationModel_)
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ class SendGridService extends NotificationService {
|
|||||||
return this.userPasswordResetData(eventData, attachmentGenerator)
|
return this.userPasswordResetData(eventData, attachmentGenerator)
|
||||||
case "customer.password_reset":
|
case "customer.password_reset":
|
||||||
return this.customerPasswordResetData(eventData, attachmentGenerator)
|
return this.customerPasswordResetData(eventData, attachmentGenerator)
|
||||||
|
case "restock-notification.restocked":
|
||||||
|
return await this.restockNotificationData(eventData, attachmentGenerator)
|
||||||
default:
|
default:
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
@@ -154,6 +156,8 @@ class SendGridService extends NotificationService {
|
|||||||
return this.options_.user_password_reset_template
|
return this.options_.user_password_reset_template
|
||||||
case "customer.password_reset":
|
case "customer.password_reset":
|
||||||
return this.options_.customer_password_reset_template
|
return this.options_.customer_password_reset_template
|
||||||
|
case "restock-notification.restocked":
|
||||||
|
return this.options_.medusa_restock_template
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -671,6 +675,19 @@ class SendGridService extends NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restockNotificationData({ variant_id, emails }) {
|
||||||
|
const variant = await this.productVariantService_.retrieve(variant_id, {
|
||||||
|
relations: ["product"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
product: variant.product,
|
||||||
|
variant,
|
||||||
|
variant_id,
|
||||||
|
emails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
userPasswordResetData(data) {
|
userPasswordResetData(data) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -698,7 +715,9 @@ class SendGridService extends NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalized = humanizeAmount(amount, currency)
|
const normalized = humanizeAmount(amount, currency)
|
||||||
return normalized.toFixed(zeroDecimalCurrencies.includes(currency.toLowerCase()) ? 0 : 2)
|
return normalized.toFixed(
|
||||||
|
zeroDecimalCurrencies.includes(currency.toLowerCase()) ? 0 : 2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeThumbUrl_(url) {
|
normalizeThumbUrl_(url) {
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ class OrderSubscriber {
|
|||||||
this.notificationService_.subscribe("swap.created", "sendgrid")
|
this.notificationService_.subscribe("swap.created", "sendgrid")
|
||||||
this.notificationService_.subscribe("order.items_returned", "sendgrid")
|
this.notificationService_.subscribe("order.items_returned", "sendgrid")
|
||||||
this.notificationService_.subscribe("order.return_requested", "sendgrid")
|
this.notificationService_.subscribe("order.return_requested", "sendgrid")
|
||||||
this.notificationService_.subscribe(
|
|
||||||
"restock_notification.restocked",
|
|
||||||
"sendgrid"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
class RestockNotification {
|
||||||
|
constructor({ eventBusService, sendgridService }) {
|
||||||
|
eventBusService.subscribe(
|
||||||
|
"restock-notification.restocked",
|
||||||
|
async (eventData) => {
|
||||||
|
const templateId = await sendgridService.getTemplateId(
|
||||||
|
"restock-notification.restocked"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!templateId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await sendgridService.fetchData(
|
||||||
|
"restock-notification.restocked",
|
||||||
|
eventData,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!data.emails) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
data.emails.map(async (e) => {
|
||||||
|
const sendOptions = {
|
||||||
|
template_id: templateId,
|
||||||
|
from: sendgridService.options_.from,
|
||||||
|
to: e,
|
||||||
|
dynamic_template_data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sendgridService.sendEmail(sendOptions)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RestockNotification
|
||||||
Reference in New Issue
Block a user