medusa-plugin-ses 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1292 @@
1
+ import * as aws from '@aws-sdk/client-ses'
2
+ import Handlebars from 'handlebars'
3
+ import nodemailer from 'nodemailer'
4
+ import path from 'path'
5
+ import fs from 'fs'
6
+ import { humanizeAmount, zeroDecimalCurrencies } from 'medusa-core-utils'
7
+ import { NotificationService } from 'medusa-interfaces'
8
+
9
+ class SESService extends NotificationService {
10
+ static identifier = "ses"
11
+
12
+ /**
13
+ * @param {Object} options - options defined in `medusa-config.js`
14
+ * e.g.
15
+ * {
16
+ * access_key_id: process.env.SES_ACCESS_KEY_ID,
17
+ * secret_access_key: process.env.SES_SECRET_ACCESS_KEY,
18
+ * region: process.env.SES_REGION,
19
+ * from: process.env.SES_FROM,
20
+ * enable_endpoint: process.env.SES_ENABLE_ENDPOINT,
21
+ * template_path: process.env.SES_TEMPLATE_PATH,
22
+ * order_placed_template: 'order_placed',
23
+ * }
24
+ */
25
+ constructor({
26
+ storeService,
27
+ orderService,
28
+ returnService,
29
+ swapService,
30
+ cartService,
31
+ lineItemService,
32
+ claimService,
33
+ fulfillmentService,
34
+ fulfillmentProviderService,
35
+ totalsService,
36
+ productVariantService,
37
+ }, options) {
38
+
39
+ super()
40
+
41
+ this.options_ = options
42
+
43
+ this.fulfillmentProviderService_ = fulfillmentProviderService
44
+ this.storeService_ = storeService
45
+ this.lineItemService_ = lineItemService
46
+ this.orderService_ = orderService
47
+ this.cartService_ = cartService
48
+ this.claimService_ = claimService
49
+ this.returnService_ = returnService
50
+ this.swapService_ = swapService
51
+ this.fulfillmentService_ = fulfillmentService
52
+ this.totalsService_ = totalsService
53
+ this.productVariantService_ = productVariantService
54
+
55
+ const ses = new aws.SES({
56
+ region: options.region,
57
+ credentials: {
58
+ accessKeyId: options.access_key_id,
59
+ secretAccessKey: options.secret_access_key,
60
+ },
61
+ })
62
+
63
+ this.transporter_ = nodemailer.createTransport({
64
+ SES: { ses, aws }
65
+ })
66
+ }
67
+
68
+ async fetchAttachments(event, data, attachmentGenerator) {
69
+ switch (event) {
70
+ case "swap.created":
71
+ case "order.return_requested": {
72
+ let attachments = []
73
+ const { shipping_method, shipping_data } = data.return_request
74
+ if (shipping_method) {
75
+ const provider = shipping_method.shipping_option.provider_id
76
+
77
+ const lbl = await this.fulfillmentProviderService_.retrieveDocuments(
78
+ provider,
79
+ shipping_data,
80
+ "label"
81
+ )
82
+
83
+ attachments = attachments.concat(
84
+ lbl.map((d) => ({
85
+ name: "return-label",
86
+ base64: d.base_64,
87
+ type: d.type,
88
+ }))
89
+ )
90
+ }
91
+
92
+ if (attachmentGenerator && attachmentGenerator.createReturnInvoice) {
93
+ const base64 = await attachmentGenerator.createReturnInvoice(
94
+ data.order,
95
+ data.return_request.items
96
+ )
97
+ attachments.push({
98
+ name: "invoice",
99
+ base64,
100
+ type: "application/pdf",
101
+ })
102
+ }
103
+
104
+ return attachments
105
+ }
106
+ default:
107
+ return []
108
+ }
109
+ }
110
+
111
+ async fetchData(event, eventData, attachmentGenerator) {
112
+ switch (event) {
113
+ case "order.return_requested":
114
+ return this.returnRequestedData(eventData, attachmentGenerator)
115
+ case "swap.shipment_created":
116
+ return this.swapShipmentCreatedData(eventData, attachmentGenerator)
117
+ case "claim.shipment_created":
118
+ return this.claimShipmentCreatedData(eventData, attachmentGenerator)
119
+ case "order.items_returned":
120
+ return this.itemsReturnedData(eventData, attachmentGenerator)
121
+ case "swap.received":
122
+ return this.swapReceivedData(eventData, attachmentGenerator)
123
+ case "swap.created":
124
+ return this.swapCreatedData(eventData, attachmentGenerator)
125
+ case "gift_card.created":
126
+ return this.gcCreatedData(eventData, attachmentGenerator)
127
+ case "order.gift_card_created":
128
+ return this.gcCreatedData(eventData, attachmentGenerator)
129
+ case "order.placed":
130
+ return this.orderPlacedData(eventData, attachmentGenerator)
131
+ case "order.shipment_created":
132
+ return this.orderShipmentCreatedData(eventData, attachmentGenerator)
133
+ case "order.canceled":
134
+ return this.orderCanceledData(eventData, attachmentGenerator)
135
+ case "user.password_reset":
136
+ return this.userPasswordResetData(eventData, attachmentGenerator)
137
+ case "customer.password_reset":
138
+ return this.customerPasswordResetData(eventData, attachmentGenerator)
139
+ case "restock-notification.restocked":
140
+ return await this.restockNotificationData(
141
+ eventData,
142
+ attachmentGenerator
143
+ )
144
+ case "order.refund_created":
145
+ return this.orderRefundCreatedData(eventData, attachmentGenerator)
146
+ default:
147
+ return {}
148
+ }
149
+ }
150
+
151
+ getLocalizedTemplateId(event, locale) {
152
+ if (this.options_.localization && this.options_.localization[locale]) {
153
+ const map = this.options_.localization[locale]
154
+ switch (event) {
155
+ case "order.return_requested":
156
+ return map.order_return_requested_template
157
+ case "swap.shipment_created":
158
+ return map.swap_shipment_created_template
159
+ case "claim.shipment_created":
160
+ return map.claim_shipment_created_template
161
+ case "order.items_returned":
162
+ return map.order_items_returned_template
163
+ case "swap.received":
164
+ return map.swap_received_template
165
+ case "swap.created":
166
+ return map.swap_created_template
167
+ case "gift_card.created":
168
+ return map.gift_card_created_template
169
+ case "order.gift_card_created":
170
+ return map.gift_card_created_template
171
+ case "order.placed":
172
+ return map.order_placed_template
173
+ case "order.shipment_created":
174
+ return map.order_shipped_template
175
+ case "order.canceled":
176
+ return map.order_canceled_template
177
+ case "user.password_reset":
178
+ return map.user_password_reset_template
179
+ case "customer.password_reset":
180
+ return map.customer_password_reset_template
181
+ case "restock-notification.restocked":
182
+ return map.medusa_restock_template
183
+ case "order.refund_created":
184
+ return map.order_refund_created_template
185
+ default:
186
+ return null
187
+ }
188
+ }
189
+ return null
190
+ }
191
+
192
+ getTemplateId(event) {
193
+ switch (event) {
194
+ case "order.return_requested":
195
+ return this.options_.order_return_requested_template
196
+ case "swap.shipment_created":
197
+ return this.options_.swap_shipment_created_template
198
+ case "claim.shipment_created":
199
+ return this.options_.claim_shipment_created_template
200
+ case "order.items_returned":
201
+ return this.options_.order_items_returned_template
202
+ case "swap.received":
203
+ return this.options_.swap_received_template
204
+ case "swap.created":
205
+ return this.options_.swap_created_template
206
+ case "gift_card.created":
207
+ return this.options_.gift_card_created_template
208
+ case "order.gift_card_created":
209
+ return this.options_.gift_card_created_template
210
+ case "order.placed":
211
+ return this.options_.order_placed_template
212
+ case "order.shipment_created":
213
+ return this.options_.order_shipped_template
214
+ case "order.canceled":
215
+ return this.options_.order_canceled_template
216
+ case "user.password_reset":
217
+ return this.options_.user_password_reset_template
218
+ case "customer.password_reset":
219
+ return this.options_.customer_password_reset_template
220
+ case "restock-notification.restocked":
221
+ return this.options_.medusa_restock_template
222
+ case "order.refund_created":
223
+ return this.options_.order_refund_created_template
224
+ default:
225
+ return null
226
+ }
227
+ }
228
+
229
+ async compileTemplate(templateId, data) {
230
+ const base = path.resolve(__dirname, '../../', this.options_.template_path, templateId)
231
+ // The absolute path below is useful when using yarn link or npm link
232
+ //const base = path.resolve(this.options_.template_path, templateId)
233
+ const subjectTemplate = Handlebars.compile(fs.readFileSync(path.join(base, 'subject.hbs'), "utf8"))
234
+ const htmlTemplate = Handlebars.compile(fs.readFileSync(path.join(base, 'html.hbs'), "utf8"))
235
+ const textTemplate = Handlebars.compile(fs.readFileSync(path.join(base, 'text.hbs'), "utf8"))
236
+ return {
237
+ subject: subjectTemplate(data),
238
+ html: htmlTemplate(data),
239
+ text: textTemplate(data)
240
+ }
241
+ }
242
+
243
+ async sendNotification(event, eventData, attachmentGenerator) {
244
+ let templateId = this.getTemplateId(event)
245
+ if (!templateId) { return false }
246
+ if (data.locale) {
247
+ templateId = this.getLocalizedTemplateId(event, data.locale) || templateId
248
+ }
249
+
250
+ const data = await this.fetchData(event, eventData, attachmentGenerator)
251
+ if (!data) { return false }
252
+
253
+ const { subject, html, text } = await this.compileTemplate(templateId, data)
254
+ if (!subject || (!html && !text)) { return false }
255
+
256
+ const sendOptions = {
257
+ from: this.options_.from,
258
+ to: data.email,
259
+ subject,
260
+ html,
261
+ text
262
+ }
263
+ console.log(sendOptions)
264
+
265
+ const attachments = await this.fetchAttachments(
266
+ event,
267
+ data,
268
+ attachmentGenerator
269
+ )
270
+
271
+ if (attachments?.length) {
272
+ sendOptions.has_attachments = true
273
+ sendOptions.attachments = attachments.map((a) => {
274
+ return {
275
+ content: a.base64,
276
+ filename: a.name,
277
+ encoding: 'base64',
278
+ contentType: a.type
279
+ }
280
+ })
281
+ }
282
+
283
+ //const status = await this.transporter_.sendMail(sendOptions).then(() => "sent").catch(() => "failed")
284
+ let status
285
+ await this.transporter_.sendMail(sendOptions)
286
+ .then(() => { status = "sent" })
287
+ .catch((error) => { status = "failed"; console.log(error) })
288
+
289
+ // We don't want heavy docs stored in DB
290
+ delete sendOptions.attachments
291
+
292
+ return { to: data.email, status, data: sendOptions }
293
+ }
294
+
295
+ async resendNotification(notification, config, attachmentGenerator) {
296
+ const sendOptions = {
297
+ ...notification.data,
298
+ to: config.to || notification.to,
299
+ }
300
+
301
+ const attachs = await this.fetchAttachments(
302
+ notification.event_name,
303
+ notification.data.dynamic_template_data,
304
+ attachmentGenerator
305
+ )
306
+
307
+ sendOptions.attachments = attachs.map((a) => {
308
+ return {
309
+ content: a.base64,
310
+ filename: a.name,
311
+ encoding: 'base64',
312
+ contentType: a.type
313
+ }
314
+ })
315
+
316
+ //const status = await this.transporter_.sendMail(sendOptions).then(() => "sent").catch(() => "failed")
317
+ let status
318
+ await this.transporter_.sendMail(sendOptions)
319
+ .then(() => { status = "sent" })
320
+ .catch((error) => { status = "failed"; console.log(error) })
321
+
322
+ return { to: sendOptions.to, status, data: sendOptions }
323
+ }
324
+
325
+ /**
326
+ * Sends an email using SES.
327
+ * @param {string} template_id - id of template to use
328
+ * @param {string} from - sender of email
329
+ * @param {string} to - receiver of email
330
+ * @param {Object} data - data to send in mail (match with template)
331
+ * @return {Promise} result of the send operation
332
+ */
333
+ async sendEmail(template_id, from, to, data) {
334
+ // This function is used by the /ses/send API endpoint included in this plugin.
335
+ // It is disabled by default.
336
+ // This endpoint may be useful for testing purposes and for use by related applications.
337
+ // There is NO SECURITY on the endpoint by default.
338
+ // Most people will NOT need to enable it.
339
+ // If you are certain that you want to enable it and that you know what you are doing,
340
+ // set the environment variable SES_ENABLE_ENDPOINT to "42" (a string, not an int).
341
+ // The unsual setting is meant to prevent enabling by accident or without thought.
342
+ if (this.options_.enable_endpoint !== '42') { return false }
343
+ const { subject, html, text } = await this.compileTemplate(template_id, data)
344
+ if (!subject || (!html && !text)) { return false }
345
+ try {
346
+ return this.transporter_.sendMail({
347
+ from: from,
348
+ to: to,
349
+ subject,
350
+ html,
351
+ text
352
+ })
353
+ } catch (error) {
354
+ throw error
355
+ }
356
+ }
357
+
358
+ async orderShipmentCreatedData({ id, fulfillment_id }, attachmentGenerator) {
359
+ const order = await this.orderService_.retrieve(id, {
360
+ select: [
361
+ "shipping_total",
362
+ "discount_total",
363
+ "tax_total",
364
+ "refunded_total",
365
+ "gift_card_total",
366
+ "subtotal",
367
+ "total",
368
+ "refundable_amount",
369
+ ],
370
+ relations: [
371
+ "customer",
372
+ "billing_address",
373
+ "shipping_address",
374
+ "discounts",
375
+ "discounts.rule",
376
+ "shipping_methods",
377
+ "shipping_methods.shipping_option",
378
+ "payments",
379
+ "fulfillments",
380
+ "returns",
381
+ "gift_cards",
382
+ "gift_card_transactions",
383
+ ],
384
+ })
385
+
386
+ const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
387
+ relations: ["items", "tracking_links"],
388
+ })
389
+
390
+ const locale = await this.extractLocale(order)
391
+
392
+ return {
393
+ locale,
394
+ order,
395
+ date: shipment.shipped_at.toDateString(),
396
+ email: order.email,
397
+ fulfillment: shipment,
398
+ tracking_links: shipment.tracking_links,
399
+ tracking_number: shipment.tracking_numbers.join(", "),
400
+ }
401
+ }
402
+
403
+ async orderCanceledData({ id }) {
404
+ const order = await this.orderService_.retrieve(id, {
405
+ select: [
406
+ "shipping_total",
407
+ "discount_total",
408
+ "tax_total",
409
+ "refunded_total",
410
+ "gift_card_total",
411
+ "subtotal",
412
+ "total",
413
+ ],
414
+ relations: [
415
+ "customer",
416
+ "billing_address",
417
+ "shipping_address",
418
+ "discounts",
419
+ "discounts.rule",
420
+ "shipping_methods",
421
+ "shipping_methods.shipping_option",
422
+ "payments",
423
+ "fulfillments",
424
+ "returns",
425
+ "gift_cards",
426
+ "gift_card_transactions",
427
+ ],
428
+ })
429
+
430
+ const {
431
+ subtotal,
432
+ tax_total,
433
+ discount_total,
434
+ shipping_total,
435
+ gift_card_total,
436
+ total,
437
+ } = order
438
+
439
+ const taxRate = order.tax_rate / 100
440
+ const currencyCode = order.currency_code.toUpperCase()
441
+
442
+ const items = this.processItems_(order.items, taxRate, currencyCode)
443
+
444
+ let discounts = []
445
+ if (order.discounts) {
446
+ discounts = order.discounts.map((discount) => {
447
+ return {
448
+ is_giftcard: false,
449
+ code: discount.code,
450
+ descriptor: `${discount.rule.value}${
451
+ discount.rule.type === "percentage" ? "%" : ` ${currencyCode}`
452
+ }`,
453
+ }
454
+ })
455
+ }
456
+
457
+ let giftCards = []
458
+ if (order.gift_cards) {
459
+ giftCards = order.gift_cards.map((gc) => {
460
+ return {
461
+ is_giftcard: true,
462
+ code: gc.code,
463
+ descriptor: `${gc.value} ${currencyCode}`,
464
+ }
465
+ })
466
+
467
+ discounts.concat(giftCards)
468
+ }
469
+
470
+ const locale = await this.extractLocale(order)
471
+
472
+ return {
473
+ ...order,
474
+ locale,
475
+ has_discounts: order.discounts.length,
476
+ has_gift_cards: order.gift_cards.length,
477
+ date: order.created_at.toDateString(),
478
+ items,
479
+ discounts,
480
+ subtotal: `${this.humanPrice_(
481
+ subtotal * (1 + taxRate),
482
+ currencyCode
483
+ )} ${currencyCode}`,
484
+ gift_card_total: `${this.humanPrice_(
485
+ gift_card_total * (1 + taxRate),
486
+ currencyCode
487
+ )} ${currencyCode}`,
488
+ tax_total: `${this.humanPrice_(tax_total, currencyCode)} ${currencyCode}`,
489
+ discount_total: `${this.humanPrice_(
490
+ discount_total * (1 + taxRate),
491
+ currencyCode
492
+ )} ${currencyCode}`,
493
+ shipping_total: `${this.humanPrice_(
494
+ shipping_total * (1 + taxRate),
495
+ currencyCode
496
+ )} ${currencyCode}`,
497
+ total: `${this.humanPrice_(total, currencyCode)} ${currencyCode}`,
498
+ }
499
+ }
500
+
501
+ async orderPlacedData({ id }) {
502
+ const order = await this.orderService_.retrieve(id, {
503
+ select: [
504
+ "shipping_total",
505
+ "discount_total",
506
+ "tax_total",
507
+ "refunded_total",
508
+ "gift_card_total",
509
+ "subtotal",
510
+ "total",
511
+ ],
512
+ relations: [
513
+ "customer",
514
+ "billing_address",
515
+ "shipping_address",
516
+ "discounts",
517
+ "discounts.rule",
518
+ "shipping_methods",
519
+ "shipping_methods.shipping_option",
520
+ "payments",
521
+ "fulfillments",
522
+ "returns",
523
+ "gift_cards",
524
+ "gift_card_transactions",
525
+ ],
526
+ })
527
+
528
+ const { tax_total, shipping_total, gift_card_total, total } = order
529
+
530
+ const currencyCode = order.currency_code.toUpperCase()
531
+
532
+ const items = await Promise.all(
533
+ order.items.map(async (i) => {
534
+ i.totals = await this.totalsService_.getLineItemTotals(i, order, {
535
+ include_tax: true,
536
+ use_tax_lines: true,
537
+ })
538
+ i.thumbnail = this.normalizeThumbUrl_(i.thumbnail)
539
+ i.discounted_price = `${this.humanPrice_(
540
+ i.totals.total / i.quantity,
541
+ currencyCode
542
+ )} ${currencyCode}`
543
+ i.price = `${this.humanPrice_(
544
+ i.totals.original_total / i.quantity,
545
+ currencyCode
546
+ )} ${currencyCode}`
547
+ return i
548
+ })
549
+ )
550
+
551
+ let discounts = []
552
+ if (order.discounts) {
553
+ discounts = order.discounts.map((discount) => {
554
+ return {
555
+ is_giftcard: false,
556
+ code: discount.code,
557
+ descriptor: `${discount.rule.value}${
558
+ discount.rule.type === "percentage" ? "%" : ` ${currencyCode}`
559
+ }`,
560
+ }
561
+ })
562
+ }
563
+
564
+ let giftCards = []
565
+ if (order.gift_cards) {
566
+ giftCards = order.gift_cards.map((gc) => {
567
+ return {
568
+ is_giftcard: true,
569
+ code: gc.code,
570
+ descriptor: `${gc.value} ${currencyCode}`,
571
+ }
572
+ })
573
+
574
+ discounts.concat(giftCards)
575
+ }
576
+
577
+ const locale = await this.extractLocale(order)
578
+
579
+ // Includes taxes in discount amount
580
+ const discountTotal = items.reduce((acc, i) => {
581
+ return acc + i.totals.original_total - i.totals.total
582
+ }, 0)
583
+
584
+ const discounted_subtotal = items.reduce((acc, i) => {
585
+ return acc + i.totals.total
586
+ }, 0)
587
+ const subtotal = items.reduce((acc, i) => {
588
+ return acc + i.totals.original_total
589
+ }, 0)
590
+
591
+ const subtotal_ex_tax = items.reduce((total, i) => {
592
+ return total + i.totals.subtotal
593
+ }, 0)
594
+
595
+ return {
596
+ ...order,
597
+ locale,
598
+ has_discounts: order.discounts.length,
599
+ has_gift_cards: order.gift_cards.length,
600
+ date: order.created_at.toDateString(),
601
+ items,
602
+ discounts,
603
+ subtotal_ex_tax: `${this.humanPrice_(
604
+ subtotal_ex_tax,
605
+ currencyCode
606
+ )} ${currencyCode}`,
607
+ subtotal: `${this.humanPrice_(subtotal, currencyCode)} ${currencyCode}`,
608
+ gift_card_total: `${this.humanPrice_(
609
+ gift_card_total,
610
+ currencyCode
611
+ )} ${currencyCode}`,
612
+ tax_total: `${this.humanPrice_(tax_total, currencyCode)} ${currencyCode}`,
613
+ discount_total: `${this.humanPrice_(
614
+ discountTotal,
615
+ currencyCode
616
+ )} ${currencyCode}`,
617
+ shipping_total: `${this.humanPrice_(
618
+ shipping_total,
619
+ currencyCode
620
+ )} ${currencyCode}`,
621
+ total: `${this.humanPrice_(total, currencyCode)} ${currencyCode}`,
622
+ }
623
+ }
624
+
625
+ async gcCreatedData({ id }) {
626
+ const giftCard = await this.giftCardService_.retrieve(id, {
627
+ relations: ["region", "order"],
628
+ })
629
+
630
+ if (!giftCard.order) {
631
+ return
632
+ }
633
+
634
+ const taxRate = giftCard.region.tax_rate / 100
635
+
636
+ const locale = await this.extractLocale(order)
637
+
638
+ return {
639
+ ...giftCard,
640
+ locale,
641
+ email: giftCard.order.email,
642
+ display_value: giftCard.value * (1 + taxRate),
643
+ }
644
+ }
645
+
646
+ async returnRequestedData({ id, return_id }) {
647
+ // Fetch the return request
648
+ const returnRequest = await this.returnService_.retrieve(return_id, {
649
+ relations: [
650
+ "items",
651
+ "items.item",
652
+ "items.item.tax_lines",
653
+ "items.item.variant",
654
+ "items.item.variant.product",
655
+ "shipping_method",
656
+ "shipping_method.tax_lines",
657
+ "shipping_method.shipping_option",
658
+ ],
659
+ })
660
+
661
+ const items = await this.lineItemService_.list(
662
+ {
663
+ id: returnRequest.items.map(({ item_id }) => item_id),
664
+ },
665
+ { relations: ["tax_lines"] }
666
+ )
667
+
668
+ // Fetch the order
669
+ const order = await this.orderService_.retrieve(id, {
670
+ select: ["total"],
671
+ relations: [
672
+ "items",
673
+ "items.tax_lines",
674
+ "discounts",
675
+ "discounts.rule",
676
+ "shipping_address",
677
+ "returns",
678
+ ],
679
+ })
680
+
681
+ const currencyCode = order.currency_code.toUpperCase()
682
+
683
+ // Calculate which items are in the return
684
+ const returnItems = await Promise.all(
685
+ returnRequest.items.map(async (i) => {
686
+ const found = items.find((oi) => oi.id === i.item_id)
687
+ found.quantity = i.quantity
688
+ found.thumbnail = this.normalizeThumbUrl_(found.thumbnail)
689
+ found.totals = await this.totalsService_.getLineItemTotals(
690
+ found,
691
+ order,
692
+ {
693
+ include_tax: true,
694
+ use_tax_lines: true,
695
+ }
696
+ )
697
+ found.price = `${this.humanPrice_(
698
+ found.totals.total,
699
+ currencyCode
700
+ )} ${currencyCode}`
701
+ found.tax_lines = found.totals.tax_lines
702
+ return found
703
+ })
704
+ )
705
+
706
+ // Get total of the returned products
707
+ const item_subtotal = returnItems.reduce(
708
+ (acc, next) => acc + next.totals.total,
709
+ 0
710
+ )
711
+
712
+ // If the return has a shipping method get the price and any attachments
713
+ let shippingTotal = 0
714
+ if (returnRequest.shipping_method) {
715
+ const base = returnRequest.shipping_method.price
716
+ shippingTotal =
717
+ base +
718
+ returnRequest.shipping_method.tax_lines.reduce((acc, next) => {
719
+ return Math.round(acc + base * (next.rate / 100))
720
+ }, 0)
721
+ }
722
+
723
+ const locale = await this.extractLocale(order)
724
+
725
+ return {
726
+ locale,
727
+ has_shipping: !!returnRequest.shipping_method,
728
+ email: order.email,
729
+ items: returnItems,
730
+ subtotal: `${this.humanPrice_(
731
+ item_subtotal,
732
+ currencyCode
733
+ )} ${currencyCode}`,
734
+ shipping_total: `${this.humanPrice_(
735
+ shippingTotal,
736
+ currencyCode
737
+ )} ${currencyCode}`,
738
+ refund_amount: `${this.humanPrice_(
739
+ returnRequest.refund_amount,
740
+ currencyCode
741
+ )} ${currencyCode}`,
742
+ return_request: {
743
+ ...returnRequest,
744
+ refund_amount: `${this.humanPrice_(
745
+ returnRequest.refund_amount,
746
+ currencyCode
747
+ )} ${currencyCode}`,
748
+ },
749
+ order,
750
+ date: returnRequest.updated_at.toDateString(),
751
+ }
752
+ }
753
+
754
+ async swapReceivedData({ id }) {
755
+ const store = await this.storeService_.retrieve()
756
+ const swap = await this.swapService_.retrieve(id, {
757
+ relations: [
758
+ "additional_items",
759
+ "additional_items.tax_lines",
760
+ "return_order",
761
+ "return_order.items",
762
+ "return_order.items.item",
763
+ "return_order.shipping_method",
764
+ "return_order.shipping_method.shipping_option",
765
+ ],
766
+ })
767
+
768
+ const returnRequest = swap.return_order
769
+
770
+ const items = await this.lineItemService_.list(
771
+ {
772
+ id: returnRequest.items.map(({ item_id }) => item_id),
773
+ },
774
+ {
775
+ relations: ["tax_lines"],
776
+ }
777
+ )
778
+
779
+ returnRequest.items = returnRequest.items.map((item) => {
780
+ const found = items.find((i) => i.id === item.item_id)
781
+ return {
782
+ ...item,
783
+ item: found,
784
+ }
785
+ })
786
+
787
+ const swapLink = store.swap_link_template.replace(
788
+ /\{cart_id\}/,
789
+ swap.cart_id
790
+ )
791
+
792
+ const order = await this.orderService_.retrieve(swap.order_id, {
793
+ select: ["total"],
794
+ relations: [
795
+ "items",
796
+ "discounts",
797
+ "discounts.rule",
798
+ "shipping_address",
799
+ "swaps",
800
+ "swaps.additional_items",
801
+ "swaps.additional_items.tax_lines",
802
+ ],
803
+ })
804
+
805
+ const cart = await this.cartService_.retrieve(swap.cart_id, {
806
+ select: [
807
+ "total",
808
+ "tax_total",
809
+ "discount_total",
810
+ "shipping_total",
811
+ "subtotal",
812
+ ],
813
+ })
814
+ const currencyCode = order.currency_code.toUpperCase()
815
+
816
+ const decoratedItems = await Promise.all(
817
+ cart.items.map(async (i) => {
818
+ const totals = await this.totalsService_.getLineItemTotals(i, cart, {
819
+ include_tax: true,
820
+ })
821
+
822
+ return {
823
+ ...i,
824
+ totals,
825
+ price: this.humanPrice_(
826
+ totals.subtotal + totals.tax_total,
827
+ currencyCode
828
+ ),
829
+ }
830
+ })
831
+ )
832
+
833
+ const returnTotal = decoratedItems.reduce((acc, next) => {
834
+ if (next.is_return) {
835
+ return acc + -1 * (next.totals.subtotal + next.totals.tax_total)
836
+ }
837
+ return acc
838
+ }, 0)
839
+
840
+ const additionalTotal = decoratedItems.reduce((acc, next) => {
841
+ if (!next.is_return) {
842
+ return acc + next.totals.subtotal + next.totals.tax_total
843
+ }
844
+ return acc
845
+ }, 0)
846
+
847
+ const refundAmount = swap.return_order.refund_amount
848
+
849
+ const locale = await this.extractLocale(order)
850
+
851
+ return {
852
+ locale,
853
+ swap,
854
+ order,
855
+ return_request: returnRequest,
856
+ date: swap.updated_at.toDateString(),
857
+ swap_link: swapLink,
858
+ email: order.email,
859
+ items: decoratedItems.filter((di) => !di.is_return),
860
+ return_items: decoratedItems.filter((di) => di.is_return),
861
+ return_total: `${this.humanPrice_(
862
+ returnTotal,
863
+ currencyCode
864
+ )} ${currencyCode}`,
865
+ tax_total: `${this.humanPrice_(
866
+ cart.total,
867
+ currencyCode
868
+ )} ${currencyCode}`,
869
+ refund_amount: `${this.humanPrice_(
870
+ refundAmount,
871
+ currencyCode
872
+ )} ${currencyCode}`,
873
+ additional_total: `${this.humanPrice_(
874
+ additionalTotal,
875
+ currencyCode
876
+ )} ${currencyCode}`,
877
+ }
878
+ }
879
+
880
+ async swapCreatedData({ id }) {
881
+ const store = await this.storeService_.retrieve()
882
+ const swap = await this.swapService_.retrieve(id, {
883
+ relations: [
884
+ "additional_items",
885
+ "additional_items.tax_lines",
886
+ "return_order",
887
+ "return_order.items",
888
+ "return_order.items.item",
889
+ "return_order.shipping_method",
890
+ "return_order.shipping_method.shipping_option",
891
+ ],
892
+ })
893
+
894
+ const returnRequest = swap.return_order
895
+
896
+ const items = await this.lineItemService_.list(
897
+ {
898
+ id: returnRequest.items.map(({ item_id }) => item_id),
899
+ },
900
+ {
901
+ relations: ["tax_lines"],
902
+ }
903
+ )
904
+
905
+ returnRequest.items = returnRequest.items.map((item) => {
906
+ const found = items.find((i) => i.id === item.item_id)
907
+ return {
908
+ ...item,
909
+ item: found,
910
+ }
911
+ })
912
+
913
+ const swapLink = store.swap_link_template.replace(
914
+ /\{cart_id\}/,
915
+ swap.cart_id
916
+ )
917
+
918
+ const order = await this.orderService_.retrieve(swap.order_id, {
919
+ select: ["total"],
920
+ relations: [
921
+ "items",
922
+ "items.tax_lines",
923
+ "discounts",
924
+ "discounts.rule",
925
+ "shipping_address",
926
+ "swaps",
927
+ "swaps.additional_items",
928
+ "swaps.additional_items.tax_lines",
929
+ ],
930
+ })
931
+
932
+ const cart = await this.cartService_.retrieve(swap.cart_id, {
933
+ select: [
934
+ "total",
935
+ "tax_total",
936
+ "discount_total",
937
+ "shipping_total",
938
+ "subtotal",
939
+ ],
940
+ })
941
+ const currencyCode = order.currency_code.toUpperCase()
942
+
943
+ const decoratedItems = await Promise.all(
944
+ cart.items.map(async (i) => {
945
+ const totals = await this.totalsService_.getLineItemTotals(i, cart, {
946
+ include_tax: true,
947
+ })
948
+
949
+ return {
950
+ ...i,
951
+ totals,
952
+ tax_lines: totals.tax_lines,
953
+ price: `${this.humanPrice_(
954
+ totals.original_total / i.quantity,
955
+ currencyCode
956
+ )} ${currencyCode}`,
957
+ discounted_price: `${this.humanPrice_(
958
+ totals.total / i.quantity,
959
+ currencyCode
960
+ )} ${currencyCode}`,
961
+ }
962
+ })
963
+ )
964
+
965
+ const returnTotal = decoratedItems.reduce((acc, next) => {
966
+ const { total } = next.totals
967
+ if (next.is_return && next.variant_id) {
968
+ return acc + -1 * total
969
+ }
970
+ return acc
971
+ }, 0)
972
+
973
+ const additionalTotal = decoratedItems.reduce((acc, next) => {
974
+ const { total } = next.totals
975
+ if (!next.is_return) {
976
+ return acc + total
977
+ }
978
+ return acc
979
+ }, 0)
980
+
981
+ const refundAmount = swap.return_order.refund_amount
982
+
983
+ const locale = await this.extractLocale(order)
984
+
985
+ return {
986
+ locale,
987
+ swap,
988
+ order,
989
+ return_request: returnRequest,
990
+ date: swap.updated_at.toDateString(),
991
+ swap_link: swapLink,
992
+ email: order.email,
993
+ items: decoratedItems.filter((di) => !di.is_return),
994
+ return_items: decoratedItems.filter((di) => di.is_return),
995
+ return_total: `${this.humanPrice_(
996
+ returnTotal,
997
+ currencyCode
998
+ )} ${currencyCode}`,
999
+ refund_amount: `${this.humanPrice_(
1000
+ refundAmount,
1001
+ currencyCode
1002
+ )} ${currencyCode}`,
1003
+ additional_total: `${this.humanPrice_(
1004
+ additionalTotal,
1005
+ currencyCode
1006
+ )} ${currencyCode}`,
1007
+ }
1008
+ }
1009
+
1010
+ async itemsReturnedData(data) {
1011
+ return this.returnRequestedData(data)
1012
+ }
1013
+
1014
+ async swapShipmentCreatedData({ id, fulfillment_id }) {
1015
+ const swap = await this.swapService_.retrieve(id, {
1016
+ relations: [
1017
+ "shipping_address",
1018
+ "shipping_methods",
1019
+ "shipping_methods.tax_lines",
1020
+ "additional_items",
1021
+ "additional_items.tax_lines",
1022
+ "return_order",
1023
+ "return_order.items",
1024
+ ],
1025
+ })
1026
+
1027
+ const order = await this.orderService_.retrieve(swap.order_id, {
1028
+ relations: [
1029
+ "region",
1030
+ "items",
1031
+ "items.tax_lines",
1032
+ "discounts",
1033
+ "discounts.rule",
1034
+ "swaps",
1035
+ "swaps.additional_items",
1036
+ "swaps.additional_items.tax_lines",
1037
+ ],
1038
+ })
1039
+
1040
+ const cart = await this.cartService_.retrieve(swap.cart_id, {
1041
+ select: [
1042
+ "total",
1043
+ "tax_total",
1044
+ "discount_total",
1045
+ "shipping_total",
1046
+ "subtotal",
1047
+ ],
1048
+ })
1049
+
1050
+ const returnRequest = swap.return_order
1051
+ const items = await this.lineItemService_.list(
1052
+ {
1053
+ id: returnRequest.items.map(({ item_id }) => item_id),
1054
+ },
1055
+ {
1056
+ relations: ["tax_lines"],
1057
+ }
1058
+ )
1059
+
1060
+ const taxRate = order.tax_rate / 100
1061
+ const currencyCode = order.currency_code.toUpperCase()
1062
+
1063
+ const returnItems = await Promise.all(
1064
+ swap.return_order.items.map(async (i) => {
1065
+ const found = items.find((oi) => oi.id === i.item_id)
1066
+ const totals = await this.totalsService_.getLineItemTotals(i, cart, {
1067
+ include_tax: true,
1068
+ })
1069
+
1070
+ return {
1071
+ ...found,
1072
+ thumbnail: this.normalizeThumbUrl_(found.thumbnail),
1073
+ price: `${this.humanPrice_(
1074
+ totals.original_total / i.quantity,
1075
+ currencyCode
1076
+ )} ${currencyCode}`,
1077
+ discounted_price: `${this.humanPrice_(
1078
+ totals.total / i.quantity,
1079
+ currencyCode
1080
+ )} ${currencyCode}`,
1081
+ quantity: i.quantity,
1082
+ }
1083
+ })
1084
+ )
1085
+
1086
+ const returnTotal = await this.totalsService_.getRefundTotal(
1087
+ order,
1088
+ returnItems
1089
+ )
1090
+
1091
+ const constructedOrder = {
1092
+ ...order,
1093
+ shipping_methods: swap.shipping_methods,
1094
+ items: swap.additional_items,
1095
+ }
1096
+
1097
+ const additionalTotal = await this.totalsService_.getTotal(constructedOrder)
1098
+
1099
+ const refundAmount = swap.return_order.refund_amount
1100
+
1101
+ const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
1102
+ relations: ["tracking_links"],
1103
+ })
1104
+
1105
+ const locale = await this.extractLocale(order)
1106
+
1107
+ return {
1108
+ locale,
1109
+ swap,
1110
+ order,
1111
+ items: await Promise.all(
1112
+ swap.additional_items.map(async (i) => {
1113
+ const totals = await this.totalsService_.getLineItemTotals(i, cart, {
1114
+ include_tax: true,
1115
+ })
1116
+
1117
+ return {
1118
+ ...i,
1119
+ thumbnail: this.normalizeThumbUrl_(i.thumbnail),
1120
+ price: `${this.humanPrice_(
1121
+ totals.original_total / i.quantity,
1122
+ currencyCode
1123
+ )} ${currencyCode}`,
1124
+ discounted_price: `${this.humanPrice_(
1125
+ totals.total / i.quantity,
1126
+ currencyCode
1127
+ )} ${currencyCode}`,
1128
+ quantity: i.quantity,
1129
+ }
1130
+ })
1131
+ ),
1132
+ date: swap.updated_at.toDateString(),
1133
+ email: order.email,
1134
+ tax_amount: `${this.humanPrice_(
1135
+ cart.tax_total,
1136
+ currencyCode
1137
+ )} ${currencyCode}`,
1138
+ paid_total: `${this.humanPrice_(
1139
+ swap.difference_due,
1140
+ currencyCode
1141
+ )} ${currencyCode}`,
1142
+ return_total: `${this.humanPrice_(
1143
+ returnTotal,
1144
+ currencyCode
1145
+ )} ${currencyCode}`,
1146
+ refund_amount: `${this.humanPrice_(
1147
+ refundAmount,
1148
+ currencyCode
1149
+ )} ${currencyCode}`,
1150
+ additional_total: `${this.humanPrice_(
1151
+ additionalTotal,
1152
+ currencyCode
1153
+ )} ${currencyCode}`,
1154
+ fulfillment: shipment,
1155
+ tracking_links: shipment.tracking_links,
1156
+ tracking_number: shipment.tracking_numbers.join(", "),
1157
+ }
1158
+ }
1159
+
1160
+ async claimShipmentCreatedData({ id, fulfillment_id }) {
1161
+ const claim = await this.claimService_.retrieve(id, {
1162
+ relations: ["order", "order.items", "order.shipping_address"],
1163
+ })
1164
+
1165
+ const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
1166
+ relations: ["tracking_links"],
1167
+ })
1168
+
1169
+ const locale = await this.extractLocale(claim.order)
1170
+
1171
+ return {
1172
+ locale,
1173
+ email: claim.order.email,
1174
+ claim,
1175
+ order: claim.order,
1176
+ fulfillment: shipment,
1177
+ tracking_links: shipment.tracking_links,
1178
+ tracking_number: shipment.tracking_numbers.join(", "),
1179
+ }
1180
+ }
1181
+
1182
+ async restockNotificationData({ variant_id, emails }) {
1183
+ const variant = await this.productVariantService_.retrieve(variant_id, {
1184
+ relations: ["product"],
1185
+ })
1186
+
1187
+ let thumb
1188
+ if (variant.product.thumbnail) {
1189
+ thumb = this.normalizeThumbUrl_(variant.product.thumbnail)
1190
+ }
1191
+
1192
+ return {
1193
+ product: {
1194
+ ...variant.product,
1195
+ thumbnail: thumb,
1196
+ },
1197
+ variant,
1198
+ variant_id,
1199
+ emails,
1200
+ }
1201
+ }
1202
+
1203
+ userPasswordResetData(data) {
1204
+ return data
1205
+ }
1206
+
1207
+ customerPasswordResetData(data) {
1208
+ return data
1209
+ }
1210
+
1211
+ async orderRefundCreatedData({ id, refund_id }) {
1212
+ const order = await this.orderService_.retrieveWithTotals(id, {
1213
+ select: [
1214
+ "total",
1215
+ ],
1216
+ relations: [
1217
+ "refunds",
1218
+ "items",
1219
+ ]
1220
+ })
1221
+
1222
+ const refund = order.refunds.find((refund) => refund.id === refund_id)
1223
+
1224
+ return {
1225
+ order,
1226
+ refund,
1227
+ refund_amount: `${this.humanPrice_(
1228
+ refund.amount,
1229
+ order.currency_code
1230
+ )} ${order.currency_code}`,
1231
+ email: order.email
1232
+ }
1233
+ }
1234
+
1235
+ processItems_(items, taxRate, currencyCode) {
1236
+ return items.map((i) => {
1237
+ return {
1238
+ ...i,
1239
+ thumbnail: this.normalizeThumbUrl_(i.thumbnail),
1240
+ price: `${this.humanPrice_(
1241
+ i.unit_price * (1 + taxRate),
1242
+ currencyCode
1243
+ )} ${currencyCode}`,
1244
+ }
1245
+ })
1246
+ }
1247
+
1248
+ humanPrice_(amount, currency) {
1249
+ if (!amount) {
1250
+ return "0.00"
1251
+ }
1252
+
1253
+ const normalized = humanizeAmount(amount, currency)
1254
+ return normalized.toFixed(
1255
+ zeroDecimalCurrencies.includes(currency.toLowerCase()) ? 0 : 2
1256
+ )
1257
+ }
1258
+
1259
+ normalizeThumbUrl_(url) {
1260
+ if (!url) {
1261
+ return null
1262
+ }
1263
+
1264
+ if (url.startsWith("http")) {
1265
+ return url
1266
+ } else if (url.startsWith("//")) {
1267
+ return `https:${url}`
1268
+ }
1269
+ return url
1270
+ }
1271
+
1272
+ async extractLocale(fromOrder) {
1273
+ if (fromOrder.cart_id) {
1274
+ try {
1275
+ const cart = await this.cartService_.retrieve(fromOrder.cart_id, {
1276
+ select: ["id", "context"],
1277
+ })
1278
+
1279
+ if (cart.context && cart.context.locale) {
1280
+ return cart.context.locale
1281
+ }
1282
+ } catch (err) {
1283
+ console.log(err)
1284
+ console.warn("Failed to gather context for order")
1285
+ return null
1286
+ }
1287
+ }
1288
+ return null
1289
+ }
1290
+ }
1291
+
1292
+ export default SESService