1243 lines
33 KiB
JavaScript
1243 lines
33 KiB
JavaScript
import SendGrid from "@sendgrid/mail"
|
|
import { humanizeAmount, zeroDecimalCurrencies } from "medusa-core-utils"
|
|
import { NotificationService } from "medusa-interfaces"
|
|
import { IsNull, Not } from "typeorm"
|
|
import { MedusaError } from "@medusajs/utils"
|
|
|
|
class SendGridService extends NotificationService {
|
|
static identifier = "sendgrid"
|
|
|
|
/**
|
|
* @param {Object} options - options defined in `medusa-config.js`
|
|
* e.g.
|
|
* {
|
|
* api_key: SendGrid api key
|
|
* from: Medusa <hello@medusa.example>,
|
|
* order_placed_template: 01234,
|
|
* order_updated_template: 56789,
|
|
* order_canceled_template: 4242,
|
|
* user_password_reset_template: 0000,
|
|
* customer_password_reset_template: 1111,
|
|
* }
|
|
*/
|
|
constructor(
|
|
{
|
|
storeService,
|
|
orderService,
|
|
returnService,
|
|
swapService,
|
|
cartService,
|
|
lineItemService,
|
|
claimService,
|
|
fulfillmentService,
|
|
fulfillmentProviderService,
|
|
totalsService,
|
|
productVariantService,
|
|
giftCardService,
|
|
logger,
|
|
},
|
|
options
|
|
) {
|
|
super()
|
|
|
|
this.options_ = options
|
|
|
|
this.fulfillmentProviderService_ = fulfillmentProviderService
|
|
this.storeService_ = storeService
|
|
this.lineItemService_ = lineItemService
|
|
this.orderService_ = orderService
|
|
this.cartService_ = cartService
|
|
this.claimService_ = claimService
|
|
this.returnService_ = returnService
|
|
this.swapService_ = swapService
|
|
this.fulfillmentService_ = fulfillmentService
|
|
this.totalsService_ = totalsService
|
|
this.productVariantService_ = productVariantService
|
|
this.giftCardService_ = giftCardService
|
|
this.logger_ = logger
|
|
|
|
SendGrid.setApiKey(options.api_key)
|
|
}
|
|
|
|
async fetchAttachments(event, data, attachmentGenerator) {
|
|
switch (event) {
|
|
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,
|
|
}))
|
|
)
|
|
}
|
|
|
|
if (attachmentGenerator && attachmentGenerator.createReturnInvoice) {
|
|
const base64 = await attachmentGenerator.createReturnInvoice(
|
|
data.order,
|
|
data.return_request.items
|
|
)
|
|
attachments.push({
|
|
name: "invoice",
|
|
base64,
|
|
type: "application/pdf",
|
|
})
|
|
}
|
|
|
|
return attachments
|
|
}
|
|
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 "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)
|
|
case "restock-notification.restocked":
|
|
return await this.restockNotificationData(
|
|
eventData,
|
|
attachmentGenerator
|
|
)
|
|
case "order.refund_created":
|
|
return this.orderRefundCreatedData(eventData, attachmentGenerator)
|
|
default:
|
|
return eventData
|
|
}
|
|
}
|
|
|
|
getLocalizedTemplateId(event, locale) {
|
|
if (this.options_.localization && this.options_.localization[locale]) {
|
|
const map = this.options_.localization[locale]
|
|
switch (event) {
|
|
case "order.return_requested":
|
|
return map.order_return_requested_template
|
|
case "swap.shipment_created":
|
|
return map.swap_shipment_created_template
|
|
case "claim.shipment_created":
|
|
return map.claim_shipment_created_template
|
|
case "order.items_returned":
|
|
return map.order_items_returned_template
|
|
case "swap.received":
|
|
return map.swap_received_template
|
|
case "swap.created":
|
|
return map.swap_created_template
|
|
case "gift_card.created":
|
|
return map.gift_card_created_template
|
|
case "order.gift_card_created":
|
|
return map.gift_card_created_template
|
|
case "order.placed":
|
|
return map.order_placed_template
|
|
case "order.shipment_created":
|
|
return map.order_shipped_template
|
|
case "order.canceled":
|
|
return map.order_canceled_template
|
|
case "user.password_reset":
|
|
return map.user_password_reset_template
|
|
case "customer.password_reset":
|
|
return map.customer_password_reset_template
|
|
case "restock-notification.restocked":
|
|
return map.medusa_restock_template
|
|
case "order.refund_created":
|
|
return map.order_refund_created_template
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
getTemplateId(event) {
|
|
const templates = Object.keys(this.options_ ?? {})
|
|
const normalizedEvent = event.toLowerCase().replaceAll(".", "_")
|
|
const key = templates.find((template) => {
|
|
return (
|
|
normalizedEvent === template ||
|
|
`${normalizedEvent}_template` === template
|
|
)
|
|
})
|
|
return this.options_[key] ?? key
|
|
}
|
|
|
|
async sendNotification(event, eventData, attachmentGenerator) {
|
|
const data = await this.fetchData(event, eventData, attachmentGenerator)
|
|
|
|
let templateId = this.getTemplateId(event)
|
|
|
|
if (data.locale) {
|
|
templateId = this.getLocalizedTemplateId(event, data.locale) || templateId
|
|
}
|
|
|
|
if (!templateId) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Sendgrid service: No template was set for event: ${event}`
|
|
)
|
|
}
|
|
|
|
const attachments = await this.fetchAttachments(
|
|
event,
|
|
data,
|
|
attachmentGenerator
|
|
)
|
|
|
|
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,
|
|
}
|
|
})
|
|
}
|
|
|
|
let status
|
|
|
|
await SendGrid.send(sendOptions)
|
|
.then(() => {
|
|
status = "sent"
|
|
})
|
|
.catch((error) => {
|
|
status = "failed"
|
|
this.logger_.error(error)
|
|
})
|
|
|
|
// We don't want heavy docs stored in DB
|
|
delete sendOptions.attachments
|
|
|
|
return { to: data.email, status, data: sendOptions }
|
|
}
|
|
|
|
async resendNotification(notification, config, attachmentGenerator) {
|
|
const sendOptions = {
|
|
...notification.data,
|
|
to: config.to || notification.to,
|
|
}
|
|
|
|
const attachs = await this.fetchAttachments(
|
|
notification.event_name,
|
|
notification.data.dynamic_template_data,
|
|
attachmentGenerator
|
|
)
|
|
|
|
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 {Object} options - send options containing to, from, template, and more. Read more here: https://github.com/sendgrid/sendgrid-nodejs/tree/main/packages/mail
|
|
* @return {Promise} result of the send operation
|
|
*/
|
|
async sendEmail(options) {
|
|
return await SendGrid.send(options)
|
|
}
|
|
|
|
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",
|
|
"discounts.rule",
|
|
"shipping_methods",
|
|
"shipping_methods.shipping_option",
|
|
"payments",
|
|
"fulfillments",
|
|
"returns",
|
|
"gift_cards",
|
|
"gift_card_transactions",
|
|
],
|
|
})
|
|
|
|
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
|
|
relations: ["items", "tracking_links"],
|
|
})
|
|
|
|
const locale = await this.extractLocale(order)
|
|
|
|
return {
|
|
locale,
|
|
order,
|
|
date: shipment.shipped_at.toDateString(),
|
|
email: order.email,
|
|
fulfillment: shipment,
|
|
tracking_links: shipment.tracking_links,
|
|
tracking_number: shipment.tracking_numbers.join(", "),
|
|
}
|
|
}
|
|
|
|
async orderCanceledData({ 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",
|
|
"discounts.rule",
|
|
"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)
|
|
}
|
|
|
|
const locale = await this.extractLocale(order)
|
|
|
|
return {
|
|
...order,
|
|
locale,
|
|
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
|
|
)} ${currencyCode}`,
|
|
gift_card_total: `${this.humanPrice_(
|
|
gift_card_total * (1 + taxRate),
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
tax_total: `${this.humanPrice_(tax_total, currencyCode)} ${currencyCode}`,
|
|
discount_total: `${this.humanPrice_(
|
|
discount_total * (1 + taxRate),
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
shipping_total: `${this.humanPrice_(
|
|
shipping_total * (1 + taxRate),
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
total: `${this.humanPrice_(total, currencyCode)} ${currencyCode}`,
|
|
}
|
|
}
|
|
|
|
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",
|
|
"discounts.rule",
|
|
"shipping_methods",
|
|
"shipping_methods.shipping_option",
|
|
"payments",
|
|
"fulfillments",
|
|
"returns",
|
|
"gift_cards",
|
|
"gift_card_transactions",
|
|
],
|
|
})
|
|
|
|
const { tax_total, shipping_total, gift_card_total, total } = order
|
|
|
|
const currencyCode = order.currency_code.toUpperCase()
|
|
|
|
const items = await Promise.all(
|
|
order.items.map(async (i) => {
|
|
i.totals = await this.totalsService_.getLineItemTotals(i, order, {
|
|
include_tax: true,
|
|
use_tax_lines: true,
|
|
})
|
|
i.thumbnail = this.normalizeThumbUrl_(i.thumbnail)
|
|
i.discounted_price = `${this.humanPrice_(
|
|
i.totals.total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`
|
|
i.price = `${this.humanPrice_(
|
|
i.totals.original_total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`
|
|
return i
|
|
})
|
|
)
|
|
|
|
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 locale = await this.extractLocale(order)
|
|
|
|
// Includes taxes in discount amount
|
|
const discountTotal = items.reduce((acc, i) => {
|
|
return acc + i.totals.original_total - i.totals.total
|
|
}, 0)
|
|
|
|
const discounted_subtotal = items.reduce((acc, i) => {
|
|
return acc + i.totals.total
|
|
}, 0)
|
|
const subtotal = items.reduce((acc, i) => {
|
|
return acc + i.totals.original_total
|
|
}, 0)
|
|
|
|
const subtotal_ex_tax = items.reduce((total, i) => {
|
|
return total + i.totals.subtotal
|
|
}, 0)
|
|
|
|
return {
|
|
...order,
|
|
locale,
|
|
has_discounts: order.discounts.length,
|
|
has_gift_cards: order.gift_cards.length,
|
|
date: order.created_at.toDateString(),
|
|
items,
|
|
discounts,
|
|
subtotal_ex_tax: `${this.humanPrice_(
|
|
subtotal_ex_tax,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
subtotal: `${this.humanPrice_(subtotal, currencyCode)} ${currencyCode}`,
|
|
gift_card_total: `${this.humanPrice_(
|
|
gift_card_total,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
tax_total: `${this.humanPrice_(tax_total, currencyCode)} ${currencyCode}`,
|
|
discount_total: `${this.humanPrice_(
|
|
discountTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
shipping_total: `${this.humanPrice_(
|
|
shipping_total,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
total: `${this.humanPrice_(total, currencyCode)} ${currencyCode}`,
|
|
}
|
|
}
|
|
|
|
async gcCreatedData({ id }) {
|
|
const giftCard = await this.giftCardService_.retrieve(id, {
|
|
relations: ["region", "order"],
|
|
})
|
|
const taxRate = giftCard.region.tax_rate / 100
|
|
const locale = giftCard.order
|
|
? await this.extractLocale(giftCard.order)
|
|
: null
|
|
const email = giftCard.order
|
|
? giftCard.order.email
|
|
: giftCard.metadata.email
|
|
|
|
return {
|
|
...giftCard,
|
|
locale,
|
|
email,
|
|
display_value: `${this.humanPrice_(
|
|
giftCard.value * 1 + taxRate,
|
|
giftCard.region.currency_code
|
|
)} ${giftCard.region.currency_code}`,
|
|
message:
|
|
giftCard.metadata?.message || giftCard.metadata?.personal_message,
|
|
}
|
|
}
|
|
|
|
async returnRequestedData({ id, return_id }) {
|
|
// Fetch the return request
|
|
const returnRequest = await this.returnService_.retrieve(return_id, {
|
|
relations: [
|
|
"items.item.tax_lines",
|
|
"items.item.variant.product.profiles",
|
|
"shipping_method",
|
|
"shipping_method.tax_lines",
|
|
"shipping_method.shipping_option",
|
|
],
|
|
})
|
|
|
|
const items = await this.lineItemService_.list(
|
|
{
|
|
id: returnRequest.items.map(({ item_id }) => item_id),
|
|
},
|
|
{
|
|
relations: ["tax_lines", "variant", "variant.product.profiles"],
|
|
}
|
|
)
|
|
|
|
// Fetch the order
|
|
const order = await this.orderService_.retrieve(id, {
|
|
select: ["total"],
|
|
relations: [
|
|
"items",
|
|
"items.variant",
|
|
"items.tax_lines",
|
|
"discounts",
|
|
"discounts.rule",
|
|
"shipping_address",
|
|
"returns",
|
|
],
|
|
})
|
|
|
|
const currencyCode = order.currency_code.toUpperCase()
|
|
|
|
// Calculate which items are in the return
|
|
const returnItems = await Promise.all(
|
|
returnRequest.items.map(async (i) => {
|
|
const found = items.find((oi) => oi.id === i.item_id)
|
|
found.quantity = i.quantity
|
|
found.thumbnail = this.normalizeThumbUrl_(found.thumbnail)
|
|
found.totals = await this.totalsService_.getLineItemTotals(
|
|
found,
|
|
order,
|
|
{
|
|
include_tax: true,
|
|
use_tax_lines: true,
|
|
}
|
|
)
|
|
found.price = `${this.humanPrice_(
|
|
found.totals.total,
|
|
currencyCode
|
|
)} ${currencyCode}`
|
|
found.tax_lines = found.totals.tax_lines
|
|
return found
|
|
})
|
|
)
|
|
|
|
// Get total of the returned products
|
|
const item_subtotal = returnItems.reduce(
|
|
(acc, next) => acc + next.totals.total,
|
|
0
|
|
)
|
|
|
|
// If the return has a shipping method get the price and any attachments
|
|
let shippingTotal = 0
|
|
if (returnRequest.shipping_method) {
|
|
const base = returnRequest.shipping_method.price
|
|
shippingTotal =
|
|
base +
|
|
returnRequest.shipping_method.tax_lines.reduce((acc, next) => {
|
|
return Math.round(acc + base * (next.rate / 100))
|
|
}, 0)
|
|
}
|
|
|
|
const locale = await this.extractLocale(order)
|
|
|
|
return {
|
|
locale,
|
|
has_shipping: !!returnRequest.shipping_method,
|
|
email: order.email,
|
|
items: returnItems,
|
|
subtotal: `${this.humanPrice_(
|
|
item_subtotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
shipping_total: `${this.humanPrice_(
|
|
shippingTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
refund_amount: `${this.humanPrice_(
|
|
returnRequest.refund_amount,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
return_request: {
|
|
...returnRequest,
|
|
refund_amount: `${this.humanPrice_(
|
|
returnRequest.refund_amount,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
},
|
|
order,
|
|
date: returnRequest.updated_at.toDateString(),
|
|
}
|
|
}
|
|
|
|
async swapReceivedData({ id }) {
|
|
const store = await this.storeService_.retrieve()
|
|
|
|
const swap = await this.swapService_.retrieve(id, {
|
|
relations: [
|
|
"additional_items",
|
|
"additional_items.tax_lines",
|
|
"additional_items.variant",
|
|
"return_order",
|
|
"return_order.items",
|
|
"return_order.items.item",
|
|
"return_order.items.item.variant",
|
|
"return_order.shipping_method",
|
|
"return_order.shipping_method.shipping_option",
|
|
],
|
|
})
|
|
|
|
const returnRequest = swap.return_order
|
|
const items = await this.lineItemService_.list(
|
|
{
|
|
id: returnRequest.items.map(({ item_id }) => item_id),
|
|
},
|
|
{
|
|
relations: ["tax_lines"],
|
|
}
|
|
)
|
|
|
|
returnRequest.items = returnRequest.items.map((item) => {
|
|
const found = items.find((i) => i.id === item.item_id)
|
|
return {
|
|
...item,
|
|
item: found,
|
|
}
|
|
})
|
|
|
|
const swapLink = store.swap_link_template.replace(
|
|
/\{cart_id\}/,
|
|
swap.cart_id
|
|
)
|
|
|
|
const order = await this.orderService_.retrieve(swap.order_id, {
|
|
select: ["total"],
|
|
relations: [
|
|
"items",
|
|
"items.variant",
|
|
"discounts",
|
|
"discounts.rule",
|
|
"shipping_address",
|
|
"swaps",
|
|
"swaps.additional_items",
|
|
"swaps.additional_items.tax_lines",
|
|
"swaps.additional_items.variant",
|
|
],
|
|
})
|
|
|
|
const cart = await this.cartService_.retrieve(swap.cart_id, {
|
|
relations: ["items.variant.product.profiles"],
|
|
select: [
|
|
"total",
|
|
"tax_total",
|
|
"discount_total",
|
|
"shipping_total",
|
|
"subtotal",
|
|
],
|
|
})
|
|
|
|
const currencyCode = order.currency_code.toUpperCase()
|
|
const decoratedItems = await Promise.all(
|
|
cart.items.map(async (i) => {
|
|
const totals = await this.totalsService_.getLineItemTotals(i, cart, {
|
|
include_tax: true,
|
|
})
|
|
|
|
return {
|
|
...i,
|
|
totals,
|
|
price: this.humanPrice_(
|
|
totals.subtotal + totals.tax_total,
|
|
currencyCode
|
|
),
|
|
}
|
|
})
|
|
)
|
|
|
|
const returnTotal = decoratedItems.reduce((acc, next) => {
|
|
if (next.is_return) {
|
|
return acc + -1 * (next.totals.subtotal + next.totals.tax_total)
|
|
}
|
|
return acc
|
|
}, 0)
|
|
|
|
const additionalTotal = decoratedItems.reduce((acc, next) => {
|
|
if (!next.is_return) {
|
|
return acc + next.totals.subtotal + next.totals.tax_total
|
|
}
|
|
return acc
|
|
}, 0)
|
|
|
|
const refundAmount = swap.return_order.refund_amount
|
|
|
|
const locale = await this.extractLocale(order)
|
|
|
|
return {
|
|
locale,
|
|
swap,
|
|
order,
|
|
return_request: returnRequest,
|
|
date: swap.updated_at.toDateString(),
|
|
swap_link: swapLink,
|
|
email: order.email,
|
|
items: decoratedItems.filter((di) => !di.is_return),
|
|
return_items: decoratedItems.filter((di) => di.is_return),
|
|
return_total: `${this.humanPrice_(
|
|
returnTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
tax_total: `${this.humanPrice_(
|
|
cart.total,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
refund_amount: `${this.humanPrice_(
|
|
refundAmount,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
additional_total: `${this.humanPrice_(
|
|
additionalTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
}
|
|
}
|
|
|
|
async swapCreatedData({ id }) {
|
|
const store = await this.storeService_.retrieve({
|
|
where: { id: Not(IsNull()) },
|
|
})
|
|
const swap = await this.swapService_.retrieve(id, {
|
|
relations: [
|
|
"additional_items.variant.product.profiles",
|
|
"additional_items.tax_lines",
|
|
"return_order",
|
|
"return_order.items",
|
|
"return_order.items.item",
|
|
"return_order.shipping_method",
|
|
"return_order.shipping_method.shipping_option",
|
|
],
|
|
})
|
|
|
|
const returnRequest = swap.return_order
|
|
|
|
const items = await this.lineItemService_.list(
|
|
{
|
|
id: returnRequest.items.map(({ item_id }) => item_id),
|
|
},
|
|
{
|
|
relations: ["tax_lines", "variant.product.profiles"],
|
|
}
|
|
)
|
|
|
|
returnRequest.items = returnRequest.items.map((item) => {
|
|
const found = items.find((i) => i.id === item.item_id)
|
|
return {
|
|
...item,
|
|
item: found,
|
|
}
|
|
})
|
|
|
|
const swapLink = store.swap_link_template.replace(
|
|
/\{cart_id\}/,
|
|
swap.cart_id
|
|
)
|
|
|
|
const order = await this.orderService_.retrieve(swap.order_id, {
|
|
select: ["total"],
|
|
relations: [
|
|
"items.variant.product.profiles",
|
|
"items.tax_lines",
|
|
"discounts",
|
|
"discounts.rule",
|
|
"shipping_address",
|
|
"swaps",
|
|
"swaps.additional_items",
|
|
"swaps.additional_items.tax_lines",
|
|
"swaps.additional_items.variant",
|
|
],
|
|
})
|
|
|
|
const cart = await this.cartService_.retrieve(swap.cart_id, {
|
|
select: [
|
|
"total",
|
|
"tax_total",
|
|
"discount_total",
|
|
"shipping_total",
|
|
"subtotal",
|
|
],
|
|
relations: ["items.variant.product.profiles"],
|
|
})
|
|
const currencyCode = order.currency_code.toUpperCase()
|
|
|
|
const decoratedItems = await Promise.all(
|
|
cart.items.map(async (i) => {
|
|
const totals = await this.totalsService_.getLineItemTotals(i, cart, {
|
|
include_tax: true,
|
|
})
|
|
|
|
return {
|
|
...i,
|
|
totals,
|
|
tax_lines: totals.tax_lines,
|
|
price: `${this.humanPrice_(
|
|
totals.original_total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
discounted_price: `${this.humanPrice_(
|
|
totals.total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
}
|
|
})
|
|
)
|
|
|
|
const returnTotal = decoratedItems.reduce((acc, next) => {
|
|
const { total } = next.totals
|
|
if (next.is_return && next.variant_id) {
|
|
return acc + -1 * total
|
|
}
|
|
return acc
|
|
}, 0)
|
|
|
|
const additionalTotal = decoratedItems.reduce((acc, next) => {
|
|
const { total } = next.totals
|
|
if (!next.is_return) {
|
|
return acc + total
|
|
}
|
|
return acc
|
|
}, 0)
|
|
|
|
const refundAmount = swap.return_order.refund_amount
|
|
|
|
const locale = await this.extractLocale(order)
|
|
|
|
return {
|
|
locale,
|
|
swap,
|
|
order,
|
|
return_request: returnRequest,
|
|
date: swap.updated_at.toDateString(),
|
|
swap_link: swapLink,
|
|
email: order.email,
|
|
items: decoratedItems.filter((di) => !di.is_return),
|
|
return_items: decoratedItems.filter((di) => di.is_return),
|
|
return_total: `${this.humanPrice_(
|
|
returnTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
refund_amount: `${this.humanPrice_(
|
|
refundAmount,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
additional_total: `${this.humanPrice_(
|
|
additionalTotal,
|
|
currencyCode
|
|
)} ${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",
|
|
"shipping_methods.shipping_option",
|
|
"shipping_methods.tax_lines",
|
|
"additional_items.variant.product.profiles",
|
|
"additional_items.tax_lines",
|
|
"return_order",
|
|
"return_order.items",
|
|
],
|
|
})
|
|
|
|
const order = await this.orderService_.retrieve(swap.order_id, {
|
|
relations: [
|
|
"region",
|
|
"items",
|
|
"items.tax_lines",
|
|
"items.variant.product.profiles",
|
|
"discounts",
|
|
"discounts.rule",
|
|
"swaps",
|
|
"swaps.additional_items.variant.product.profiles",
|
|
"swaps.additional_items.tax_lines",
|
|
],
|
|
})
|
|
|
|
const cart = await this.cartService_.retrieve(swap.cart_id, {
|
|
select: [
|
|
"total",
|
|
"tax_total",
|
|
"discount_total",
|
|
"shipping_total",
|
|
"subtotal",
|
|
],
|
|
relations: ["items.variant.product.profiles"],
|
|
})
|
|
|
|
const returnRequest = swap.return_order
|
|
const items = await this.lineItemService_.list(
|
|
{
|
|
id: returnRequest.items.map(({ item_id }) => item_id),
|
|
},
|
|
{
|
|
relations: ["tax_lines", "variant.product.profiles"],
|
|
}
|
|
)
|
|
|
|
const taxRate = order.tax_rate / 100
|
|
const currencyCode = order.currency_code.toUpperCase()
|
|
|
|
const returnItems = await Promise.all(
|
|
swap.return_order.items.map(async (i) => {
|
|
const found = items.find((oi) => oi.id === i.item_id)
|
|
const totals = await this.totalsService_.getLineItemTotals(i, cart, {
|
|
include_tax: true,
|
|
})
|
|
|
|
return {
|
|
...found,
|
|
thumbnail: this.normalizeThumbUrl_(found.thumbnail),
|
|
price: `${this.humanPrice_(
|
|
totals.original_total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
discounted_price: `${this.humanPrice_(
|
|
totals.total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
quantity: i.quantity,
|
|
}
|
|
})
|
|
)
|
|
|
|
const returnTotal = await this.totalsService_.getRefundTotal(
|
|
order,
|
|
returnItems
|
|
)
|
|
|
|
const constructedOrder = {
|
|
...order,
|
|
shipping_methods: swap.shipping_methods,
|
|
items: swap.additional_items,
|
|
}
|
|
|
|
const additionalTotal = await this.totalsService_.getTotal(constructedOrder)
|
|
|
|
const refundAmount = swap.return_order.refund_amount
|
|
|
|
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
|
|
relations: ["tracking_links"],
|
|
})
|
|
|
|
const locale = await this.extractLocale(order)
|
|
|
|
return {
|
|
locale,
|
|
swap,
|
|
order,
|
|
items: await Promise.all(
|
|
swap.additional_items.map(async (i) => {
|
|
const totals = await this.totalsService_.getLineItemTotals(i, cart, {
|
|
include_tax: true,
|
|
})
|
|
|
|
return {
|
|
...i,
|
|
thumbnail: this.normalizeThumbUrl_(i.thumbnail),
|
|
price: `${this.humanPrice_(
|
|
totals.original_total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
discounted_price: `${this.humanPrice_(
|
|
totals.total / i.quantity,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
quantity: i.quantity,
|
|
}
|
|
})
|
|
),
|
|
date: swap.updated_at.toDateString(),
|
|
email: order.email,
|
|
tax_amount: `${this.humanPrice_(
|
|
cart.tax_total,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
paid_total: `${this.humanPrice_(
|
|
swap.difference_due,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
return_total: `${this.humanPrice_(
|
|
returnTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
refund_amount: `${this.humanPrice_(
|
|
refundAmount,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
additional_total: `${this.humanPrice_(
|
|
additionalTotal,
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
fulfillment: shipment,
|
|
tracking_links: shipment.tracking_links,
|
|
tracking_number: shipment.tracking_numbers.join(", "),
|
|
}
|
|
}
|
|
|
|
async claimShipmentCreatedData({ id, fulfillment_id }) {
|
|
const claim = await this.claimService_.retrieve(id, {
|
|
relations: [
|
|
"order.items.variant.product.profiles",
|
|
"order.shipping_address",
|
|
],
|
|
})
|
|
|
|
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
|
|
relations: ["tracking_links"],
|
|
})
|
|
|
|
const locale = await this.extractLocale(claim.order)
|
|
|
|
return {
|
|
locale,
|
|
email: claim.order.email,
|
|
claim,
|
|
order: claim.order,
|
|
fulfillment: shipment,
|
|
tracking_links: shipment.tracking_links,
|
|
tracking_number: shipment.tracking_numbers.join(", "),
|
|
}
|
|
}
|
|
|
|
async restockNotificationData({ variant_id, emails }) {
|
|
const variant = await this.productVariantService_.retrieve(variant_id, {
|
|
relations: ["product"],
|
|
})
|
|
|
|
let thumb
|
|
if (variant.product.thumbnail) {
|
|
thumb = this.normalizeThumbUrl_(variant.product.thumbnail)
|
|
}
|
|
|
|
return {
|
|
product: {
|
|
...variant.product,
|
|
thumbnail: thumb,
|
|
},
|
|
variant,
|
|
variant_id,
|
|
emails,
|
|
}
|
|
}
|
|
|
|
userPasswordResetData(data) {
|
|
return data
|
|
}
|
|
|
|
customerPasswordResetData(data) {
|
|
return data
|
|
}
|
|
|
|
async orderRefundCreatedData({ id, refund_id }) {
|
|
const order = await this.orderService_.retrieveWithTotals(id, {
|
|
relations: ["refunds", "items"],
|
|
})
|
|
|
|
const refund = order.refunds.find((refund) => refund.id === refund_id)
|
|
|
|
return {
|
|
order,
|
|
refund,
|
|
refund_amount: `${this.humanPrice_(refund.amount, order.currency_code)} ${
|
|
order.currency_code
|
|
}`,
|
|
email: order.email,
|
|
}
|
|
}
|
|
|
|
processItems_(items, taxRate, currencyCode) {
|
|
return items.map((i) => {
|
|
return {
|
|
...i,
|
|
thumbnail: this.normalizeThumbUrl_(i.thumbnail),
|
|
price: `${this.humanPrice_(
|
|
i.unit_price * (1 + taxRate),
|
|
currencyCode
|
|
)} ${currencyCode}`,
|
|
}
|
|
})
|
|
}
|
|
|
|
humanPrice_(amount, currency) {
|
|
if (!amount) {
|
|
return "0.00"
|
|
}
|
|
|
|
const normalized = humanizeAmount(amount, currency)
|
|
return normalized.toFixed(
|
|
zeroDecimalCurrencies.includes(currency.toLowerCase()) ? 0 : 2
|
|
)
|
|
}
|
|
|
|
normalizeThumbUrl_(url) {
|
|
if (!url) {
|
|
return null
|
|
}
|
|
|
|
if (url.startsWith("http")) {
|
|
return url
|
|
} else if (url.startsWith("//")) {
|
|
return `https:${url}`
|
|
}
|
|
return url
|
|
}
|
|
|
|
async extractLocale(fromOrder) {
|
|
if (fromOrder.cart_id) {
|
|
try {
|
|
const cart = await this.cartService_.retrieve(fromOrder.cart_id, {
|
|
select: ["id", "context"],
|
|
})
|
|
|
|
if (cart.context && cart.context.locale) {
|
|
return cart.context.locale
|
|
}
|
|
} catch (err) {
|
|
console.log(err)
|
|
console.warn("Failed to gather context for order")
|
|
return null
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|
|
export default SendGridService
|