Merge branch 'release/next' of github.com:medusajs/medusa into release/next

This commit is contained in:
Sebastian Rindom
2021-02-15 11:57:38 +01:00
46 changed files with 1538 additions and 254 deletions
@@ -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 []
}
}
/**
+1
View File
@@ -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",
]
+6
View File
@@ -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")
+15
View File
@@ -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"`)
}
}
@@ -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() {
+19 -1
View File
@@ -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,
+1 -1
View File
@@ -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)
}
+6
View File
@@ -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)
+8 -4
View File
@@ -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
+9
View File
@@ -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",
+7
View File
@@ -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
})
}
+4 -1
View File
@@ -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
+4 -2
View File
@@ -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."