diff --git a/.changeset/loud-fans-own.md b/.changeset/loud-fans-own.md new file mode 100644 index 0000000000..fb1021c172 --- /dev/null +++ b/.changeset/loud-fans-own.md @@ -0,0 +1,5 @@ +--- +"medusa-plugin-sendgrid": patch +--- + +fix(medusa-plugin-sendgrid): Use correct SendGrid client diff --git a/packages/medusa-plugin-sendgrid/src/services/__tests__/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/__tests__/sendgrid.js new file mode 100644 index 0000000000..09fde7436a --- /dev/null +++ b/packages/medusa-plugin-sendgrid/src/services/__tests__/sendgrid.js @@ -0,0 +1,196 @@ +import SendGridService from "../sendgrid" +import SendGrid from "@sendgrid/mail" + +jest.genMockFromModule("@sendgrid/mail") +jest.mock("@sendgrid/mail") + +const mockedSendGrid = SendGrid +mockedSendGrid.setApiKey.mockResolvedValue(mockedSendGrid) +mockedSendGrid.send.mockResolvedValue(Promise.resolve()) + +describe("SendGridService", () => { + let sendGridService + + const totalsService = { + withTransaction: function () { + return this + }, + getCalculationContext: jest.fn().mockImplementation((order, lineItems) => { + return Promise.resolve({}) + }), + getLineItemTotals: jest.fn().mockImplementation(() => { + return Promise.resolve({}) + }), + getLineItemRefund: () => {}, + getTotal: (o) => { + return o.total || 0 + }, + getGiftCardableAmount: (o) => { + return o.subtotal || 0 + }, + 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 + }, + getPaidTotal: (o) => { + return o.paid_total || 0 + }, + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("should call SendGrid.send when template is configured and correct data is passed", async () => { + const orderServiceMock = { + retrieve: jest.fn().mockImplementation((data) => { + return Promise.resolve({ + email: "test@test.com", + currency_code: "usd", + items: [], + discounts: [], + gift_cards: [], + created_at: new Date(), + }) + }), + } + + sendGridService = new SendGridService( + { orderService: orderServiceMock, totalsService }, + { + api_key: "SG.test", + order_placed_template: "lol", + } + ) + + await sendGridService.sendNotification("order.placed", { id: "test" }) + expect(mockedSendGrid.send).toBeCalled() + }) + + it("should failed to send an email when event does not exist", async () => { + expect.assertions(1) + const orderServiceMock = { + retrieve: jest.fn().mockImplementation((data) => { + return Promise.resolve({ + email: "test@test.com", + currency_code: "usd", + items: [], + discounts: [], + gift_cards: [], + created_at: new Date(), + }) + }), + } + + sendGridService = new SendGridService( + { orderService: orderServiceMock, totalsService }, + { + api_key: "SG.test", + order_placed_template: "lol", + } + ) + + try { + await sendGridService.sendNotification("some.non-existing_event", { + id: "test", + }) + } catch (error) { + expect(error.message).toEqual( + "Sendgrid service: No template was set for event: some.non-existing_event" + ) + } + }) + + it("should failed to send an email when template id is not configured", async () => { + expect.assertions(1) + const orderServiceMock = { + retrieve: jest.fn().mockImplementation((data) => { + return Promise.resolve({ + email: "test@test.com", + currency_code: "usd", + items: [], + discounts: [], + gift_cards: [], + created_at: new Date(), + }) + }), + } + + sendGridService = new SendGridService( + { orderService: orderServiceMock, totalsService }, + { + api_key: "SG.test", + } + ) + + try { + await sendGridService.sendNotification("order.placed", { + id: "test", + }) + } catch (error) { + expect(error.message).toEqual( + "Sendgrid service: No template was set for event: order.placed" + ) + } + }) + + it("should use localized template to send an email", async () => { + const cartServiceMock = { + retrieve: jest.fn().mockImplementation((data) => { + return Promise.resolve({ + context: { + locale: "de-DE", + }, + }) + }), + } + const orderServiceMock = { + retrieve: jest.fn().mockImplementation((data) => { + return Promise.resolve({ + email: "test@test.com", + currency_code: "usd", + items: [], + discounts: [], + gift_cards: [], + created_at: new Date(), + cart_id: "test-id", + }) + }), + } + + sendGridService = new SendGridService( + { + orderService: orderServiceMock, + totalsService, + cartService: cartServiceMock, + }, + { + api_key: "SG.test", + localization: { + "de-DE": { + order_placed_template: "lol", + }, + }, + } + ) + + await sendGridService.sendNotification("order.placed", { + id: "test", + }) + expect(mockedSendGrid.send).toBeCalled() + }) +}) diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index 8ac063c6b9..98757a4e05 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -34,6 +34,7 @@ class SendGridService extends NotificationService { totalsService, productVariantService, giftCardService, + logger, }, options ) { @@ -53,6 +54,7 @@ class SendGridService extends NotificationService { this.totalsService_ = totalsService this.productVariantService_ = productVariantService this.giftCardService_ = giftCardService + this.logger_ = logger SendGrid.setApiKey(options.api_key) } @@ -219,16 +221,20 @@ class SendGridService extends NotificationService { } async sendNotification(event, eventData, attachmentGenerator) { + const data = await this.fetchData(event, eventData, attachmentGenerator) + let templateId = this.getTemplateId(event) - if (!templateId) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, `Sendgrid service: No template was set for event: ${event}`) + if (data.locale) { + templateId = this.getLocalizedTemplateId(event, data.locale) || templateId } - const data = await this.fetchData(event, eventData, attachmentGenerator) - if (!data) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, "Sendgrid service: Invalid event data was received") - } + if (!templateId) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Sendgrid service: No template was set for event: ${event}` + ) + } const attachments = await this.fetchAttachments( event, @@ -236,10 +242,6 @@ class SendGridService extends NotificationService { attachmentGenerator ) - if (data.locale) { - templateId = this.getLocalizedTemplateId(event, data.locale) || templateId - } - const sendOptions = { template_id: templateId, from: this.options_.from, @@ -261,10 +263,15 @@ class SendGridService extends NotificationService { }) } - let status - await this.transporter_.sendMail(sendOptions) - .then(() => { status = "sent" }) - .catch((error) => { status = "failed"; console.log(error) }) + 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 @@ -303,18 +310,11 @@ class SendGridService extends NotificationService { /** * Sends an email using SendGrid. - * @param {string} templateId - id of template in SendGrid - * @param {string} from - sender of email - * @param {string} to - receiver of email - * @param {Object} data - data to send in mail (match with template) + * @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) { - try { - return SendGrid.send(options) - } catch (error) { - throw error - } + return await SendGrid.send(options) } async orderShipmentCreatedData({ id, fulfillment_id }, attachmentGenerator) {