From 9c08fa1082775e42e3dcebf6a389772d47ff6b59 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 14 May 2024 10:49:38 +0200 Subject: [PATCH 01/11] base tests for use case --- .../send-event-messages.use-case.test.ts | 135 +++++++++++++++++- .../send-event-messages.use-case.ts | 27 +++- .../smtp-configuration.service.ts | 8 +- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts index 2cdbf287a..81f96c794 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts @@ -1,8 +1,109 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { SendEventMessagesUseCase } from "./send-event-messages.use-case"; import { SendEventMessagesUseCaseFactory } from "./send-event-messages.use-case.factory"; +import { errAsync, ok, okAsync, Result } from "neverthrow"; +import { CompiledEmail, IEmailCompiler } from "../smtp/services/email-compiler"; +import { BaseError } from "../../errors"; +import { ISMTPEmailSender } from "../smtp/services/smtp-email-sender"; +import { IGetSmtpConfiguration } from "../smtp/configuration/smtp-configuration.service"; +import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema"; + +class MockEmailCompiler implements IEmailCompiler { + static returnSuccessCompiledEmail = (): Result => + ok({ + text: "html text", + from: "email from", + subject: "email subject", + html: "html text", + to: "email to", + }); + + mockEmailCompileMethod = vi.fn< + Parameters, + ReturnType + >(); + + compile = this.mockEmailCompileMethod; +} + +class MockSmtpSender implements ISMTPEmailSender { + static returnEmptyResponse: ISMTPEmailSender["sendEmailWithSmtp"] = async () => { + return { response: {} }; + }; + + mockSendEmailMethod = vi.fn< + Parameters, + ReturnType + >(); + + sendEmailWithSmtp = this.mockSendEmailMethod; +} + +class MockSmptConfigurationService implements IGetSmtpConfiguration { + static getSimpleConfigurationValue = (): SmtpConfiguration => { + return { + id: "1", + active: true, + senderEmail: "sender@email.com", + senderName: "Sender Name", + events: [ + { + active: true, + eventType: "ACCOUNT_DELETE", + subject: "Subject", + template: "html text", + }, + ], + channels: { channels: ["default-channel"], mode: "exclude", override: false }, + encryption: "SSL", + name: "Config 1", + smtpHost: "localhost", + smtpPassword: "admin1234", + smtpPort: "2137", + smtpUser: "admin", + }; + }; + + static returnValidSingleConfiguration: IGetSmtpConfiguration["getConfigurations"] = () => { + return okAsync([MockSmptConfigurationService.getSimpleConfigurationValue()]); + }; + + static returnEmptyConfigurationsList: IGetSmtpConfiguration["getConfigurations"] = () => { + return okAsync([]); + }; + + static returnErrorFetchingConfigurations: IGetSmtpConfiguration["getConfigurations"] = () => { + return errAsync(new BaseError("Mock fail to fetch")); + }; + + mockGetConfigurationsMethod = vi.fn< + Parameters, + ReturnType + >(); + + getConfigurations = this.mockGetConfigurationsMethod; +} describe("SendEventMessagesUseCase", () => { + const emailCompiler = new MockEmailCompiler(); + const emailSender = new MockSmtpSender(); + const smtpConfigurationService = new MockSmptConfigurationService(); + + beforeEach(() => { + vi.resetAllMocks(); + + /** + * Apply default return values, which can be partially overwritten in tests + */ + emailCompiler.mockEmailCompileMethod.mockImplementation( + MockEmailCompiler.returnSuccessCompiledEmail, + ); + emailSender.mockSendEmailMethod.mockImplementation(MockSmtpSender.returnEmptyResponse); + smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( + MockSmptConfigurationService.returnValidSingleConfiguration, + ); + }); + describe("Factory", () => { it("Build with default dependencies from AuthData", () => { const instance = new SendEventMessagesUseCaseFactory().createFromAuthData({ @@ -16,7 +117,37 @@ describe("SendEventMessagesUseCase", () => { }); describe("sendEventMessages method", () => { - it.todo("Returns error when no active configurations are available for selected channel"); + it.todo("Returns error if failed to fetch configurations"); + + it("Returns error when no active configurations are available for selected channel", async () => { + const instance = new SendEventMessagesUseCase({ + emailCompiler, + emailSender, + smtpConfigurationService, + }); + + smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( + MockSmptConfigurationService.returnErrorFetchingConfigurations, + ); + + const result = await instance.sendEventMessages({ + event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", + }); + + expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + SendEventMessagesUseCase.MissingAvailableConfigurationError, + ); + + /** + * Additionally check if outer error contains inner error cause + */ + expect(result?._unsafeUnwrapErr().errors).toStrictEqual( + expect.arrayContaining([expect.any(BaseError)]), + ); + }); describe("Multiple configurations assigned for the same event", () => { it.todo("Calls SMTP service to send email for each configuration"); diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index 57cdbd440..5928be604 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -1,8 +1,13 @@ -import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service"; +import { + IGetSmtpConfiguration, + SmtpConfigurationService, +} from "../smtp/configuration/smtp-configuration.service"; import { IEmailCompiler } from "../smtp/services/email-compiler"; import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; +import { BaseError } from "../../errors"; +import { errAsync } from "neverthrow"; /* * todo test @@ -11,9 +16,14 @@ import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-send export class SendEventMessagesUseCase { private logger = createLogger("SendEventMessagesUseCase"); + static SendEventMessagesUseCaseError = BaseError.subclass("SendEventMessagesUseCaseError"); + static MissingAvailableConfigurationError = this.SendEventMessagesUseCaseError.subclass( + "MissingAvailableConfigurationError", + ); + constructor( private deps: { - smtpConfigurationService: SmtpConfigurationService; + smtpConfigurationService: IGetSmtpConfiguration; emailCompiler: IEmailCompiler; emailSender: ISMTPEmailSender; }, @@ -38,7 +48,18 @@ export class SendEventMessagesUseCase { }); if (availableSmtpConfigurations.isErr()) { - throw new Error(availableSmtpConfigurations.error.message); //todo add neverthrow + return errAsync( + new SendEventMessagesUseCase.MissingAvailableConfigurationError( + "Missing active configuration for this channel", + { + errors: [availableSmtpConfigurations.error], + props: { + channelSlug, + event, + }, + }, + ), + ); } /** diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts index 907f4641d..2f3758088 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.service.ts @@ -21,7 +21,13 @@ export interface FilterConfigurationsArgs { active?: boolean; } -export class SmtpConfigurationService { +export interface IGetSmtpConfiguration { + getConfigurations( + filter?: FilterConfigurationsArgs, + ): ResultAsync>; +} + +export class SmtpConfigurationService implements IGetSmtpConfiguration { static SmtpConfigurationServiceError = BaseError.subclass("SmtpConfigurationServiceError"); static ConfigNotFoundError = BaseError.subclass("ConfigNotFoundError"); static EventConfigNotFoundError = BaseError.subclass("EventConfigNotFoundError"); From 71ad860ef2cc5a0a7dc2027f709a19e7e0c98254 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 14 May 2024 10:56:27 +0200 Subject: [PATCH 02/11] more tests --- .../send-event-messages.use-case.test.ts | 52 +++++++++++++++---- .../send-event-messages.use-case.ts | 22 +++++++- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts index 81f96c794..f52415c8f 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts @@ -85,13 +85,25 @@ class MockSmptConfigurationService implements IGetSmtpConfiguration { } describe("SendEventMessagesUseCase", () => { - const emailCompiler = new MockEmailCompiler(); - const emailSender = new MockSmtpSender(); - const smtpConfigurationService = new MockSmptConfigurationService(); + let emailCompiler: MockEmailCompiler; + let emailSender: MockSmtpSender; + let smtpConfigurationService: MockSmptConfigurationService; + + let useCaseInstance: SendEventMessagesUseCase; beforeEach(() => { + /** + * Just in case reset mocks if some reference is preserved + */ vi.resetAllMocks(); + /** + * Create direct dependencies + */ + emailCompiler = new MockEmailCompiler(); + emailSender = new MockSmtpSender(); + smtpConfigurationService = new MockSmptConfigurationService(); + /** * Apply default return values, which can be partially overwritten in tests */ @@ -102,6 +114,15 @@ describe("SendEventMessagesUseCase", () => { smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( MockSmptConfigurationService.returnValidSingleConfiguration, ); + + /** + * Create service instance for testing + */ + useCaseInstance = new SendEventMessagesUseCase({ + emailCompiler, + emailSender, + smtpConfigurationService, + }); }); describe("Factory", () => { @@ -117,20 +138,29 @@ describe("SendEventMessagesUseCase", () => { }); describe("sendEventMessages method", () => { - it.todo("Returns error if failed to fetch configurations"); + it("Returns error if configurations list is empty", async () => { + smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( + MockSmptConfigurationService.returnEmptyConfigurationsList, + ); - it("Returns error when no active configurations are available for selected channel", async () => { - const instance = new SendEventMessagesUseCase({ - emailCompiler, - emailSender, - smtpConfigurationService, + const result = await useCaseInstance.sendEventMessages({ + event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", }); + expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + SendEventMessagesUseCase.MissingAvailableConfigurationError, + ); + }); + + it("Returns error if failed to fetch configurations", async () => { smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( MockSmptConfigurationService.returnErrorFetchingConfigurations, ); - const result = await instance.sendEventMessages({ + const result = await useCaseInstance.sendEventMessages({ event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", payload: {}, channelSlug: "channel-slug", @@ -138,7 +168,7 @@ describe("SendEventMessagesUseCase", () => { }); expect(result?._unsafeUnwrapErr()).toBeInstanceOf( - SendEventMessagesUseCase.MissingAvailableConfigurationError, + SendEventMessagesUseCase.FailedToFetchConfigurationError, ); /** diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index 5928be604..62550512b 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -17,6 +17,9 @@ export class SendEventMessagesUseCase { private logger = createLogger("SendEventMessagesUseCase"); static SendEventMessagesUseCaseError = BaseError.subclass("SendEventMessagesUseCaseError"); + static FailedToFetchConfigurationError = this.SendEventMessagesUseCaseError.subclass( + "FailedToFetchConfigurationError", + ); static MissingAvailableConfigurationError = this.SendEventMessagesUseCaseError.subclass( "MissingAvailableConfigurationError", ); @@ -49,8 +52,8 @@ export class SendEventMessagesUseCase { if (availableSmtpConfigurations.isErr()) { return errAsync( - new SendEventMessagesUseCase.MissingAvailableConfigurationError( - "Missing active configuration for this channel", + new SendEventMessagesUseCase.FailedToFetchConfigurationError( + "Failed to fetch configuration", { errors: [availableSmtpConfigurations.error], props: { @@ -62,6 +65,21 @@ export class SendEventMessagesUseCase { ); } + if (availableSmtpConfigurations.value.length === 0) { + return errAsync( + new SendEventMessagesUseCase.MissingAvailableConfigurationError( + "Missing configuration for this channel that is active", + { + errors: [], + props: { + channelSlug, + event, + }, + }, + ), + ); + } + /** * TODO: Why this is not in parallel? */ From 7b3451ab51c68e8c5da1cfe604ac951fe18c0a2f Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Wed, 15 May 2024 16:00:45 +0200 Subject: [PATCH 03/11] more tests --- .../send-event-messages.use-case.test.ts | 117 ++++++++++++++++-- .../send-event-messages.use-case.ts | 68 ++++++++-- 2 files changed, 160 insertions(+), 25 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts index f52415c8f..14b2e653f 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts @@ -1,15 +1,18 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { SendEventMessagesUseCase } from "./send-event-messages.use-case"; import { SendEventMessagesUseCaseFactory } from "./send-event-messages.use-case.factory"; -import { errAsync, ok, okAsync, Result } from "neverthrow"; +import { err, errAsync, ok, okAsync, Result } from "neverthrow"; import { CompiledEmail, IEmailCompiler } from "../smtp/services/email-compiler"; import { BaseError } from "../../errors"; import { ISMTPEmailSender } from "../smtp/services/smtp-email-sender"; import { IGetSmtpConfiguration } from "../smtp/configuration/smtp-configuration.service"; import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema"; +import { MessageEventTypes } from "./message-event-types"; + +const EVENT_TYPE = "ACCOUNT_DELETE" satisfies MessageEventTypes; class MockEmailCompiler implements IEmailCompiler { - static returnSuccessCompiledEmail = (): Result => + static returnSuccessCompiledEmail: IEmailCompiler["compile"] = () => ok({ text: "html text", from: "email from", @@ -18,6 +21,10 @@ class MockEmailCompiler implements IEmailCompiler { to: "email to", }); + static returnErrorCompiling: IEmailCompiler["compile"] = () => { + return err(new BaseError("Error compiling")); + }; + mockEmailCompileMethod = vi.fn< Parameters, ReturnType @@ -49,7 +56,7 @@ class MockSmptConfigurationService implements IGetSmtpConfiguration { events: [ { active: true, - eventType: "ACCOUNT_DELETE", + eventType: EVENT_TYPE, subject: "Subject", template: "html text", }, @@ -76,6 +83,15 @@ class MockSmptConfigurationService implements IGetSmtpConfiguration { return errAsync(new BaseError("Mock fail to fetch")); }; + static returnValidTwoConfigurations: IGetSmtpConfiguration["getConfigurations"] = () => { + const c1 = this.getSimpleConfigurationValue(); + const c2 = this.getSimpleConfigurationValue(); + + c2.id = "2"; + + return okAsync([c1, c2]); + }; + mockGetConfigurationsMethod = vi.fn< Parameters, ReturnType @@ -144,7 +160,7 @@ describe("SendEventMessagesUseCase", () => { ); const result = await useCaseInstance.sendEventMessages({ - event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", + event: EVENT_TYPE, payload: {}, channelSlug: "channel-slug", recipientEmail: "recipient@test.com", @@ -161,7 +177,7 @@ describe("SendEventMessagesUseCase", () => { ); const result = await useCaseInstance.sendEventMessages({ - event: "ACCOUNT_CHANGE_EMAIL_CONFIRM", + event: EVENT_TYPE, payload: {}, channelSlug: "channel-slug", recipientEmail: "recipient@test.com", @@ -180,21 +196,98 @@ describe("SendEventMessagesUseCase", () => { }); describe("Multiple configurations assigned for the same event", () => { - it.todo("Calls SMTP service to send email for each configuration"); + it("Calls SMTP service to send email for each configuration", async () => { + smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( + MockSmptConfigurationService.returnValidTwoConfigurations, + ); + + await useCaseInstance.sendEventMessages({ + event: EVENT_TYPE, + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", + }); + + expect(emailSender.mockSendEmailMethod).toHaveBeenCalledTimes(2); + }); + + it.todo("Returns error if at least one configuration fails, even if second one works"); }); describe("Single configuration assigned for the event", () => { - it.todo("Does nothing (?) if config is missing for this event"); + it("Returns error if event is set to not active", async () => { + const smtpConfig = MockSmptConfigurationService.getSimpleConfigurationValue(); - it.todo("Does nothing (?) if event is set to not active"); + smtpConfig.events[0].active = false; - it.todo("Does nothing (?) if configuration sender name is missing"); + smtpConfigurationService.mockGetConfigurationsMethod.mockReturnValue(okAsync([smtpConfig])); - it.todo("Does nothing (?) if configuration sender email is missing"); + const result = await useCaseInstance.sendEventMessages({ + event: EVENT_TYPE, + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", + }); + + expect(result?.isErr()).toBe(true); + expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + SendEventMessagesUseCase.EventConfigNotActiveError, + ); + }); - it.todo("Does nothing (?) if email compilation fails"); + it.each(["senderName", "senderEmail"] as const)( + "Returns error if configuration '%s' is missing in configuration", + async (field) => { + const smtpConfig = MockSmptConfigurationService.getSimpleConfigurationValue(); - it.todo("Calls SMTP service to send email"); + smtpConfig[field] = undefined; + + smtpConfigurationService.mockGetConfigurationsMethod.mockReturnValue( + okAsync([smtpConfig]), + ); + + const result = await useCaseInstance.sendEventMessages({ + event: EVENT_TYPE, + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", + }); + + expect(result?.isErr()).toBe(true); + expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + SendEventMessagesUseCase.InvalidSenderConfigError, + ); + }, + ); + + it("Returns error if email compilation fails", async () => { + emailCompiler.mockEmailCompileMethod.mockImplementation( + MockEmailCompiler.returnErrorCompiling, + ); + + const result = await useCaseInstance.sendEventMessages({ + event: EVENT_TYPE, + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", + }); + + expect(result?.isErr()).toBe(true); + expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + SendEventMessagesUseCase.EmailCompilationError, + ); + }); + + it("Calls SMTP service to send email", async () => { + await useCaseInstance.sendEventMessages({ + event: EVENT_TYPE, + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "recipient@test.com", + }); + + expect(emailSender.mockSendEmailMethod).toHaveBeenCalledOnce(); + }); }); }); }); diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index 62550512b..ee5d11a2e 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -7,7 +7,7 @@ import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; import { BaseError } from "../../errors"; -import { errAsync } from "neverthrow"; +import { err, errAsync, okAsync } from "neverthrow"; /* * todo test @@ -23,6 +23,14 @@ export class SendEventMessagesUseCase { static MissingAvailableConfigurationError = this.SendEventMessagesUseCaseError.subclass( "MissingAvailableConfigurationError", ); + static EmailCompilationError = + this.SendEventMessagesUseCaseError.subclass("EmailCompilationError"); + static InvalidSenderConfigError = this.SendEventMessagesUseCaseError.subclass( + "InvalidSenderConfigError", + ); + static EventConfigNotActiveError = this.SendEventMessagesUseCaseError.subclass( + "EventConfigNotActiveError", + ); constructor( private deps: { @@ -51,7 +59,9 @@ export class SendEventMessagesUseCase { }); if (availableSmtpConfigurations.isErr()) { - return errAsync( + this.logger.warn("Failed to fetch configuration"); + + return err( new SendEventMessagesUseCase.FailedToFetchConfigurationError( "Failed to fetch configuration", { @@ -66,7 +76,9 @@ export class SendEventMessagesUseCase { } if (availableSmtpConfigurations.value.length === 0) { - return errAsync( + this.logger.warn("Configuration list is empty, app is not configured"); + + return err( new SendEventMessagesUseCase.MissingAvailableConfigurationError( "Missing configuration for this channel that is active", { @@ -84,30 +96,47 @@ export class SendEventMessagesUseCase { * TODO: Why this is not in parallel? */ for (const smtpConfiguration of availableSmtpConfigurations.value) { + this.logger.info("Detected configuration, will attempt to send email"); + try { const eventSettings = smtpConfiguration.events.find((e) => e.eventType === event); if (!eventSettings) { + this.logger.info("Configuration found but settings for this event are missing"); /* * Config missing, ignore * todo log + * todo this probalby should be for / continue instead, or Promise.all */ return; } if (!eventSettings.active) { - /** - * Config found, but set as disabled, ignore. - * todo: log - */ - return; + this.logger.info("Configuration found, but setting for this event are not active"); + + return errAsync( + new SendEventMessagesUseCase.EventConfigNotActiveError("Event config is disabled", { + props: { + event: eventSettings.eventType, + }, + }), + ); } if (!smtpConfiguration.senderName || !smtpConfiguration.senderEmail) { - /** - * TODO: check if this should be allowed - */ - return; + this.logger.warn("Configuration is invalid: missing sender data", { + senderName: smtpConfiguration.senderName, + senderEmail: smtpConfiguration.senderEmail, + }); + + return errAsync( + new SendEventMessagesUseCase.InvalidSenderConfigError("Missing sender name or email", { + props: { + senderName: smtpConfiguration.senderName, + senderEmail: smtpConfiguration.senderEmail, + }, + }), + ); } const preparedEmailResult = this.deps.emailCompiler.compile({ @@ -121,7 +150,17 @@ export class SendEventMessagesUseCase { }); if (preparedEmailResult.isErr()) { - return; // todo log + what should we do? + this.logger.warn("Failed to compile email template"); + + return errAsync( + new SendEventMessagesUseCase.EmailCompilationError("Failed to compile error", { + errors: [preparedEmailResult.error], + props: { + channelSlug, + event, + }, + }), + ); } const smtpSettings: SendMailArgs["smtpSettings"] = { @@ -138,10 +177,13 @@ export class SendEventMessagesUseCase { } try { + // todo get errors from smtp and map to proper response await this.deps.emailSender.sendEmailWithSmtp({ mailData: preparedEmailResult.value, smtpSettings, }); + + return okAsync(null); // todo result } catch (e) { this.logger.error("SMTP returned errors", { error: e }); } From 72bfae13ccf7b67bf1947843b474d80146f179b3 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Wed, 15 May 2024 16:22:46 +0200 Subject: [PATCH 04/11] Refactor --- .../send-event-messages.use-case.test.ts | 12 +- .../send-event-messages.use-case.ts | 258 ++++++++++-------- 2 files changed, 148 insertions(+), 122 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts index 14b2e653f..1b42711c9 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts @@ -166,7 +166,7 @@ describe("SendEventMessagesUseCase", () => { recipientEmail: "recipient@test.com", }); - expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + expect(result?._unsafeUnwrapErr()[0]).toBeInstanceOf( SendEventMessagesUseCase.MissingAvailableConfigurationError, ); }); @@ -183,14 +183,14 @@ describe("SendEventMessagesUseCase", () => { recipientEmail: "recipient@test.com", }); - expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + expect(result?._unsafeUnwrapErr()[0]).toBeInstanceOf( SendEventMessagesUseCase.FailedToFetchConfigurationError, ); /** * Additionally check if outer error contains inner error cause */ - expect(result?._unsafeUnwrapErr().errors).toStrictEqual( + expect(result?._unsafeUnwrapErr()[0].errors).toStrictEqual( expect.arrayContaining([expect.any(BaseError)]), ); }); @@ -230,7 +230,7 @@ describe("SendEventMessagesUseCase", () => { }); expect(result?.isErr()).toBe(true); - expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + expect(result?._unsafeUnwrapErr()[0]).toBeInstanceOf( SendEventMessagesUseCase.EventConfigNotActiveError, ); }); @@ -254,7 +254,7 @@ describe("SendEventMessagesUseCase", () => { }); expect(result?.isErr()).toBe(true); - expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + expect(result?._unsafeUnwrapErr()[0]).toBeInstanceOf( SendEventMessagesUseCase.InvalidSenderConfigError, ); }, @@ -273,7 +273,7 @@ describe("SendEventMessagesUseCase", () => { }); expect(result?.isErr()).toBe(true); - expect(result?._unsafeUnwrapErr()).toBeInstanceOf( + expect(result?._unsafeUnwrapErr()[0]).toBeInstanceOf( SendEventMessagesUseCase.EmailCompilationError, ); }); diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index ee5d11a2e..067d637da 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -7,7 +7,9 @@ import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; import { BaseError } from "../../errors"; -import { err, errAsync, okAsync } from "neverthrow"; +import { err, errAsync, okAsync, Result } from "neverthrow"; +import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema"; +import combineWithAllErrors = Result.combineWithAllErrors; /* * todo test @@ -16,21 +18,17 @@ import { err, errAsync, okAsync } from "neverthrow"; export class SendEventMessagesUseCase { private logger = createLogger("SendEventMessagesUseCase"); - static SendEventMessagesUseCaseError = BaseError.subclass("SendEventMessagesUseCaseError"); - static FailedToFetchConfigurationError = this.SendEventMessagesUseCaseError.subclass( + static BaseError = BaseError.subclass("SendEventMessagesUseCaseError"); + static FailedToFetchConfigurationError = this.BaseError.subclass( "FailedToFetchConfigurationError", ); - static MissingAvailableConfigurationError = this.SendEventMessagesUseCaseError.subclass( + static MissingAvailableConfigurationError = this.BaseError.subclass( "MissingAvailableConfigurationError", ); - static EmailCompilationError = - this.SendEventMessagesUseCaseError.subclass("EmailCompilationError"); - static InvalidSenderConfigError = this.SendEventMessagesUseCaseError.subclass( - "InvalidSenderConfigError", - ); - static EventConfigNotActiveError = this.SendEventMessagesUseCaseError.subclass( - "EventConfigNotActiveError", - ); + static EmailCompilationError = this.BaseError.subclass("EmailCompilationError"); + static InvalidSenderConfigError = this.BaseError.subclass("InvalidSenderConfigError"); + static EventConfigNotActiveError = this.BaseError.subclass("EventConfigNotActiveError"); + static EventSettingsMissingError = this.BaseError.subclass("EventSettingsMissingError"); constructor( private deps: { @@ -40,6 +38,116 @@ export class SendEventMessagesUseCase { }, ) {} + private async processSingleConfiguration({ + config, + event, + payload, + recipientEmail, + channelSlug, + }: { + config: SmtpConfiguration; + event: MessageEventTypes; + payload: unknown; + recipientEmail: string; + channelSlug: string; + }) { + const eventSettings = config.events.find((e) => e.eventType === event); + + if (!eventSettings) { + this.logger.info("Configuration found but settings for this event are missing"); + + return errAsync( + new SendEventMessagesUseCase.EventSettingsMissingError( + "Settings not found for this event", + { + props: { + event, + }, + }, + ), + ); + } + + if (!eventSettings.active) { + this.logger.info("Configuration found, but setting for this event are not active"); + + return errAsync( + new SendEventMessagesUseCase.EventConfigNotActiveError("Event config is disabled", { + props: { + event: eventSettings.eventType, + }, + }), + ); + } + + if (!config.senderName || !config.senderEmail) { + this.logger.warn("Configuration is invalid: missing sender data", { + senderName: config.senderName, + senderEmail: config.senderEmail, + }); + + return errAsync( + new SendEventMessagesUseCase.InvalidSenderConfigError("Missing sender name or email", { + props: { + senderName: config.senderName, + senderEmail: config.senderEmail, + }, + }), + ); + } + + const preparedEmailResult = this.deps.emailCompiler.compile({ + event: event, + payload: payload, + recipientEmail: recipientEmail, + bodyTemplate: eventSettings.template, + subjectTemplate: eventSettings.subject, + senderEmail: config.senderEmail, + senderName: config.senderName, + }); + + if (preparedEmailResult.isErr()) { + this.logger.warn("Failed to compile email template"); + + return errAsync( + new SendEventMessagesUseCase.EmailCompilationError("Failed to compile error", { + errors: [preparedEmailResult.error], + props: { + channelSlug, + event, + }, + }), + ); + } + + const smtpSettings: SendMailArgs["smtpSettings"] = { + host: config.smtpHost, + port: parseInt(config.smtpPort, 10), + encryption: config.encryption, + }; + + if (config.smtpUser) { + smtpSettings.auth = { + user: config.smtpUser, + pass: config.smtpPassword, + }; + } + + try { + // todo get errors from smtp and map to proper response + await this.deps.emailSender.sendEmailWithSmtp({ + mailData: preparedEmailResult.value, + smtpSettings, + }); + + return okAsync(null); // todo result + } catch (e) { + this.logger.error("SMTP returned errors", { error: e }); + + return errAsync(new SendEventMessagesUseCase.BaseError("Unhandled error")); + } + } + async sendEventMessages({ event, payload, @@ -50,7 +158,7 @@ export class SendEventMessagesUseCase { payload: unknown; recipientEmail: string; event: MessageEventTypes; - }) { + }): Promise>>> { this.logger.info("Calling sendEventMessages", { channelSlug, event }); const availableSmtpConfigurations = await this.deps.smtpConfigurationService.getConfigurations({ @@ -61,7 +169,7 @@ export class SendEventMessagesUseCase { if (availableSmtpConfigurations.isErr()) { this.logger.warn("Failed to fetch configuration"); - return err( + return err([ new SendEventMessagesUseCase.FailedToFetchConfigurationError( "Failed to fetch configuration", { @@ -72,13 +180,13 @@ export class SendEventMessagesUseCase { }, }, ), - ); + ]); } if (availableSmtpConfigurations.value.length === 0) { this.logger.warn("Configuration list is empty, app is not configured"); - return err( + return err([ new SendEventMessagesUseCase.MissingAvailableConfigurationError( "Missing configuration for this channel that is active", { @@ -89,107 +197,25 @@ export class SendEventMessagesUseCase { }, }, ), - ); + ]); } - /** - * TODO: Why this is not in parallel? - */ - for (const smtpConfiguration of availableSmtpConfigurations.value) { - this.logger.info("Detected configuration, will attempt to send email"); - - try { - const eventSettings = smtpConfiguration.events.find((e) => e.eventType === event); - - if (!eventSettings) { - this.logger.info("Configuration found but settings for this event are missing"); - /* - * Config missing, ignore - * todo log - * todo this probalby should be for / continue instead, or Promise.all - */ - return; - } - - if (!eventSettings.active) { - this.logger.info("Configuration found, but setting for this event are not active"); - - return errAsync( - new SendEventMessagesUseCase.EventConfigNotActiveError("Event config is disabled", { - props: { - event: eventSettings.eventType, - }, - }), - ); - } - - if (!smtpConfiguration.senderName || !smtpConfiguration.senderEmail) { - this.logger.warn("Configuration is invalid: missing sender data", { - senderName: smtpConfiguration.senderName, - senderEmail: smtpConfiguration.senderEmail, - }); - - return errAsync( - new SendEventMessagesUseCase.InvalidSenderConfigError("Missing sender name or email", { - props: { - senderName: smtpConfiguration.senderName, - senderEmail: smtpConfiguration.senderEmail, - }, - }), - ); - } - - const preparedEmailResult = this.deps.emailCompiler.compile({ - event: event, - payload: payload, - recipientEmail: recipientEmail, - bodyTemplate: eventSettings.template, - subjectTemplate: eventSettings.subject, - senderEmail: smtpConfiguration.senderEmail, - senderName: smtpConfiguration.senderName, - }); - - if (preparedEmailResult.isErr()) { - this.logger.warn("Failed to compile email template"); - - return errAsync( - new SendEventMessagesUseCase.EmailCompilationError("Failed to compile error", { - errors: [preparedEmailResult.error], - props: { - channelSlug, - event, - }, - }), - ); - } - - const smtpSettings: SendMailArgs["smtpSettings"] = { - host: smtpConfiguration.smtpHost, - port: parseInt(smtpConfiguration.smtpPort, 10), - encryption: smtpConfiguration.encryption, - }; - - if (smtpConfiguration.smtpUser) { - smtpSettings.auth = { - user: smtpConfiguration.smtpUser, - pass: smtpConfiguration.smtpPassword, - }; - } - - try { - // todo get errors from smtp and map to proper response - await this.deps.emailSender.sendEmailWithSmtp({ - mailData: preparedEmailResult.value, - smtpSettings, - }); - - return okAsync(null); // todo result - } catch (e) { - this.logger.error("SMTP returned errors", { error: e }); - } - } catch (e) { - this.logger.error("Error compiling"); - } - } + this.logger.info( + `Detected valid configuration (${availableSmtpConfigurations.value.length}). Will process to send emails`, + ); + + const processingResults = await Promise.all( + availableSmtpConfigurations.value.map((config) => + this.processSingleConfiguration({ + config, + event, + payload, + recipientEmail, + channelSlug, + }), + ), + ); + + return combineWithAllErrors(processingResults); } } From 9743b9df032cbbd7217e34c1882245976227213b Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Wed, 15 May 2024 16:51:25 +0200 Subject: [PATCH 05/11] refactor --- .../send-event-messages.use-case.ts | 71 +++++++++++-------- .../smtp/services/smtp-email-sender.ts | 5 +- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index 067d637da..f150ac46d 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -1,34 +1,49 @@ -import { - IGetSmtpConfiguration, - SmtpConfigurationService, -} from "../smtp/configuration/smtp-configuration.service"; +import { IGetSmtpConfiguration } from "../smtp/configuration/smtp-configuration.service"; import { IEmailCompiler } from "../smtp/services/email-compiler"; import { MessageEventTypes } from "./message-event-types"; import { createLogger } from "../../logger"; import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; import { BaseError } from "../../errors"; -import { err, errAsync, okAsync, Result } from "neverthrow"; +import { err, errAsync, Result, ResultAsync } from "neverthrow"; import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema"; import combineWithAllErrors = Result.combineWithAllErrors; -/* - * todo test - * todo: how this service should handle error for one config and success for another? - */ export class SendEventMessagesUseCase { - private logger = createLogger("SendEventMessagesUseCase"); - static BaseError = BaseError.subclass("SendEventMessagesUseCaseError"); - static FailedToFetchConfigurationError = this.BaseError.subclass( + + /** + * Errors thrown when something goes wrong and Saleor should retry + */ + static ServerError = BaseError.subclass("SendEventMessagesUseCaseServerError"); + + static FailedToFetchConfigurationError = this.ServerError.subclass( "FailedToFetchConfigurationError", ); - static MissingAvailableConfigurationError = this.BaseError.subclass( + + /** + * Errors related to broken configuration + */ + static ClientError = BaseError.subclass("SendEventMessagesUseCaseServerError"); + + static MissingAvailableConfigurationError = this.ClientError.subclass( "MissingAvailableConfigurationError", ); - static EmailCompilationError = this.BaseError.subclass("EmailCompilationError"); - static InvalidSenderConfigError = this.BaseError.subclass("InvalidSenderConfigError"); - static EventConfigNotActiveError = this.BaseError.subclass("EventConfigNotActiveError"); - static EventSettingsMissingError = this.BaseError.subclass("EventSettingsMissingError"); + + static EmailCompilationError = this.ClientError.subclass("EmailCompilationError"); + + static InvalidSenderConfigError = this.ClientError.subclass("InvalidSenderConfigError"); + + /** + * Errors that externally can be translated to no-op operations due to design of the app. + * In some cases app should just ignore the event, e.g. when it's disabled. + */ + static NoOpError = BaseError.subclass("SendEventMessagesUseCaseServerError"); + + static EventConfigNotActiveError = this.NoOpError.subclass("EventConfigNotActiveError"); + + static EventSettingsMissingError = this.NoOpError.subclass("EventSettingsMissingError"); + + private logger = createLogger("SendEventMessagesUseCase"); constructor( private deps: { @@ -38,7 +53,7 @@ export class SendEventMessagesUseCase { }, ) {} - private async processSingleConfiguration({ + private processSingleConfiguration({ config, event, payload, @@ -133,19 +148,17 @@ export class SendEventMessagesUseCase { }; } - try { - // todo get errors from smtp and map to proper response - await this.deps.emailSender.sendEmailWithSmtp({ + return ResultAsync.fromPromise( + this.deps.emailSender.sendEmailWithSmtp({ mailData: preparedEmailResult.value, smtpSettings, - }); - - return okAsync(null); // todo result - } catch (e) { - this.logger.error("SMTP returned errors", { error: e }); - - return errAsync(new SendEventMessagesUseCase.BaseError("Unhandled error")); - } + }), + (err) => { + return new SendEventMessagesUseCase.ServerError("Failed to send email via SMTP", { + errors: [err], + }); + }, + ); } async sendEventMessages({ diff --git a/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts b/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts index 15f309a57..617fa1c51 100644 --- a/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts +++ b/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts @@ -22,9 +22,12 @@ export interface SendMailArgs { } export interface ISMTPEmailSender { - sendEmailWithSmtp({ smtpSettings, mailData }: SendMailArgs): Promise<{ response: any }>; + sendEmailWithSmtp({ smtpSettings, mailData }: SendMailArgs): Promise<{ response: unknown }>; } +/** + * TODO: Implement errors mapping and neverthrow + */ export class SmtpEmailSender implements ISMTPEmailSender { private logger = createLogger("SmtpEmailSender"); From 6421a6ef64b9304675e68c9d1d7c581c0fa300de Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 17 May 2024 13:54:43 +0200 Subject: [PATCH 06/11] add missing test --- .../send-event-messages.use-case.test.ts | 27 ++++++++++++++++++- .../send-event-messages.use-case.ts | 6 +++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts index 1b42711c9..aec9e6c4c 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts @@ -38,6 +38,10 @@ class MockSmtpSender implements ISMTPEmailSender { return { response: {} }; }; + static throwInvalidResponse: ISMTPEmailSender["sendEmailWithSmtp"] = async () => { + throw new BaseError("Some error"); + }; + mockSendEmailMethod = vi.fn< Parameters, ReturnType @@ -211,7 +215,28 @@ describe("SendEventMessagesUseCase", () => { expect(emailSender.mockSendEmailMethod).toHaveBeenCalledTimes(2); }); - it.todo("Returns error if at least one configuration fails, even if second one works"); + it("Returns error if at least one configuration fails, even if second one works", async () => { + smtpConfigurationService.mockGetConfigurationsMethod.mockImplementation( + MockSmptConfigurationService.returnValidTwoConfigurations, + ); + + emailSender.mockSendEmailMethod.mockImplementationOnce(MockSmtpSender.returnEmptyResponse); + + emailSender.mockSendEmailMethod.mockImplementationOnce(MockSmtpSender.throwInvalidResponse); + + const result = await useCaseInstance.sendEventMessages({ + event: EVENT_TYPE, + payload: {}, + channelSlug: "channel-slug", + recipientEmail: "email@example.com", + }); + + expect(result.isErr()).toBe(true); + + const errors = result._unsafeUnwrapErr(); + + expect(errors[0]).toBeInstanceOf(SendEventMessagesUseCase.ServerError); + }); }); describe("Single configuration assigned for the event", () => { diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts index f150ac46d..60ab8d1f7 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts @@ -135,6 +135,8 @@ export class SendEventMessagesUseCase { ); } + this.logger.info("Successfully compiled email template"); + const smtpSettings: SendMailArgs["smtpSettings"] = { host: config.smtpHost, port: parseInt(config.smtpPort, 10), @@ -142,6 +144,8 @@ export class SendEventMessagesUseCase { }; if (config.smtpUser) { + this.logger.debug("Detected SMTP user config. Applying to configuration."); + smtpSettings.auth = { user: config.smtpUser, pass: config.smtpPassword, @@ -154,6 +158,8 @@ export class SendEventMessagesUseCase { smtpSettings, }), (err) => { + this.logger.debug("Error sending email with SMTP", { error: err }); + return new SendEventMessagesUseCase.ServerError("Failed to send email via SMTP", { errors: [err], }); From b1128d347b64dfbd43af3007ed95e1260457ffc6 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 17 May 2024 13:56:55 +0200 Subject: [PATCH 07/11] move usecase to folder --- .../send-event-messages.use-case.factory.ts | 20 +++++++++---------- .../send-event-messages.use-case.test.ts | 12 +++++------ .../send-event-messages.use-case.ts | 14 ++++++------- .../src/pages/api/webhooks/gift-card-sent.ts | 2 +- .../src/pages/api/webhooks/invoice-sent.ts | 2 +- apps/smtp/src/pages/api/webhooks/notify.ts | 2 +- .../src/pages/api/webhooks/order-cancelled.ts | 2 +- .../src/pages/api/webhooks/order-confirmed.ts | 2 +- .../src/pages/api/webhooks/order-created.ts | 2 +- .../src/pages/api/webhooks/order-fulfilled.ts | 2 +- .../pages/api/webhooks/order-fully-paid.ts | 2 +- .../src/pages/api/webhooks/order-refunded.ts | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) rename apps/smtp/src/modules/event-handlers/{ => use-case}/send-event-messages.use-case.factory.ts (52%) rename apps/smtp/src/modules/event-handlers/{ => use-case}/send-event-messages.use-case.test.ts (95%) rename apps/smtp/src/modules/event-handlers/{ => use-case}/send-event-messages.use-case.ts (93%) diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts b/apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.factory.ts similarity index 52% rename from apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts rename to apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.factory.ts index a608b1a90..2a94d73c4 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.factory.ts +++ b/apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.factory.ts @@ -1,15 +1,15 @@ import { AuthData } from "@saleor/app-sdk/APL"; import { SendEventMessagesUseCase } from "./send-event-messages.use-case"; -import { SmtpEmailSender } from "../smtp/services/smtp-email-sender"; -import { EmailCompiler } from "../smtp/services/email-compiler"; -import { HandlebarsTemplateCompiler } from "../smtp/services/handlebars-template-compiler"; -import { HtmlToTextCompiler } from "../smtp/services/html-to-text-compiler"; -import { MjmlCompiler } from "../smtp/services/mjml-compiler"; -import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service"; -import { FeatureFlagService } from "../feature-flag-service/feature-flag-service"; -import { SmtpMetadataManager } from "../smtp/configuration/smtp-metadata-manager"; -import { createSettingsManager } from "../../lib/metadata-manager"; -import { createInstrumentedGraphqlClient } from "../../lib/create-instrumented-graphql-client"; +import { SmtpEmailSender } from "../../smtp/services/smtp-email-sender"; +import { EmailCompiler } from "../../smtp/services/email-compiler"; +import { HandlebarsTemplateCompiler } from "../../smtp/services/handlebars-template-compiler"; +import { HtmlToTextCompiler } from "../../smtp/services/html-to-text-compiler"; +import { MjmlCompiler } from "../../smtp/services/mjml-compiler"; +import { SmtpConfigurationService } from "../../smtp/configuration/smtp-configuration.service"; +import { FeatureFlagService } from "../../feature-flag-service/feature-flag-service"; +import { SmtpMetadataManager } from "../../smtp/configuration/smtp-metadata-manager"; +import { createSettingsManager } from "../../../lib/metadata-manager"; +import { createInstrumentedGraphqlClient } from "../../../lib/create-instrumented-graphql-client"; export class SendEventMessagesUseCaseFactory { createFromAuthData(authData: AuthData): SendEventMessagesUseCase { diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts b/apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.test.ts similarity index 95% rename from apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts rename to apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.test.ts index aec9e6c4c..ea0449e61 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.test.ts +++ b/apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.test.ts @@ -2,12 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { SendEventMessagesUseCase } from "./send-event-messages.use-case"; import { SendEventMessagesUseCaseFactory } from "./send-event-messages.use-case.factory"; import { err, errAsync, ok, okAsync, Result } from "neverthrow"; -import { CompiledEmail, IEmailCompiler } from "../smtp/services/email-compiler"; -import { BaseError } from "../../errors"; -import { ISMTPEmailSender } from "../smtp/services/smtp-email-sender"; -import { IGetSmtpConfiguration } from "../smtp/configuration/smtp-configuration.service"; -import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema"; -import { MessageEventTypes } from "./message-event-types"; +import { CompiledEmail, IEmailCompiler } from "../../smtp/services/email-compiler"; +import { BaseError } from "../../../errors"; +import { ISMTPEmailSender } from "../../smtp/services/smtp-email-sender"; +import { IGetSmtpConfiguration } from "../../smtp/configuration/smtp-configuration.service"; +import { SmtpConfiguration } from "../../smtp/configuration/smtp-config-schema"; +import { MessageEventTypes } from "../message-event-types"; const EVENT_TYPE = "ACCOUNT_DELETE" satisfies MessageEventTypes; diff --git a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts b/apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.ts similarity index 93% rename from apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts rename to apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.ts index 60ab8d1f7..270003399 100644 --- a/apps/smtp/src/modules/event-handlers/send-event-messages.use-case.ts +++ b/apps/smtp/src/modules/event-handlers/use-case/send-event-messages.use-case.ts @@ -1,11 +1,11 @@ -import { IGetSmtpConfiguration } from "../smtp/configuration/smtp-configuration.service"; -import { IEmailCompiler } from "../smtp/services/email-compiler"; -import { MessageEventTypes } from "./message-event-types"; -import { createLogger } from "../../logger"; -import { ISMTPEmailSender, SendMailArgs } from "../smtp/services/smtp-email-sender"; -import { BaseError } from "../../errors"; +import { IGetSmtpConfiguration } from "../../smtp/configuration/smtp-configuration.service"; +import { IEmailCompiler } from "../../smtp/services/email-compiler"; +import { MessageEventTypes } from "../message-event-types"; +import { createLogger } from "../../../logger"; +import { ISMTPEmailSender, SendMailArgs } from "../../smtp/services/smtp-email-sender"; +import { BaseError } from "../../../errors"; import { err, errAsync, Result, ResultAsync } from "neverthrow"; -import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema"; +import { SmtpConfiguration } from "../../smtp/configuration/smtp-config-schema"; import combineWithAllErrors = Result.combineWithAllErrors; export class SendEventMessagesUseCase { diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index 70cd0ac57..c6f4aeb89 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -4,7 +4,7 @@ import { saleorApp } from "../../../saleor-app"; import { GiftCardSentWebhookPayloadFragment } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const GiftCardSentWebhookPayload = gql` fragment GiftCardSentWebhookPayload on GiftCardSent { diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index 514aafbe3..1bd306482 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -7,7 +7,7 @@ import { } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const InvoiceSentWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index c2aeab886..694efecd6 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -3,7 +3,7 @@ import { saleorApp } from "../../../saleor-app"; import { notifyEventMapping, NotifySubscriptionPayload } from "../../../lib/notify-event-types"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; /* * The Notify webhook is triggered on multiple Saleor events. diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index 86bb24efa..8ad830b23 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -7,7 +7,7 @@ import { } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const OrderCancelledWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index 7de0973b8..ff35db7b8 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -7,7 +7,7 @@ import { } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const OrderConfirmedWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index 6e6464c3e..976283a08 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -5,7 +5,7 @@ import { saleorApp } from "../../../saleor-app"; import { OrderCreatedWebhookPayloadFragment } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const OrderCreatedWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index 86247a028..5b769dfee 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -7,7 +7,7 @@ import { } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const OrderFulfilledWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index 551d62cf2..da2b4a4e8 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -7,7 +7,7 @@ import { } from "../../../../generated/graphql"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const OrderFullyPaidWebhookPayload = gql` ${OrderDetailsFragmentDoc} diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index 44eac1357..c1383df0e 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -7,7 +7,7 @@ import { gql } from "urql"; import { saleorApp } from "../../../saleor-app"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; -import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; const OrderRefundedWebhookPayload = gql` ${OrderDetailsFragmentDoc} From 71e39579ea48ecadfda6afb107716e493266c1d0 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 17 May 2024 14:08:22 +0200 Subject: [PATCH 08/11] Apply use case error handling to webhook handlers --- .../src/pages/api/webhooks/gift-card-sent.ts | 51 ++++++++++++++++--- .../src/pages/api/webhooks/invoice-sent.ts | 51 ++++++++++++++++--- apps/smtp/src/pages/api/webhooks/notify.ts | 49 +++++++++++++++--- .../src/pages/api/webhooks/order-cancelled.ts | 51 ++++++++++++++++--- .../src/pages/api/webhooks/order-confirmed.ts | 51 ++++++++++++++++--- .../src/pages/api/webhooks/order-created.ts | 51 ++++++++++++++++--- .../src/pages/api/webhooks/order-fulfilled.ts | 51 ++++++++++++++++--- .../pages/api/webhooks/order-fully-paid.ts | 51 ++++++++++++++++--- .../src/pages/api/webhooks/order-refunded.ts | 51 ++++++++++++++++--- 9 files changed, 386 insertions(+), 71 deletions(-) diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index c6f4aeb89..4b61bd6ab 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -5,6 +5,8 @@ import { GiftCardSentWebhookPayloadFragment } from "../../../../generated/graphq import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const GiftCardSentWebhookPayload = gql` fragment GiftCardSentWebhookPayload on GiftCardSent { @@ -103,14 +105,47 @@ const handler: NextWebhookApiHandler = async const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "GIFT_CARD_SENT", - payload, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "GIFT_CARD_SENT", + payload, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel(giftCardSentWebhook.createHandler(handler), "/api/webhooks/gift-card-sent"); diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index 1bd306482..fd1ac69d6 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -8,6 +8,8 @@ import { import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const InvoiceSentWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -84,14 +86,47 @@ const handler: NextWebhookApiHandler = async const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "INVOICE_SENT", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "INVOICE_SENT", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel(invoiceSentWebhook.createHandler(handler), "api/webhooks/invoice-sent"); diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index 694efecd6..a3c44fcb7 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -4,6 +4,8 @@ import { notifyEventMapping, NotifySubscriptionPayload } from "../../../lib/noti import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; /* * The Notify webhook is triggered on multiple Saleor events. @@ -48,14 +50,47 @@ const handler: NextWebhookApiHandler = async (req, re const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event, - payload: payload.payload, - recipientEmail, - }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event, + payload: payload.payload, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); - return res.status(200).json({ message: "The event has been handled" }); + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel(notifyWebhook.createHandler(handler), "api/webhooks/notify"); diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index 8ad830b23..242302592 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -8,6 +8,8 @@ import { import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const OrderCancelledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -67,14 +69,47 @@ const handler: NextWebhookApiHandler = asy const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "ORDER_CANCELLED", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "ORDER_CANCELLED", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel( diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index ff35db7b8..004c77e08 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -8,6 +8,8 @@ import { import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const OrderConfirmedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -68,14 +70,47 @@ const handler: NextWebhookApiHandler = asy const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "ORDER_CONFIRMED", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "ORDER_CONFIRMED", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel( diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index 976283a08..d1f21a41f 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -6,6 +6,8 @@ import { OrderCreatedWebhookPayloadFragment } from "../../../../generated/graphq import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const OrderCreatedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -65,14 +67,47 @@ const handler: NextWebhookApiHandler = async const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "ORDER_CREATED", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "ORDER_CREATED", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel(orderCreatedWebhook.createHandler(handler), "api/webhooks/order-created"); diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index 5b769dfee..ecde0aab9 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -8,6 +8,8 @@ import { import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const OrderFulfilledWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -68,14 +70,47 @@ const handler: NextWebhookApiHandler = asy const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "ORDER_FULFILLED", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "ORDER_FULFILLED", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel( diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index da2b4a4e8..a5c75de88 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -8,6 +8,8 @@ import { import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const OrderFullyPaidWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -68,14 +70,47 @@ const handler: NextWebhookApiHandler = asy const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "ORDER_FULLY_PAID", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "ORDER_FULLY_PAID", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel( diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index c1383df0e..364790cfe 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -8,6 +8,8 @@ import { saleorApp } from "../../../saleor-app"; import { withOtel } from "@saleor/apps-otel"; import { createLogger } from "../../../logger"; import { SendEventMessagesUseCaseFactory } from "../../../modules/event-handlers/use-case/send-event-messages.use-case.factory"; +import { SendEventMessagesUseCase } from "../../../modules/event-handlers/use-case/send-event-messages.use-case"; +import { captureException } from "@sentry/nextjs"; const OrderRefundedWebhookPayload = gql` ${OrderDetailsFragmentDoc} @@ -67,14 +69,47 @@ const handler: NextWebhookApiHandler = asyn const useCase = useCaseFactory.createFromAuthData(authData); - await useCase.sendEventMessages({ - channelSlug: channel, - event: "ORDER_REFUNDED", - payload: { order: payload.order }, - recipientEmail, - }); - - return res.status(200).json({ message: "The event has been handled" }); + return useCase + .sendEventMessages({ + channelSlug: channel, + event: "ORDER_REFUNDED", + payload: { order: payload.order }, + recipientEmail, + }) + .then((result) => + result.match( + (r) => { + logger.info("Successfully sent email(s)"); + + return res.status(200).json({ message: "The event has been handled" }); + }, + (err) => { + switch (err[0].constructor) { + case SendEventMessagesUseCase.ServerError: { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.ClientError: { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } + case SendEventMessagesUseCase.NoOpError: { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); + } + default: { + logger.error("Failed to send email(s) [server error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); + } + } + }, + ), + ); }; export default withOtel(orderRefundedWebhook.createHandler(handler), "api/webhooks/order-refunded"); From ddc1fdcbe71b8ec3f75b92f31d162689f53bcbe7 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 17 May 2024 15:17:00 +0200 Subject: [PATCH 09/11] Fix error mapping --- .../smtp-configuration.router.ts | 2 + .../configuration/smtp-metadata-manager.ts | 36 ++++++++++------ .../smtp/services/smtp-email-sender.ts | 2 +- .../src/pages/api/webhooks/gift-card-sent.ts | 41 +++++++++---------- .../src/pages/api/webhooks/invoice-sent.ts | 41 +++++++++---------- apps/smtp/src/pages/api/webhooks/notify.ts | 41 +++++++++---------- .../src/pages/api/webhooks/order-cancelled.ts | 41 +++++++++---------- .../src/pages/api/webhooks/order-confirmed.ts | 41 +++++++++---------- .../src/pages/api/webhooks/order-created.ts | 41 +++++++++---------- .../src/pages/api/webhooks/order-fulfilled.ts | 41 +++++++++---------- .../pages/api/webhooks/order-fully-paid.ts | 41 +++++++++---------- .../src/pages/api/webhooks/order-refunded.ts | 41 +++++++++---------- 12 files changed, 198 insertions(+), 211 deletions(-) diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts index c209e850e..fb3501ad1 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-configuration.router.ts @@ -23,6 +23,8 @@ import { createLogger } from "../../../logger"; export const throwTrpcErrorFromConfigurationServiceError = ( error: typeof SmtpConfigurationService.SmtpConfigurationServiceError | unknown, ) => { + createLogger("trpcError").debug("Error from TRPC", { error }); + if (error instanceof SmtpConfigurationService.SmtpConfigurationServiceError) { switch (error["constructor"]) { case SmtpConfigurationService.ConfigNotFoundError: diff --git a/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts b/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts index 9c9aa41f9..aedf8c5a5 100644 --- a/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts +++ b/apps/smtp/src/modules/smtp/configuration/smtp-metadata-manager.ts @@ -1,10 +1,13 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager"; import { SmtpConfig } from "./smtp-config-schema"; -import { fromAsyncThrowable, fromThrowable, ok, ResultAsync } from "neverthrow"; +import { fromAsyncThrowable, fromPromise, fromThrowable, ok, ResultAsync } from "neverthrow"; import { BaseError } from "../../../errors"; +import { createLogger } from "../../../logger"; +// todo test export class SmtpMetadataManager { private metadataKey = "smtp-config"; + private logger = createLogger("SmtpMetadataManager"); static SmtpMetadataManagerError = BaseError.subclass("SmtpMetadataManagerError"); static ParseConfigError = this.SmtpMetadataManagerError.subclass("ParseConfigError"); @@ -22,14 +25,21 @@ export class SmtpMetadataManager { typeof SmtpMetadataManager.ParseConfigError | typeof SmtpMetadataManager.FetchConfigError > > { - return fromAsyncThrowable( - this.metadataManager.get, - SmtpMetadataManager.FetchConfigError.normalize, - )(this.metadataKey, this.saleorApiUrl).andThen((config) => { + this.logger.debug("Fetching SMTP config"); + + return fromPromise(this.metadataManager.get(this.metadataKey, this.saleorApiUrl), (e) => { + this.logger.debug("Failed to fetch config", { error: e }); + + return new SmtpMetadataManager.FetchConfigError("Failed to fetch metadata", { errors: [e] }); + }).andThen((config) => { if (!config) { + this.logger.debug("Config not found, returning undefined"); + return ok(undefined); } + this.logger.debug("Config found, will parse JSON now"); + return fromThrowable(JSON.parse, SmtpMetadataManager.ParseConfigError.normalize)(config); }); } @@ -37,13 +47,15 @@ export class SmtpMetadataManager { setConfig( config: SmtpConfig, ): ResultAsync> { - return ResultAsync.fromThrowable( - this.metadataManager.set, + this.logger.debug("Trying to set config"); + + return ResultAsync.fromPromise( + this.metadataManager.set({ + key: this.metadataKey, + value: JSON.stringify(config), + domain: this.saleorApiUrl, + }), SmtpMetadataManager.SetConfigError.normalize, - )({ - key: this.metadataKey, - value: JSON.stringify(config), - domain: this.saleorApiUrl, - }); + ); } } diff --git a/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts b/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts index 617fa1c51..376279323 100644 --- a/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts +++ b/apps/smtp/src/modules/smtp/services/smtp-email-sender.ts @@ -88,7 +88,7 @@ export class SmtpEmailSender implements ISMTPEmailSender { ...mailData, }); - this.logger.debug("An email has been sent"); + this.logger.debug("An email has been sent", { response }); return { response }; } catch (error) { diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index 4b61bd6ab..7654b7284 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -120,29 +120,26 @@ const handler: NextWebhookApiHandler = async return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index fd1ac69d6..b808a965b 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -101,29 +101,26 @@ const handler: NextWebhookApiHandler = async return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index a3c44fcb7..44e892778 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -65,29 +65,26 @@ const handler: NextWebhookApiHandler = async (req, re return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index 242302592..20e9663c6 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -84,29 +84,26 @@ const handler: NextWebhookApiHandler = asy return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index 004c77e08..ea42647ad 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -85,29 +85,26 @@ const handler: NextWebhookApiHandler = asy return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index d1f21a41f..bf40ac500 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -82,29 +82,26 @@ const handler: NextWebhookApiHandler = async return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index ecde0aab9..b234e29b6 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -85,29 +85,26 @@ const handler: NextWebhookApiHandler = asy return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index a5c75de88..e0e29bea5 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -85,29 +85,26 @@ const handler: NextWebhookApiHandler = asy return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index 364790cfe..ea4726760 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -84,29 +84,26 @@ const handler: NextWebhookApiHandler = asyn return res.status(200).json({ message: "The event has been handled" }); }, (err) => { - switch (err[0].constructor) { - case SendEventMessagesUseCase.ServerError: { - logger.error("Failed to send email(s) [server error]", { error: err }); - - return res.status(500).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.ClientError: { - logger.info("Failed to send email(s) [client error]", { error: err }); - - return res.status(400).json({ message: "Failed to send email" }); - } - case SendEventMessagesUseCase.NoOpError: { - logger.error("Sending emails aborted [no op]", { error: err }); - - return res.status(200).json({ message: "The event has been handled [no op]" }); - } - default: { - logger.error("Failed to send email(s) [server error]", { error: err }); - captureException(new Error("Unhandled useCase error", { cause: err })); - - return res.status(500).json({ message: "Failed to send email [unhandled]" }); - } + const errorInstance = err[0]; + + if (errorInstance instanceof SendEventMessagesUseCase.ServerError) { + logger.error("Failed to send email(s) [server error]", { error: err }); + + return res.status(500).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.ClientError) { + logger.info("Failed to send email(s) [client error]", { error: err }); + + return res.status(400).json({ message: "Failed to send email" }); + } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { + logger.error("Sending emails aborted [no op]", { error: err }); + + return res.status(200).json({ message: "The event has been handled [no op]" }); } + + logger.error("Failed to send email(s) [unhandled error]", { error: err }); + captureException(new Error("Unhandled useCase error", { cause: err })); + + return res.status(500).json({ message: "Failed to send email [unhandled]" }); }, ), ); From d13cf57507f2028db9c052e1f5893af53f43799a Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Fri, 17 May 2024 15:29:03 +0200 Subject: [PATCH 10/11] readme --- apps/smtp/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/smtp/README.md b/apps/smtp/README.md index a8e22d309..090c0ebb1 100644 --- a/apps/smtp/README.md +++ b/apps/smtp/README.md @@ -4,3 +4,7 @@ After extraction is done, there will be: 1. Separate SMTP app 2. Separate Sendgrid app 3. Old emails-and-messages app will be deprecated and removed + +# Development and testing + +Easiest way to test configuration is to use [Mailtrap](https://mailtrap.io/) From b5f797a2642f32352536e978e9f563e4bc6fdf20 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 20 May 2024 19:47:38 +0200 Subject: [PATCH 11/11] change noop error log to info --- apps/smtp/src/pages/api/webhooks/gift-card-sent.ts | 2 +- apps/smtp/src/pages/api/webhooks/invoice-sent.ts | 2 +- apps/smtp/src/pages/api/webhooks/notify.ts | 2 +- apps/smtp/src/pages/api/webhooks/order-cancelled.ts | 2 +- apps/smtp/src/pages/api/webhooks/order-confirmed.ts | 2 +- apps/smtp/src/pages/api/webhooks/order-created.ts | 2 +- apps/smtp/src/pages/api/webhooks/order-fulfilled.ts | 2 +- apps/smtp/src/pages/api/webhooks/order-fully-paid.ts | 2 +- apps/smtp/src/pages/api/webhooks/order-refunded.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts index 7654b7284..1c95ebd3a 100644 --- a/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/gift-card-sent.ts @@ -131,7 +131,7 @@ const handler: NextWebhookApiHandler = async return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts index b808a965b..988303a47 100644 --- a/apps/smtp/src/pages/api/webhooks/invoice-sent.ts +++ b/apps/smtp/src/pages/api/webhooks/invoice-sent.ts @@ -112,7 +112,7 @@ const handler: NextWebhookApiHandler = async return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/notify.ts b/apps/smtp/src/pages/api/webhooks/notify.ts index 44e892778..ebff2f2e5 100644 --- a/apps/smtp/src/pages/api/webhooks/notify.ts +++ b/apps/smtp/src/pages/api/webhooks/notify.ts @@ -76,7 +76,7 @@ const handler: NextWebhookApiHandler = async (req, re return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts index 20e9663c6..d9457cb74 100644 --- a/apps/smtp/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-cancelled.ts @@ -95,7 +95,7 @@ const handler: NextWebhookApiHandler = asy return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts index ea42647ad..9d4f1c427 100644 --- a/apps/smtp/src/pages/api/webhooks/order-confirmed.ts +++ b/apps/smtp/src/pages/api/webhooks/order-confirmed.ts @@ -96,7 +96,7 @@ const handler: NextWebhookApiHandler = asy return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/order-created.ts b/apps/smtp/src/pages/api/webhooks/order-created.ts index bf40ac500..2fca763d9 100644 --- a/apps/smtp/src/pages/api/webhooks/order-created.ts +++ b/apps/smtp/src/pages/api/webhooks/order-created.ts @@ -93,7 +93,7 @@ const handler: NextWebhookApiHandler = async return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts index b234e29b6..5bd2bdbb2 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fulfilled.ts @@ -96,7 +96,7 @@ const handler: NextWebhookApiHandler = asy return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts index e0e29bea5..ce7aa8781 100644 --- a/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/smtp/src/pages/api/webhooks/order-fully-paid.ts @@ -96,7 +96,7 @@ const handler: NextWebhookApiHandler = asy return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); } diff --git a/apps/smtp/src/pages/api/webhooks/order-refunded.ts b/apps/smtp/src/pages/api/webhooks/order-refunded.ts index ea4726760..bb24dac7d 100644 --- a/apps/smtp/src/pages/api/webhooks/order-refunded.ts +++ b/apps/smtp/src/pages/api/webhooks/order-refunded.ts @@ -95,7 +95,7 @@ const handler: NextWebhookApiHandler = asyn return res.status(400).json({ message: "Failed to send email" }); } else if (errorInstance instanceof SendEventMessagesUseCase.NoOpError) { - logger.error("Sending emails aborted [no op]", { error: err }); + logger.info("Sending emails aborted [no op]", { error: err }); return res.status(200).json({ message: "The event has been handled [no op]" }); }