feat: notifications (#172)
The Notifications API allows plugins to register Notification Providers which have `sendNotification` and `resendNotification`. Each plugin can listen to any events transmittet over the event bus and the result of the notification send will be persisted in the database to allow for clear communications timeline + ability to resend notifications.
This commit is contained in:
@@ -422,8 +422,41 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
/**
|
||||
* This plugin doesn't support shipment documents.
|
||||
*/
|
||||
async getShipmentDocuments() {
|
||||
return []
|
||||
async retrieveDocuments(fulfillmentData, documentType) {
|
||||
switch (documentType) {
|
||||
case "label":
|
||||
const labelRelation = fulfillmentData?.relationships?.labels
|
||||
if (labelRelation) {
|
||||
const docs = await this.retrieveRelationship(labelRelation)
|
||||
.then(({ data }) => data)
|
||||
.catch((_) => [])
|
||||
|
||||
return docs.map((d) => ({
|
||||
name: d.attributes.document_type,
|
||||
base_64: d.attributes.base64,
|
||||
type: "application/pdf",
|
||||
}))
|
||||
}
|
||||
return []
|
||||
|
||||
case "invoice":
|
||||
const docRelation = fulfillmentData?.relationships?.documents
|
||||
if (docRelation) {
|
||||
const docs = await this.retrieveRelationship(docRelation)
|
||||
.then(({ data }) => data)
|
||||
.catch((_) => [])
|
||||
|
||||
return docs.map((d) => ({
|
||||
name: d.attributes.document_type,
|
||||
base_64: d.attributes.base64,
|
||||
type: "application/pdf",
|
||||
}))
|
||||
}
|
||||
return []
|
||||
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,4 +3,5 @@ export { default as BaseModel } from "./base-model"
|
||||
export { default as PaymentService } from "./payment-service"
|
||||
export { default as FulfillmentService } from "./fulfillment-service"
|
||||
export { default as FileService } from "./file-service"
|
||||
export { default as NotificationService } from "./notification-service"
|
||||
export { default as OauthService } from "./oauth-service"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import BaseService from "./base-service"
|
||||
|
||||
/**
|
||||
* Interface for Notification Providers
|
||||
* @interface
|
||||
*/
|
||||
class BaseNotificationService extends BaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
getIdentifier() {
|
||||
return this.constructor.identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to retrieve documents related to a shipment.
|
||||
*/
|
||||
sendNotification(event, data) {
|
||||
throw new Error("Must be overridden by child")
|
||||
}
|
||||
|
||||
resendNotification(notification, config = {}) {
|
||||
throw new Error("Must be overridden by child")
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseNotificationService
|
||||
@@ -1,7 +1,9 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { NotificationService } from "medusa-interfaces"
|
||||
import SendGrid from "@sendgrid/mail"
|
||||
|
||||
class SendGridService extends BaseService {
|
||||
class SendGridService extends NotificationService {
|
||||
static identifier = "sendgrid"
|
||||
|
||||
/**
|
||||
* @param {Object} options - options defined in `medusa-config.js`
|
||||
* e.g.
|
||||
@@ -15,69 +17,217 @@ class SendGridService extends BaseService {
|
||||
* customer_password_reset_template: 1111,
|
||||
* }
|
||||
*/
|
||||
constructor({}, options) {
|
||||
constructor(
|
||||
{
|
||||
storeService,
|
||||
orderService,
|
||||
returnService,
|
||||
swapService,
|
||||
claimService,
|
||||
fulfillmentService,
|
||||
fulfillmentProviderService,
|
||||
totalsService,
|
||||
},
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.options_ = options
|
||||
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
this.storeService_ = storeService
|
||||
this.orderService_ = orderService
|
||||
this.claimService_ = claimService
|
||||
this.returnService_ = returnService
|
||||
this.swapService_ = swapService
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
SendGrid.setApiKey(options.api_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a transactional email based on an event using SendGrid.
|
||||
* @param {string} event - event related to the order
|
||||
* @param {Object} order - the order object sent to SendGrid, that must
|
||||
* correlate with the structure specificed in the dynamic template
|
||||
* @returns {Promise} result of the send operation
|
||||
*/
|
||||
async transactionalEmail(event, data) {
|
||||
let templateId
|
||||
async fetchAttachments(event, data) {
|
||||
switch (event) {
|
||||
case "order.gift_card_created":
|
||||
templateId = this.options_.gift_card_created_template
|
||||
data = {
|
||||
...data,
|
||||
display_value: data.giftcard.rule.value * (1 + data.tax_rate),
|
||||
case "swap.created":
|
||||
case "order.return_requested": {
|
||||
let attachments = []
|
||||
const { shipping_method, shipping_data } = data.return_request
|
||||
if (shipping_method) {
|
||||
const provider = shipping_method.shipping_option.provider_id
|
||||
|
||||
const lbl = await this.fulfillmentProviderService_.retrieveDocuments(
|
||||
provider,
|
||||
shipping_data,
|
||||
"label"
|
||||
)
|
||||
|
||||
attachments = attachments.concat(
|
||||
lbl.map((d) => ({
|
||||
name: "return-label",
|
||||
base64: d.base_64,
|
||||
type: d.type,
|
||||
}))
|
||||
)
|
||||
|
||||
const inv = await this.fulfillmentProviderService_.retrieveDocuments(
|
||||
provider,
|
||||
shipping_data,
|
||||
"invoice"
|
||||
)
|
||||
|
||||
attachments = attachments.concat(
|
||||
inv.map((d) => ({
|
||||
name: "invoice",
|
||||
base64: d.base_64,
|
||||
type: d.type,
|
||||
}))
|
||||
)
|
||||
}
|
||||
break
|
||||
case "order.placed":
|
||||
templateId = this.options_.order_placed_template
|
||||
break
|
||||
case "order.updated":
|
||||
templateId = this.options_.order_updated_template
|
||||
break
|
||||
case "order.shipment_created":
|
||||
templateId = this.options_.order_shipped_template
|
||||
break
|
||||
case "order.cancelled":
|
||||
templateId = this.options_.order_cancelled_template
|
||||
break
|
||||
case "order.completed":
|
||||
templateId = this.options_.order_completed_template
|
||||
break
|
||||
case "user.password_reset":
|
||||
templateId = this.options_.user_password_reset_template
|
||||
break
|
||||
case "customer.password_reset":
|
||||
templateId = this.options_.customer_password_reset_template
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (templateId) {
|
||||
return SendGrid.send({
|
||||
template_id: templateId,
|
||||
from: this.options_.from,
|
||||
to: data.email,
|
||||
dynamic_template_data: data,
|
||||
})
|
||||
|
||||
return attachments
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async fetchData(event, eventData, attachmentGenerator) {
|
||||
switch (event) {
|
||||
case "order.return_requested":
|
||||
return this.returnRequestedData(eventData, attachmentGenerator)
|
||||
case "swap.shipment_created":
|
||||
return this.swapShipmentCreatedData(eventData, attachmentGenerator)
|
||||
case "claim.shipment_created":
|
||||
return this.claimShipmentCreatedData(eventData, attachmentGenerator)
|
||||
case "order.items_returned":
|
||||
return this.itemsReturnedData(eventData, attachmentGenerator)
|
||||
case "order.swap_received":
|
||||
return this.swapReceivedData(eventData, attachmentGenerator)
|
||||
case "swap.created":
|
||||
return this.swapCreatedData(eventData, attachmentGenerator)
|
||||
case "gift_card.created":
|
||||
return this.gcCreatedData(eventData, attachmentGenerator)
|
||||
case "order.gift_card_created":
|
||||
return this.gcCreatedData(eventData, attachmentGenerator)
|
||||
case "order.placed":
|
||||
return this.orderPlacedData(eventData, attachmentGenerator)
|
||||
case "order.shipment_created":
|
||||
return this.orderShipmentCreatedData(eventData, attachmentGenerator)
|
||||
case "order.canceled":
|
||||
return this.orderCanceledData(eventData, attachmentGenerator)
|
||||
case "user.password_reset":
|
||||
return this.userPasswordResetData(eventData, attachmentGenerator)
|
||||
case "customer.password_reset":
|
||||
return this.customerPasswordResetData(eventData, attachmentGenerator)
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
getTemplateId(event) {
|
||||
switch (event) {
|
||||
case "order.return_requested":
|
||||
return this.options_.order_return_requested_template
|
||||
case "swap.shipment_created":
|
||||
return this.options_.swap_shipment_created_template
|
||||
case "claim.shipment_created":
|
||||
return this.options_.claim_shipment_created_template
|
||||
case "order.items_returned":
|
||||
return this.options_.order_items_returned_template
|
||||
case "order.swap_received":
|
||||
return this.options_.order_swap_received_template
|
||||
case "swap.created":
|
||||
return this.options_.swap_created_template
|
||||
case "gift_card.created":
|
||||
return this.options_.gift_card_created_template
|
||||
case "order.gift_card_created":
|
||||
return this.options_.gift_card_created_template
|
||||
case "order.placed":
|
||||
return this.options_.order_placed_template
|
||||
case "order.shipment_created":
|
||||
return this.options_.order_shipped_template
|
||||
case "order.canceled":
|
||||
return this.options_.order_canceled_template
|
||||
case "user.password_reset":
|
||||
return this.options_.user_password_reset_template
|
||||
case "customer.password_reset":
|
||||
return this.options_.customer_password_reset_template
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async sendNotification(event, eventData, attachmentGenerator) {
|
||||
let templateId = this.getTemplateId(event)
|
||||
|
||||
if (!templateId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data = await this.fetchData(event, eventData, attachmentGenerator)
|
||||
const attachments = await this.fetchAttachments(event, data)
|
||||
|
||||
const sendOptions = {
|
||||
template_id: templateId,
|
||||
from: this.options_.from,
|
||||
to: data.email,
|
||||
dynamic_template_data: data,
|
||||
has_attachments: attachments?.length,
|
||||
}
|
||||
|
||||
if (attachments?.length) {
|
||||
sendOptions.has_attachments = true
|
||||
sendOptions.attachments = attachments.map((a) => {
|
||||
return {
|
||||
content: a.base64,
|
||||
filename: a.name,
|
||||
type: a.type,
|
||||
disposition: "attachment",
|
||||
contentId: a.name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const status = await SendGrid.send(sendOptions)
|
||||
.then(() => "sent")
|
||||
.catch(() => "failed")
|
||||
|
||||
// We don't want heavy docs stored in DB
|
||||
delete sendOptions.attachments
|
||||
|
||||
return { to: data.email, status, data: sendOptions }
|
||||
}
|
||||
|
||||
async resendNotification(notification, config) {
|
||||
const sendOptions = {
|
||||
...notification.data,
|
||||
to: config.to || notification.to,
|
||||
}
|
||||
|
||||
if (notification.data?.has_attachments) {
|
||||
const attachs = await this.fetchAttachments(
|
||||
notification.event_name,
|
||||
notification.data.dynamic_template_data
|
||||
)
|
||||
|
||||
sendOptions.attachments = attachs.map((a) => {
|
||||
return {
|
||||
content: a.base64,
|
||||
filename: a.name,
|
||||
type: a.type,
|
||||
disposition: "attachment",
|
||||
contentId: a.name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const status = await SendGrid.send(sendOptions)
|
||||
.then(() => "sent")
|
||||
.catch(() => "failed")
|
||||
|
||||
return { to: sendOptions.to, status, data: sendOptions }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email using SendGrid.
|
||||
* @param {string} templateId - id of template in SendGrid
|
||||
@@ -93,6 +243,381 @@ class SendGridService extends BaseService {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async orderShipmentCreatedData({ id, fulfillment_id }, attachmentGenerator) {
|
||||
const order = await this.orderService_.retrieve(id, {
|
||||
select: [
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"refunded_total",
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"total",
|
||||
"refundable_amount",
|
||||
],
|
||||
relations: [
|
||||
"customer",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
"returns",
|
||||
"gift_cards",
|
||||
"gift_card_transactions",
|
||||
],
|
||||
})
|
||||
|
||||
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
|
||||
relations: ["items"],
|
||||
})
|
||||
|
||||
return {
|
||||
order,
|
||||
email: order.email,
|
||||
fulfillment: shipment,
|
||||
tracking_number: shipment.tracking_numbers.join(", "),
|
||||
}
|
||||
}
|
||||
|
||||
async orderPlacedData({ id }) {
|
||||
const order = await this.orderService_.retrieve(id, {
|
||||
select: [
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"refunded_total",
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"total",
|
||||
],
|
||||
relations: [
|
||||
"customer",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
"returns",
|
||||
"gift_cards",
|
||||
"gift_card_transactions",
|
||||
],
|
||||
})
|
||||
|
||||
const {
|
||||
subtotal,
|
||||
tax_total,
|
||||
discount_total,
|
||||
shipping_total,
|
||||
gift_card_total,
|
||||
total,
|
||||
} = order
|
||||
|
||||
const taxRate = order.tax_rate / 100
|
||||
const currencyCode = order.currency_code.toUpperCase()
|
||||
|
||||
const items = this.processItems_(order.items, taxRate, currencyCode)
|
||||
|
||||
let discounts = []
|
||||
if (order.discounts) {
|
||||
discounts = order.discounts.map((discount) => {
|
||||
return {
|
||||
is_giftcard: false,
|
||||
code: discount.code,
|
||||
descriptor: `${discount.rule.value}${
|
||||
discount.rule.type === "percentage" ? "%" : ` ${currencyCode}`
|
||||
}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let giftCards = []
|
||||
if (order.gift_cards) {
|
||||
giftCards = order.gift_cards.map((gc) => {
|
||||
return {
|
||||
is_giftcard: true,
|
||||
code: gc.code,
|
||||
descriptor: `${gc.value} ${currencyCode}`,
|
||||
}
|
||||
})
|
||||
|
||||
discounts.concat(giftCards)
|
||||
}
|
||||
|
||||
return {
|
||||
...order,
|
||||
has_discounts: order.discounts.length,
|
||||
has_gift_cards: order.gift_cards.length,
|
||||
date: order.created_at.toDateString(),
|
||||
items,
|
||||
discounts,
|
||||
subtotal: `${this.humanPrice_(subtotal * (1 + taxRate))} ${currencyCode}`,
|
||||
gift_card_total: `${this.humanPrice_(
|
||||
gift_card_total * (1 + taxRate)
|
||||
)} ${currencyCode}`,
|
||||
tax_total: `${this.humanPrice_(tax_total)} ${currencyCode}`,
|
||||
discount_total: `${this.humanPrice_(
|
||||
discount_total * (1 + taxRate)
|
||||
)} ${currencyCode}`,
|
||||
shipping_total: `${this.humanPrice_(
|
||||
shipping_total * (1 + taxRate)
|
||||
)} ${currencyCode}`,
|
||||
total: `${this.humanPrice_(total)} ${currencyCode}`,
|
||||
}
|
||||
}
|
||||
|
||||
async gcCreatedData({ id }) {
|
||||
const giftCard = await this.giftCardService_.retrieve(id, {
|
||||
relations: ["region", "order"],
|
||||
})
|
||||
|
||||
const taxRate = giftCard.region.tax_rate / 100
|
||||
|
||||
return {
|
||||
...giftCard,
|
||||
email: giftCard.order.email,
|
||||
display_value: giftCard.value * (1 + taxRate),
|
||||
}
|
||||
}
|
||||
|
||||
async returnRequestedData({ id, return_id }) {
|
||||
// Fetch the return request
|
||||
const returnRequest = await this.returnService_.retrieve(return_id, {
|
||||
relations: [
|
||||
"items",
|
||||
"shipping_method",
|
||||
"shipping_method.shipping_option",
|
||||
],
|
||||
})
|
||||
|
||||
// Fetch the order
|
||||
const order = await this.orderService_.retrieve(id, {
|
||||
relations: ["items", "discounts", "shipping_address"],
|
||||
})
|
||||
|
||||
// Calculate which items are in the return
|
||||
const returnItems = returnRequest.items.map((i) => {
|
||||
const found = order.items.find((oi) => oi.id === i.item_id)
|
||||
return {
|
||||
...found,
|
||||
quantity: i.quantity,
|
||||
}
|
||||
})
|
||||
|
||||
const taxRate = order.tax_rate / 100
|
||||
const currencyCode = order.currency_code.toUpperCase()
|
||||
|
||||
// Get total of the returned products
|
||||
const item_subtotal = this.totalsService_.getRefundTotal(order, returnItems)
|
||||
|
||||
// If the return has a shipping method get the price and any attachments
|
||||
let shippingTotal = 0
|
||||
if (returnRequest.shipping_method) {
|
||||
shippingTotal = returnRequest.shipping_method.price * (1 + taxRate)
|
||||
}
|
||||
|
||||
return {
|
||||
has_shipping: !!returnRequest.shipping_method,
|
||||
email: order.email,
|
||||
items: this.processItems_(returnItems, taxRate, currencyCode),
|
||||
subtotal: `${this.humanPrice_(item_subtotal)} ${currencyCode}`,
|
||||
shipping_total: `${this.humanPrice_(shippingTotal)} ${currencyCode}`,
|
||||
refund_amount: `${this.humanPrice_(
|
||||
returnRequest.refund_amount
|
||||
)} ${currencyCode}`,
|
||||
return_request: {
|
||||
...returnRequest,
|
||||
refund_amount: `${this.humanPrice_(
|
||||
returnRequest.refund_amount
|
||||
)} ${currencyCode}`,
|
||||
},
|
||||
order,
|
||||
date: returnRequest.updated_at.toDateString(),
|
||||
}
|
||||
}
|
||||
|
||||
async swapCreatedData({ id }) {
|
||||
const store = await this.storeService_.retrieve()
|
||||
const swap = await this.swapService_.retrieve(id, {
|
||||
relations: [
|
||||
"additional_items",
|
||||
"return_order",
|
||||
"return_order.items",
|
||||
"return_order.shipping_method",
|
||||
"return_order.shipping_method.shipping_option",
|
||||
],
|
||||
})
|
||||
|
||||
const swapLink = store.swap_link_template.replace(
|
||||
/\{cart_id\}/,
|
||||
swap.cart_id
|
||||
)
|
||||
|
||||
const order = await this.orderService_.retrieve(swap.order_id, {
|
||||
relations: ["items", "discounts", "shipping_address"],
|
||||
})
|
||||
|
||||
const taxRate = order.tax_rate / 100
|
||||
const currencyCode = order.currency_code.toUpperCase()
|
||||
|
||||
const returnItems = this.processItems_(
|
||||
swap.return_order.items.map((i) => {
|
||||
const found = order.items.find((oi) => oi.id === i.item_id)
|
||||
return {
|
||||
...found,
|
||||
quantity: i.quantity,
|
||||
}
|
||||
}),
|
||||
taxRate,
|
||||
currencyCode
|
||||
)
|
||||
|
||||
const returnTotal = this.totalsService_.getRefundTotal(order, returnItems)
|
||||
|
||||
const constructedOrder = {
|
||||
...order,
|
||||
shipping_methods: [],
|
||||
items: swap.additional_items,
|
||||
}
|
||||
|
||||
const additionalTotal = this.totalsService_.getTotal(constructedOrder)
|
||||
|
||||
const refundAmount = swap.return_order.refund_amount
|
||||
|
||||
return {
|
||||
swap,
|
||||
order,
|
||||
return_request: swap.return_order,
|
||||
date: swap.updated_at.toDateString(),
|
||||
swap_link: swapLink,
|
||||
email: order.email,
|
||||
items: this.processItems_(swap.additional_items, taxRate, currencyCode),
|
||||
return_items: returnItems,
|
||||
return_total: `${this.humanPrice_(returnTotal)} ${currencyCode}`,
|
||||
refund_amount: `${this.humanPrice_(refundAmount)} ${currencyCode}`,
|
||||
additional_total: `${this.humanPrice_(additionalTotal)} ${currencyCode}`,
|
||||
}
|
||||
}
|
||||
|
||||
async itemsReturnedData(data) {
|
||||
return this.returnRequestedData(data)
|
||||
}
|
||||
|
||||
async swapShipmentCreatedData({ id, fulfillment_id }) {
|
||||
const swap = await this.swapService_.retrieve(id, {
|
||||
relations: [
|
||||
"shipping_address",
|
||||
"shipping_methods",
|
||||
"additional_items",
|
||||
"return_order",
|
||||
"return_order.items",
|
||||
],
|
||||
})
|
||||
|
||||
const order = await this.orderService_.retrieve(swap.order_id, {
|
||||
relations: ["items", "discounts"],
|
||||
})
|
||||
|
||||
const taxRate = order.tax_rate / 100
|
||||
const currencyCode = order.currency_code.toUpperCase()
|
||||
|
||||
const returnItems = this.processItems_(
|
||||
swap.return_order.items.map((i) => {
|
||||
const found = order.items.find((oi) => oi.id === i.item_id)
|
||||
return {
|
||||
...found,
|
||||
quantity: i.quantity,
|
||||
}
|
||||
}),
|
||||
taxRate,
|
||||
currencyCode
|
||||
)
|
||||
|
||||
const returnTotal = this.totalsService_.getRefundTotal(order, returnItems)
|
||||
|
||||
const constructedOrder = {
|
||||
...order,
|
||||
shipping_methods: swap.shipping_methods,
|
||||
items: swap.additional_items,
|
||||
}
|
||||
|
||||
const additionalTotal = this.totalsService_.getTotal(constructedOrder)
|
||||
|
||||
const refundAmount = swap.return_order.refund_amount
|
||||
|
||||
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id)
|
||||
|
||||
return {
|
||||
swap,
|
||||
order,
|
||||
items: this.processItems_(swap.additional_items, taxRate, currencyCode),
|
||||
date: swap.updated_at.toDateString(),
|
||||
email: order.email,
|
||||
tax_amount: `${this.humanPrice_(
|
||||
swap.difference_due * taxRate
|
||||
)} ${currencyCode}`,
|
||||
paid_total: `${this.humanPrice_(swap.difference_due)} ${currencyCode}`,
|
||||
return_total: `${this.humanPrice_(returnTotal)} ${currencyCode}`,
|
||||
refund_amount: `${this.humanPrice_(refundAmount)} ${currencyCode}`,
|
||||
additional_total: `${this.humanPrice_(additionalTotal)} ${currencyCode}`,
|
||||
fulfillment: shipment,
|
||||
tracking_number: shipment.tracking_numbers.join(", "),
|
||||
}
|
||||
}
|
||||
|
||||
async claimShipmentCreatedData({ id, fulfillment_id }) {
|
||||
const claim = await this.claimService_.retrieve(id, {
|
||||
relations: ["order", "order.items", "order.shipping_address"],
|
||||
})
|
||||
|
||||
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id)
|
||||
|
||||
return {
|
||||
email: claim.order.email,
|
||||
claim,
|
||||
order: claim.order,
|
||||
fulfillment: shipment,
|
||||
tracking_number: shipment.tracking_numbers.join(", "),
|
||||
}
|
||||
}
|
||||
|
||||
userPasswordResetData(data) {
|
||||
return data
|
||||
}
|
||||
|
||||
customerPasswordResetData(data) {
|
||||
return data
|
||||
}
|
||||
|
||||
processItems_(items, taxRate, currencyCode) {
|
||||
return items.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
thumbnail: this.normalizeThumbUrl_(i.thumbnail),
|
||||
price: `${this.humanPrice_(
|
||||
i.unit_price * (1 + taxRate)
|
||||
)} ${currencyCode}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
humanPrice_(amount) {
|
||||
return amount ? (amount / 100).toFixed(2) : "0.00"
|
||||
}
|
||||
|
||||
normalizeThumbUrl_(url) {
|
||||
if (url.startsWith("http")) {
|
||||
return url
|
||||
} else if (url.startsWith("//")) {
|
||||
return `https:${url}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export default SendGridService
|
||||
|
||||
@@ -3,187 +3,26 @@ class OrderSubscriber {
|
||||
totalsService,
|
||||
orderService,
|
||||
sendgridService,
|
||||
eventBusService,
|
||||
notificationService,
|
||||
fulfillmentService,
|
||||
}) {
|
||||
this.orderService_ = orderService
|
||||
this.totalsService_ = totalsService
|
||||
this.sendgridService_ = sendgridService
|
||||
this.eventBus_ = eventBusService
|
||||
this.notificationService_ = notificationService
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
|
||||
this.eventBus_.subscribe(
|
||||
"order.shipment_created",
|
||||
async ({ id, fulfillment_id }) => {
|
||||
const order = await this.orderService_.retrieve(id, {
|
||||
select: [
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"refunded_total",
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"total",
|
||||
"refundable_amount",
|
||||
],
|
||||
relations: [
|
||||
"customer",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
"returns",
|
||||
"gift_cards",
|
||||
"gift_card_transactions",
|
||||
"swaps",
|
||||
"swaps.return_order",
|
||||
"swaps.payment",
|
||||
"swaps.shipping_methods",
|
||||
"swaps.shipping_address",
|
||||
"swaps.additional_items",
|
||||
"swaps.fulfillments",
|
||||
],
|
||||
})
|
||||
|
||||
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id)
|
||||
|
||||
const data = {
|
||||
...order,
|
||||
tracking_number: shipment.tracking_numbers.join(", "),
|
||||
}
|
||||
|
||||
await this.sendgridService_.transactionalEmail(
|
||||
"order.shipment_created",
|
||||
data
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
this.eventBus_.subscribe("order.gift_card_created", async (order) => {
|
||||
await this.sendgridService_.transactionalEmail(
|
||||
"order.gift_card_created",
|
||||
order
|
||||
)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("order.placed", async (orderObj) => {
|
||||
try {
|
||||
const order = await this.orderService_.retrieve(orderObj.id, {
|
||||
select: [
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"refunded_total",
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"total",
|
||||
],
|
||||
relations: [
|
||||
"customer",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
"returns",
|
||||
"gift_cards",
|
||||
"gift_card_transactions",
|
||||
"swaps",
|
||||
"swaps.return_order",
|
||||
"swaps.payment",
|
||||
"swaps.shipping_methods",
|
||||
"swaps.shipping_address",
|
||||
"swaps.additional_items",
|
||||
"swaps.fulfillments",
|
||||
],
|
||||
})
|
||||
|
||||
const {
|
||||
subtotal,
|
||||
tax_total,
|
||||
discount_total,
|
||||
shipping_total,
|
||||
total,
|
||||
} = order
|
||||
|
||||
const taxRate = order.tax_rate / 100
|
||||
const currencyCode = order.currency_code.toUpperCase()
|
||||
|
||||
const items = order.items.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
price: `${((i.unit_price / 100) * (1 + taxRate)).toFixed(
|
||||
2
|
||||
)} ${currencyCode}`,
|
||||
}
|
||||
})
|
||||
|
||||
let discounts = []
|
||||
if (order.discounts) {
|
||||
discounts = order.discounts.map((discount) => {
|
||||
return {
|
||||
is_giftcard: false,
|
||||
code: discount.code,
|
||||
descriptor: `${discount.rule.value}${
|
||||
discount.rule.type === "percentage" ? "%" : ` ${currencyCode}`
|
||||
}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let giftCards = []
|
||||
if (order.gift_cards) {
|
||||
giftCards = order.gift_cards.map((gc) => {
|
||||
return {
|
||||
is_giftcard: true,
|
||||
code: gc.code,
|
||||
descriptor: `${gc.value} ${currencyCode}`,
|
||||
}
|
||||
})
|
||||
|
||||
discounts.concat(giftCards)
|
||||
}
|
||||
|
||||
const data = {
|
||||
...order,
|
||||
date: order.created_at.toDateString(),
|
||||
items,
|
||||
discounts,
|
||||
subtotal: `${((subtotal / 100) * (1 + taxRate)).toFixed(
|
||||
2
|
||||
)} ${currencyCode}`,
|
||||
tax_total: `${(tax_total / 100).toFixed(2)} ${currencyCode}`,
|
||||
discount_total: `${((discount_total / 100) * (1 + taxRate)).toFixed(
|
||||
2
|
||||
)} ${currencyCode}`,
|
||||
shipping_total: `${((shipping_total / 100) * (1 + taxRate)).toFixed(
|
||||
2
|
||||
)} ${currencyCode}`,
|
||||
total: `${(total / 100).toFixed(2)} ${currencyCode}`,
|
||||
}
|
||||
|
||||
await this.sendgridService_.transactionalEmail("order.placed", data)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("order.cancelled", async (order) => {
|
||||
await this.sendgridService_.transactionalEmail("order.cancelled", order)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("order.completed", async (order) => {
|
||||
await this.sendgridService_.transactionalEmail("order.completed", order)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("order.updated", async (order) => {
|
||||
await this.sendgridService_.transactionalEmail("order.updated", order)
|
||||
})
|
||||
this.notificationService_.subscribe("order.shipment_created", "sendgrid")
|
||||
this.notificationService_.subscribe("order.gift_card_created", "sendgrid")
|
||||
this.notificationService_.subscribe("gift_card.created", "sendgrid")
|
||||
this.notificationService_.subscribe("order.placed", "sendgrid")
|
||||
this.notificationService_.subscribe("order.canceled", "sendgrid")
|
||||
this.notificationService_.subscribe("customer.password_reset", "sendgrid")
|
||||
this.notificationService_.subscribe("claim.shipment_created", "sendgrid")
|
||||
this.notificationService_.subscribe("swap.shipment_created", "sendgrid")
|
||||
this.notificationService_.subscribe("swap.created", "sendgrid")
|
||||
this.notificationService_.subscribe("order.items_returned", "sendgrid")
|
||||
this.notificationService_.subscribe("order.return_requested", "sendgrid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,6 @@ class UserSubscriber {
|
||||
data
|
||||
)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("customer.password_reset", async (data) => {
|
||||
await this.sendgridService_.transactionalEmail(
|
||||
"customer.password_reset",
|
||||
data
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import swapRoutes from "./swaps"
|
||||
import returnRoutes from "./returns"
|
||||
import variantRoutes from "./variants"
|
||||
import collectionRoutes from "./collections"
|
||||
import notificationRoutes from "./notifications"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -62,6 +63,7 @@ export default (app, container, config) => {
|
||||
returnRoutes(route)
|
||||
variantRoutes(route)
|
||||
collectionRoutes(route)
|
||||
notificationRoutes(route)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/notifications", route)
|
||||
|
||||
/**
|
||||
* List notifications
|
||||
*/
|
||||
route.get("/", middlewares.wrap(require("./list-notifications").default))
|
||||
|
||||
/**
|
||||
* Resend a notification
|
||||
*/
|
||||
route.post(
|
||||
"/:id/resend",
|
||||
middlewares.wrap(require("./resend-notification").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultRelations = ["resends"]
|
||||
export const allowedRelations = ["resends"]
|
||||
|
||||
export const defaultFields = [
|
||||
"id",
|
||||
"resource_type",
|
||||
"resource_id",
|
||||
"event_name",
|
||||
"to",
|
||||
"provider_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
export const allowedFields = [
|
||||
"id",
|
||||
"resource_type",
|
||||
"resource_id",
|
||||
"provider_id",
|
||||
"event_name",
|
||||
"to",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
import _ from "lodash"
|
||||
import { defaultRelations, defaultFields } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const notificationService = req.scope.resolve("notificationService")
|
||||
|
||||
const limit = parseInt(req.query.limit) || 50
|
||||
const offset = parseInt(req.query.offset) || 0
|
||||
|
||||
let selector = {}
|
||||
|
||||
let includeFields = []
|
||||
if ("fields" in req.query) {
|
||||
includeFields = req.query.fields.split(",")
|
||||
}
|
||||
|
||||
let expandFields = []
|
||||
if ("expand" in req.query) {
|
||||
expandFields = req.query.expand.split(",")
|
||||
}
|
||||
|
||||
if ("event_name" in req.query) {
|
||||
const values = req.query.event_name.split(",")
|
||||
selector.event_name = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if ("resource_type" in req.query) {
|
||||
const values = req.query.resource_type.split(",")
|
||||
selector.resource_type = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if ("resource_id" in req.query) {
|
||||
const values = req.query.resource_id.split(",")
|
||||
selector.resource_id = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if ("to" in req.query) {
|
||||
const values = req.query.to.split(",")
|
||||
selector.to = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if (!("include_resends" in req.query)) {
|
||||
selector.parent_id = null
|
||||
}
|
||||
|
||||
const listConfig = {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: expandFields.length ? expandFields : defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
|
||||
const notifications = await notificationService.list(selector, listConfig)
|
||||
|
||||
const fields = [...listConfig.select, ...listConfig.relations]
|
||||
const data = notifications.map(o => _.pick(o, fields))
|
||||
|
||||
res.json({ notifications: data, offset, limit })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultFields, defaultRelations } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
to: Validator.string().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const notificationService = req.scope.resolve("notificationService")
|
||||
|
||||
const config = {}
|
||||
|
||||
if (value.to) {
|
||||
config.to = value.to
|
||||
}
|
||||
|
||||
await notificationService.resend(id, config)
|
||||
|
||||
const notification = await notificationService.retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.json({ notification })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultFields, defaultRelations } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, claim_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
fulfillment_id: Validator.string().required(),
|
||||
tracking_numbers: Validator.array()
|
||||
.items(Validator.string())
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const claimService = req.scope.resolve("claimService")
|
||||
|
||||
await claimService.createShipment(
|
||||
claim_id,
|
||||
value.fulfillment_id,
|
||||
value.tracking_numbers
|
||||
)
|
||||
|
||||
const order = await orderService.retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,14 @@ export default app => {
|
||||
middlewares.wrap(require("./fulfill-claim").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates claim fulfillment
|
||||
*/
|
||||
route.post(
|
||||
"/:id/claims/:claim_id/shipments",
|
||||
middlewares.wrap(require("./create-claim-shipment").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete metadata key / value pair.
|
||||
*/
|
||||
|
||||
@@ -53,6 +53,7 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const returnService = req.scope.resolve("returnService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
|
||||
let inProgress = true
|
||||
let err = false
|
||||
@@ -98,6 +99,13 @@ export default async (req, res) => {
|
||||
.fulfill(createdReturn.id)
|
||||
}
|
||||
|
||||
await eventBus
|
||||
.withTransaction(manager)
|
||||
.emit("order.return_requested", {
|
||||
id,
|
||||
return_id: createdReturn.id,
|
||||
})
|
||||
|
||||
return {
|
||||
recovery_point: "return_requested",
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("POST /store/customers", () => {
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{ relations: ["orders", "shipping_addresses"] }
|
||||
{ relations: ["shipping_addresses"] }
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("POST /store/customers/:id", () => {
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{ relations: ["orders", "shipping_addresses"] }
|
||||
{ relations: ["shipping_addresses"] }
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async (req, res) => {
|
||||
|
||||
let customer = await customerService.addAddress(id, value.address)
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async (req, res) => {
|
||||
})
|
||||
|
||||
customer = await customerService.retrieve(customer.id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async (req, res) => {
|
||||
let customer = await customerService.removeAddress(id, address_id)
|
||||
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.json({ customer: data })
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async (req, res) => {
|
||||
try {
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
const customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
res.json({ customer })
|
||||
} catch (err) {
|
||||
|
||||
@@ -33,6 +33,8 @@ export default (app, container) => {
|
||||
route.get("/:id", middlewares.wrap(require("./get-customer").default))
|
||||
route.post("/:id", middlewares.wrap(require("./update-customer").default))
|
||||
|
||||
route.get("/:id/orders", middlewares.wrap(require("./list-orders").default))
|
||||
|
||||
route.post(
|
||||
"/:id/addresses",
|
||||
middlewares.wrap(require("./create-address").default)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import _ from "lodash"
|
||||
import {
|
||||
defaultRelations,
|
||||
defaultFields,
|
||||
allowedFields,
|
||||
allowedRelations,
|
||||
} from "../orders"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
|
||||
let selector = {
|
||||
customer_id: id,
|
||||
}
|
||||
|
||||
const limit = parseInt(req.query.limit) || 10
|
||||
const offset = parseInt(req.query.offset) || 0
|
||||
|
||||
let includeFields = []
|
||||
if ("fields" in req.query) {
|
||||
includeFields = req.query.fields.split(",")
|
||||
includeFields = includeFields.filter(f => allowedFields.includes(f))
|
||||
}
|
||||
|
||||
let expandFields = []
|
||||
if ("expand" in req.query) {
|
||||
expandFields = req.query.expand.split(",")
|
||||
expandFields = expandFields.filter(f => allowedRelations.includes(f))
|
||||
}
|
||||
|
||||
const listConfig = {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: expandFields.length ? expandFields : defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
|
||||
const [orders, count] = await orderService.listAndCount(
|
||||
selector,
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.json({ orders, count, offset, limit })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default async (req, res) => {
|
||||
)
|
||||
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.json({ customer })
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async (req, res) => {
|
||||
let customer = await customerService.update(id, value)
|
||||
|
||||
customer = await customerService.retrieve(customer.id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
@@ -18,6 +18,7 @@ export default app => {
|
||||
|
||||
export const defaultRelations = [
|
||||
"shipping_address",
|
||||
"fulfillments",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
@@ -46,3 +47,36 @@ export const defaultFields = [
|
||||
"subtotal",
|
||||
"total",
|
||||
]
|
||||
|
||||
export const allowedRelations = [
|
||||
"shipping_address",
|
||||
"fulfillments",
|
||||
"billing_address",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"customer",
|
||||
"payments",
|
||||
"region",
|
||||
]
|
||||
|
||||
export const allowedFields = [
|
||||
"id",
|
||||
"display_id",
|
||||
"cart_id",
|
||||
"customer_id",
|
||||
"email",
|
||||
"region_id",
|
||||
"currency_code",
|
||||
"tax_rate",
|
||||
"created_at",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"refunded_total",
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"total",
|
||||
]
|
||||
|
||||
@@ -13,6 +13,12 @@ export default async ({ container }) => {
|
||||
payIds = payProviders.map(p => p.getIdentifier())
|
||||
await pProviderService.registerInstalledProviders(payIds)
|
||||
|
||||
let notiIds
|
||||
const nProviderService = container.resolve("notificationService")
|
||||
const notiProviders = container.resolve("notificationProviders")
|
||||
notiIds = notiProviders.map(p => p.getIdentifier())
|
||||
await nProviderService.registerInstalledProviders(notiIds)
|
||||
|
||||
let fulfilIds
|
||||
const fProviderService = container.resolve("fulfillmentProviderService")
|
||||
const fulfilProviders = container.resolve("fulfillmentProviders")
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
BaseService,
|
||||
PaymentService,
|
||||
FulfillmentService,
|
||||
NotificationService,
|
||||
FileService,
|
||||
OauthService,
|
||||
} from "medusa-interfaces"
|
||||
@@ -228,6 +229,20 @@ async function registerServices(pluginDetails, container) {
|
||||
).singleton(),
|
||||
[`fp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof NotificationService) {
|
||||
container.registerAdd(
|
||||
"notificationProviders",
|
||||
asFunction(cradle => new loaded(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which payment provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
).singleton(),
|
||||
[`noti_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof FileService) {
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which payment provider we need to use
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class notifications1613146953072 implements MigrationInterface {
|
||||
name = "notifications1613146953072"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notification_provider" ("id" character varying NOT NULL, "is_installed" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_0425c2423e2ce9fdfd5c23761d9" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notification" ("id" character varying NOT NULL, "event_name" character varying, "resource_type" character varying NOT NULL, "resource_id" character varying NOT NULL, "customer_id" character varying, "to" character varying NOT NULL, "data" jsonb NOT NULL, "parent_id" character varying, "provider_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_df1494d263740fcfb1d09a98fc" ON "notification" ("resource_type") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ea6a358d9ce41c16499aae55f9" ON "notification" ("resource_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b5df0f53a74b9d0c0a2b652c88" ON "notification" ("customer_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" ADD CONSTRAINT "FK_b5df0f53a74b9d0c0a2b652c88d" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" ADD CONSTRAINT "FK_371db513192c083f48ba63c33be" FOREIGN KEY ("parent_id") REFERENCES "notification"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" ADD CONSTRAINT "FK_0425c2423e2ce9fdfd5c23761d9" FOREIGN KEY ("provider_id") REFERENCES "notification_provider"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" DROP CONSTRAINT "FK_0425c2423e2ce9fdfd5c23761d9"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" DROP CONSTRAINT "FK_371db513192c083f48ba63c33be"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" DROP CONSTRAINT "FK_b5df0f53a74b9d0c0a2b652c88d"`
|
||||
)
|
||||
await queryRunner.query(`DROP INDEX "IDX_b5df0f53a74b9d0c0a2b652c88"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_ea6a358d9ce41c16499aae55f9"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_df1494d263740fcfb1d09a98fc"`)
|
||||
await queryRunner.query(`DROP TABLE "notification"`)
|
||||
await queryRunner.query(`DROP TABLE "notification_provider"`)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class productTypeCategoryTags1611909563253
|
||||
export class productTypeCategoryTags1613146953073
|
||||
implements MigrationInterface {
|
||||
name = "productTypeCategoryTags1611909563253"
|
||||
name = "productTypeCategoryTags1613146953073"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Entity, Column, PrimaryColumn } from "typeorm"
|
||||
|
||||
@Entity()
|
||||
export class NotificationProvider {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ default: true })
|
||||
is_installed: boolean
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { Customer } from "./customer"
|
||||
import { NotificationProvider } from "./notification-provider"
|
||||
|
||||
@Entity()
|
||||
export class Notification {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
event_name: string
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
resource_type: string
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
resource_id: string
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
customer_id: string
|
||||
|
||||
@ManyToOne(() => Customer)
|
||||
@JoinColumn({ name: "customer_id" })
|
||||
customer: Customer
|
||||
|
||||
@Column()
|
||||
to: string
|
||||
|
||||
@Column({ type: "jsonb" })
|
||||
data: any
|
||||
|
||||
@Column({ nullable: true })
|
||||
parent_id: string
|
||||
|
||||
@ManyToOne(() => Notification)
|
||||
@JoinColumn({ name: "parent_id" })
|
||||
parent_notification: Notification
|
||||
|
||||
@OneToMany(
|
||||
() => Notification,
|
||||
noti => noti.parent_notification
|
||||
)
|
||||
resends: Notification[]
|
||||
|
||||
@Column({ nullable: true })
|
||||
provider_id: string
|
||||
|
||||
@ManyToOne(() => NotificationProvider)
|
||||
@JoinColumn({ name: "provider_id" })
|
||||
provider: NotificationProvider
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: "timestamptz" })
|
||||
updated_at: Date
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `noti_${id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { NotificationProvider } from "../models/notification-provider"
|
||||
|
||||
@EntityRepository(NotificationProvider)
|
||||
export class NotificationProviderRepository extends Repository<
|
||||
NotificationProvider
|
||||
> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { Notification } from "../models/notification"
|
||||
|
||||
@EntityRepository(Notification)
|
||||
export class NotificationRepository extends Repository<Notification> {}
|
||||
@@ -1,6 +1,9 @@
|
||||
export const EventBusServiceMock = {
|
||||
emit: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import NotificationService from "../notification"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
|
||||
describe("NotificationService", () => {
|
||||
describe("send", () => {
|
||||
const notificationRepository = MockRepository({ create: c => c })
|
||||
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
notificationRepository,
|
||||
noti_test: {
|
||||
sendNotification: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
to: "test@mail.com",
|
||||
data: { id: "something" },
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const notificationService = new NotificationService(container)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully calls provider and saves noti", async () => {
|
||||
await notificationService.send("event.test", { id: "test" }, "test")
|
||||
|
||||
expect(container.noti_test.sendNotification).toHaveBeenCalledTimes(1)
|
||||
expect(container.noti_test.sendNotification).toHaveBeenCalledWith(
|
||||
"event.test",
|
||||
{ id: "test" },
|
||||
null
|
||||
)
|
||||
|
||||
const constructed = {
|
||||
resource_type: "event",
|
||||
resource_id: "test",
|
||||
customer_id: null,
|
||||
to: "test@mail.com",
|
||||
data: { id: "something" },
|
||||
event_name: "event.test",
|
||||
provider_id: "test",
|
||||
}
|
||||
|
||||
expect(notificationRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(notificationRepository.create).toHaveBeenCalledWith(constructed)
|
||||
|
||||
expect(notificationRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(notificationRepository.save).toHaveBeenCalledWith(constructed)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,23 @@ describe("OrderService", () => {
|
||||
getRefundedTotal: o => {
|
||||
return o.refunded_total || 0
|
||||
},
|
||||
getShippingTotal: o => {
|
||||
return o.shipping_total || 0
|
||||
},
|
||||
getGiftCardTotal: o => {
|
||||
return o.gift_card_total || 0
|
||||
},
|
||||
getDiscountTotal: o => {
|
||||
return o.discount_total || 0
|
||||
},
|
||||
getTaxTotal: o => {
|
||||
return o.tax_total || 0
|
||||
},
|
||||
getSubtotal: o => {
|
||||
return o.subtotal || 0
|
||||
},
|
||||
}
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import SwapService from "../swap"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const generateOrder = (orderId, items, additional = {}) => {
|
||||
return {
|
||||
id: IdMap.getId(orderId),
|
||||
@@ -70,7 +77,9 @@ describe("SwapService", () => {
|
||||
})
|
||||
|
||||
it("fails if item is returned", async () => {
|
||||
const swapService = new SwapService({})
|
||||
const swapService = new SwapService({
|
||||
eventBusService,
|
||||
})
|
||||
const res = () =>
|
||||
swapService.validateReturnItems_(
|
||||
{
|
||||
@@ -168,6 +177,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
cartService,
|
||||
lineItemService,
|
||||
@@ -236,6 +246,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
})
|
||||
const res = swapService.createCart(IdMap.getId("swap-1"))
|
||||
@@ -274,6 +285,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
lineItemService,
|
||||
@@ -358,6 +370,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
})
|
||||
@@ -401,6 +414,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
})
|
||||
@@ -475,6 +489,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
fulfillmentService,
|
||||
lineItemService,
|
||||
@@ -595,6 +610,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
@@ -691,6 +707,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
totalsService,
|
||||
paymentProviderService,
|
||||
@@ -770,6 +787,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
|
||||
@@ -99,7 +99,7 @@ class ClaimService extends BaseService {
|
||||
const { claim_items, shipping_methods, metadata } = data
|
||||
|
||||
if (metadata) {
|
||||
claim.metadata = this.setMetadata_(claim, update.metadata)
|
||||
claim.metadata = this.setMetadata_(claim, metadata)
|
||||
await claimRepo.save(claim)
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ class CustomerService extends BaseService {
|
||||
const token = jwt.sign(payload, secret)
|
||||
// Notify subscribers
|
||||
this.eventBus_.emit(CustomerService.Events.PASSWORD_RESET, {
|
||||
id: customerId,
|
||||
email: customer.email,
|
||||
first_name: customer.first_name,
|
||||
last_name: customer.last_name,
|
||||
@@ -292,6 +293,7 @@ class CustomerService extends BaseService {
|
||||
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
password_hash,
|
||||
billing_address,
|
||||
metadata,
|
||||
@@ -314,6 +316,10 @@ class CustomerService extends BaseService {
|
||||
customer[key] = value
|
||||
}
|
||||
|
||||
if (password) {
|
||||
customer.password_hash = await this.hashPassword_(password)
|
||||
}
|
||||
|
||||
const updated = await customerRepository.save(customer)
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
|
||||
@@ -217,14 +217,18 @@ class EventBusService {
|
||||
*/
|
||||
worker_ = job => {
|
||||
const { eventName, data } = job.data
|
||||
const observers = this.observers_[eventName] || []
|
||||
const eventObservers = this.observers_[eventName] || []
|
||||
const wildcardObservers = this.observers_["*"] || []
|
||||
|
||||
const observers = eventObservers.concat(wildcardObservers)
|
||||
|
||||
this.logger_.info(
|
||||
`Processing ${eventName} which has ${observers.length} subscribers`
|
||||
`Processing ${eventName} which has ${eventObservers.length} subscribers`
|
||||
)
|
||||
|
||||
return Promise.all(
|
||||
observers.map(subscriber => {
|
||||
return subscriber(data).catch(err => {
|
||||
return subscriber(data, eventName).catch(err => {
|
||||
this.logger_.warn(
|
||||
`An error occured while processing ${eventName}: ${err}`
|
||||
)
|
||||
@@ -242,7 +246,7 @@ class EventBusService {
|
||||
|
||||
return Promise.all(
|
||||
observers.map(subscriber => {
|
||||
return subscriber(data).catch(err => {
|
||||
return subscriber(data, eventName).catch(err => {
|
||||
this.logger_.warn(
|
||||
`An error occured while processing ${eventName}: ${err}`
|
||||
)
|
||||
|
||||
@@ -89,6 +89,18 @@ class FulfillmentProviderService {
|
||||
const provider = this.retrieveProvider(option.provider_id)
|
||||
return provider.createReturn(returnOrder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches documents from the fulfillment provider
|
||||
* @param {string} providerId - the id of the provider
|
||||
* @param {object} fulfillmentData - the data relating to the fulfillment
|
||||
* @param {"invoice" | "label"} documentType - the typ of
|
||||
* document to fetch
|
||||
*/
|
||||
async retrieveDocuments(providerId, fulfillmentData, documentType) {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
return provider.retrieveDocuments(fulfillmentData, documentType)
|
||||
}
|
||||
}
|
||||
|
||||
export default FulfillmentProviderService
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import _ from "lodash"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate orchestrate notifications.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class NotificationService extends BaseService {
|
||||
constructor(container) {
|
||||
super()
|
||||
|
||||
const {
|
||||
manager,
|
||||
notificationProviderRepository,
|
||||
notificationRepository,
|
||||
logger,
|
||||
} = container
|
||||
|
||||
this.container_ = container
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
this.logger_ = logger
|
||||
|
||||
/** @private @const {NotificationRepository} */
|
||||
this.notificationRepository_ = notificationRepository
|
||||
this.notificationProviderRepository_ = notificationProviderRepository
|
||||
|
||||
this.subscribers_ = {}
|
||||
this.attachmentGenerator_ = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an attachment generator to the service. The generator can be
|
||||
* used to generate on demand invoices or other documents.
|
||||
*/
|
||||
registerAttachmentGenerator(service) {
|
||||
this.attachmentGenerator_ = service
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service's manager to a given transaction manager.
|
||||
* @parma {EntityManager} transactionManager - the manager to use
|
||||
* return {NotificationService} a cloned notification service
|
||||
*/
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new LineItemService({
|
||||
manager: transactionManager,
|
||||
notificationRepository: this.notificationRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a list of notification provider ids and persists them in the database.
|
||||
* @param {Array<string>} providers - a list of provider ids
|
||||
*/
|
||||
async registerInstalledProviders(providers) {
|
||||
const { manager, notificationProviderRepository } = this.container_
|
||||
const model = manager.getCustomRepository(notificationProviderRepository)
|
||||
model.update({}, { is_installed: false })
|
||||
for (const p of providers) {
|
||||
const n = model.create({ id: p, is_installed: true })
|
||||
await model.save(n)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of notifications.
|
||||
* @param {object} selector - the params to select the notifications by.
|
||||
* @param {object} config - the configuration to apply to the query
|
||||
* @return {Array<Notification>} the notifications that satisfy the query.
|
||||
*/
|
||||
async list(
|
||||
selector,
|
||||
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
|
||||
) {
|
||||
const notiRepo = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return notiRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a notification with a given id
|
||||
* @param {string} id - the id of the notification
|
||||
* @return {Notification} the notification
|
||||
*/
|
||||
async retrieve(id, config = {}) {
|
||||
const notiRepository = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
|
||||
const validatedId = this.validateId_(id)
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
|
||||
const notification = await notiRepository.findOne(query)
|
||||
|
||||
if (!notification) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Notification with id: ${id} was not found.`
|
||||
)
|
||||
}
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a given provider to an event.
|
||||
* @param {string} eventName - the event to subscribe to
|
||||
* @param {string} providerId - the provider that the event will be sent to
|
||||
*/
|
||||
subscribe(eventName, providerId) {
|
||||
if (typeof providerId !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"providerId must be a string"
|
||||
)
|
||||
}
|
||||
|
||||
if (this.subscribers_[eventName]) {
|
||||
this.subscribers_[eventName].push(providerId)
|
||||
} else {
|
||||
this.subscribers_[eventName] = [providerId]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a provider with a given id. Will throw a NOT_FOUND error if the
|
||||
* resolution fails.
|
||||
* @param {string} id - the id of the provider
|
||||
* @return {NotificationProvider} the notification provider
|
||||
*/
|
||||
retrieveProvider_(id) {
|
||||
try {
|
||||
return this.container_[`noti_${id}`]
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a notification provider with id: ${id}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an event by relaying the event data to the subscribing providers.
|
||||
* The result of the notification send will be persisted in the database in
|
||||
* order to allow for resends. Will log any errors that are encountered.
|
||||
* @param {string} eventName - the event to handle
|
||||
* @param {object} data - the data the event was sent with
|
||||
*/
|
||||
handleEvent(eventName, data) {
|
||||
const subs = this.subscribers_[eventName]
|
||||
if (!subs) {
|
||||
return
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
subs.map(async providerId => {
|
||||
return this.send(eventName, data, providerId).catch(err => {
|
||||
console.log(err)
|
||||
this.logger_.warn(
|
||||
`An error occured while ${providerId} was processing a notification for ${eventName}: ${err.message}`
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification, by calling the given provider's sendNotification
|
||||
* method. Persists the Notification in the database.
|
||||
* @param {string} event - the name of the event
|
||||
* @param {object} eventData - the data the event was sent with
|
||||
* @param {string} providerId - the provider that should hande the event.
|
||||
* @return {Notification} the created notification
|
||||
*/
|
||||
async send(event, eventData, providerId) {
|
||||
const provider = this.retrieveProvider_(providerId)
|
||||
const result = await provider.sendNotification(
|
||||
event,
|
||||
eventData,
|
||||
this.attachmentGenerator_
|
||||
)
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
const { to, data } = result
|
||||
const notiRepo = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
|
||||
const [resource_type] = event.split(".")
|
||||
const resource_id = eventData.id
|
||||
const customer_id = eventData.customer_id || null
|
||||
|
||||
const created = notiRepo.create({
|
||||
resource_type,
|
||||
resource_id,
|
||||
customer_id,
|
||||
to,
|
||||
data,
|
||||
event_name: event,
|
||||
provider_id: providerId,
|
||||
})
|
||||
|
||||
return notiRepo.save(created)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends a notification by retrieving a prior notification and calling the
|
||||
* underlying provider's resendNotification method.
|
||||
* @param {string} id - the id of the notification
|
||||
* @param {object} config - any configuration that might override the previous
|
||||
* send
|
||||
* @return {Notification} the newly created notification
|
||||
*/
|
||||
async resend(id, config = {}) {
|
||||
const notification = await this.retrieve(id)
|
||||
|
||||
const provider = this.retrieveProvider_(notification.provider_id)
|
||||
const { to, data } = await provider.resendNotification(
|
||||
notification,
|
||||
config,
|
||||
this.attachmentGenerator_
|
||||
)
|
||||
|
||||
const notiRepo = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
const created = notiRepo.create({
|
||||
...notification,
|
||||
to,
|
||||
data,
|
||||
parent_id: id,
|
||||
})
|
||||
|
||||
return notiRepo.save(created)
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationService
|
||||
@@ -959,7 +959,16 @@ class OrderService extends BaseService {
|
||||
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const order = await this.retrieve(orderId, {
|
||||
select: [
|
||||
"subtotal",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"gift_card_total",
|
||||
"total",
|
||||
],
|
||||
relations: [
|
||||
"discounts",
|
||||
"region",
|
||||
"fulfillments",
|
||||
"shipping_address",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MedusaError } from "medusa-core-utils"
|
||||
*/
|
||||
class SwapService extends BaseService {
|
||||
static Events = {
|
||||
CREATED: "swap.created",
|
||||
SHIPMENT_CREATED: "swap.shipment_created",
|
||||
PAYMENT_COMPLETED: "swap.payment_completed",
|
||||
PAYMENT_CAPTURED: "swap.payment_captured",
|
||||
@@ -247,6 +248,12 @@ class SwapService extends BaseService {
|
||||
order
|
||||
)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(SwapService.Events.CREATED, {
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,7 +94,10 @@ class TotalsService extends BaseService {
|
||||
|
||||
getLineItemRefund(object, lineItem) {
|
||||
const { discounts } = object
|
||||
const tax_rate = object.tax_rate || object.region.tax_rate
|
||||
const tax_rate =
|
||||
typeof object.tax_rate !== "undefined"
|
||||
? object.tax_rate
|
||||
: object.region.tax_rate
|
||||
const taxRate = (tax_rate || 0) / 100
|
||||
|
||||
const discount = discounts.find(({ rule }) => rule.type !== "free_shipping")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class NotificationSubscriber {
|
||||
constructor({ eventBusService, notificationService }) {
|
||||
this.notificationService_ = notificationService
|
||||
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
this.eventBus_.subscribe("*", this.onEvent)
|
||||
}
|
||||
|
||||
onEvent = (data, eventName) => {
|
||||
return this.notificationService_.handleEvent(eventName, data)
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationSubscriber
|
||||
@@ -23,8 +23,10 @@ fi
|
||||
|
||||
FILES_COUNT="$(git diff-tree --no-commit-id --name-only -r "$CIRCLE_BRANCH" origin/master | grep -E "$GREP_PATTERN" -c)"
|
||||
|
||||
# reset to previous state
|
||||
git reset --hard HEAD@{1}
|
||||
if [ "$IS_CI" = true ]; then
|
||||
# reset to previous state
|
||||
git reset --hard $CIRCLE_SHA1
|
||||
fi
|
||||
|
||||
if [ "$FILES_COUNT" -eq 0 ]; then
|
||||
echo "0 files matching '$GREP_PATTERN'; exiting and marking successful."
|
||||
|
||||
Reference in New Issue
Block a user