Merge branch 'release/next' of github.com:medusajs/medusa into release/next
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