From 7df2dc3f332c1e83fdff900cd1a83c4108c76133 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 07:43:14 +0000 Subject: [PATCH 1/5] Initial plan From 94cb894c555600ba31038154431ed0e161d84f99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 07:55:18 +0000 Subject: [PATCH 2/5] Add ReservationRequestCreated event and handler for email notification Co-authored-by: rohit-r-kumar <175348946+rohit-r-kumar@users.noreply.github.com> --- apps/api/src/index.ts | 8 +- .../reservation-request.ts | 12 ++ .../sthrift/domain/src/domain/events/index.ts | 3 +- .../domain/src/domain/events/types/index.ts | 1 + .../types/reservation-request-created.ts | 12 ++ .../event-handler/src/handlers/index.ts | 6 +- .../src/handlers/integration/index.ts | 5 +- ...ervation-request-created--notify-sharer.ts | 114 ++++++++++++++++++ 8 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 packages/sthrift/domain/src/domain/events/types/index.ts create mode 100644 packages/sthrift/domain/src/domain/events/types/reservation-request-created.ts create mode 100644 packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 76e941623..b3780c369 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -24,6 +24,7 @@ import { ServiceMessagingMock } from '@sthrift/messaging-service-mock'; import { graphHandlerCreator } from '@sthrift/graphql'; import { restHandlerCreator } from '@sthrift/rest'; import { ServiceCybersource } from '@sthrift/service-cybersource'; +import { SendGrid } from '@sthrift/service-sendgrid'; const { NODE_ENV } = process.env; const isDevelopment = NODE_ENV === 'development'; @@ -45,7 +46,8 @@ Cellix.initializeInfrastructureServices( .registerInfrastructureService( isDevelopment ? new ServiceMessagingMock() : new ServiceMessagingTwilio(), ) - .registerInfrastructureService(new ServiceCybersource()); + .registerInfrastructureService(new ServiceCybersource()) + .registerInfrastructureService(new SendGrid('magic-link-email')); }, ) .setContext((serviceRegistry) => { @@ -59,8 +61,10 @@ Cellix.initializeInfrastructureServices( ? serviceRegistry.getInfrastructureService(ServiceMessagingMock) : serviceRegistry.getInfrastructureService(ServiceMessagingTwilio); + const sendGridService = serviceRegistry.getInfrastructureService(SendGrid); + const { domainDataSource } = dataSourcesFactory.withSystemPassport(); - RegisterEventHandlers(domainDataSource); + RegisterEventHandlers(domainDataSource, sendGridService); return { dataSourcesFactory, diff --git a/packages/sthrift/domain/src/domain/contexts/reservation-request/reservation-request/reservation-request.ts b/packages/sthrift/domain/src/domain/contexts/reservation-request/reservation-request/reservation-request.ts index 4901354f8..8167f16c8 100644 --- a/packages/sthrift/domain/src/domain/contexts/reservation-request/reservation-request/reservation-request.ts +++ b/packages/sthrift/domain/src/domain/contexts/reservation-request/reservation-request/reservation-request.ts @@ -9,6 +9,7 @@ import type { ReservationRequestEntityReference, ReservationRequestProps, } from './reservation-request.entity.ts'; +import { ReservationRequestCreatedEvent } from '../../../events/types/reservation-request-created.ts'; export class ReservationRequest extends DomainSeedwork.AggregateRoot @@ -52,6 +53,17 @@ export class ReservationRequest instance.reservationPeriodStart = reservationPeriodStart; instance.reservationPeriodEnd = reservationPeriodEnd; instance.isNew = false; + + // Fire integration event for reservation request creation + instance.addIntegrationEvent(ReservationRequestCreatedEvent, { + reservationRequestId: newProps.id, + listingId: listing.id, + reserverId: reserver.id, + sharerId: listing.sharer.id, + reservationPeriodStart, + reservationPeriodEnd, + }); + return instance; } diff --git a/packages/sthrift/domain/src/domain/events/index.ts b/packages/sthrift/domain/src/domain/events/index.ts index b3c5f8a04..ddfad773f 100644 --- a/packages/sthrift/domain/src/domain/events/index.ts +++ b/packages/sthrift/domain/src/domain/events/index.ts @@ -1 +1,2 @@ -export { EventBusInstance } from './event-bus.ts'; \ No newline at end of file +export { EventBusInstance } from './event-bus.ts'; +export * from './types/index.ts'; \ No newline at end of file diff --git a/packages/sthrift/domain/src/domain/events/types/index.ts b/packages/sthrift/domain/src/domain/events/types/index.ts new file mode 100644 index 000000000..7cc145c96 --- /dev/null +++ b/packages/sthrift/domain/src/domain/events/types/index.ts @@ -0,0 +1 @@ +export * from './reservation-request-created.ts'; diff --git a/packages/sthrift/domain/src/domain/events/types/reservation-request-created.ts b/packages/sthrift/domain/src/domain/events/types/reservation-request-created.ts new file mode 100644 index 000000000..557f98873 --- /dev/null +++ b/packages/sthrift/domain/src/domain/events/types/reservation-request-created.ts @@ -0,0 +1,12 @@ +import { DomainSeedwork } from '@cellix/domain-seedwork'; + +export interface ReservationRequestCreatedProps { + reservationRequestId: string; + listingId: string; + reserverId: string; + sharerId: string; + reservationPeriodStart: Date; + reservationPeriodEnd: Date; +} + +export class ReservationRequestCreatedEvent extends DomainSeedwork.CustomDomainEventImpl {} diff --git a/packages/sthrift/event-handler/src/handlers/index.ts b/packages/sthrift/event-handler/src/handlers/index.ts index 08253a3c6..a1d614f56 100644 --- a/packages/sthrift/event-handler/src/handlers/index.ts +++ b/packages/sthrift/event-handler/src/handlers/index.ts @@ -1,10 +1,12 @@ import type { DomainDataSource } from "@sthrift/domain"; +import type { SendGrid } from '@sthrift/service-sendgrid'; import { RegisterDomainEventHandlers } from "./domain/index.ts"; import { RegisterIntegrationEventHandlers } from "./integration/index.ts"; export const RegisterEventHandlers = ( - domainDataSource: DomainDataSource + domainDataSource: DomainDataSource, + sendGridService: SendGrid ) => { RegisterDomainEventHandlers(domainDataSource); - RegisterIntegrationEventHandlers(domainDataSource); + RegisterIntegrationEventHandlers(domainDataSource, sendGridService); } \ No newline at end of file diff --git a/packages/sthrift/event-handler/src/handlers/integration/index.ts b/packages/sthrift/event-handler/src/handlers/integration/index.ts index 3044527de..41dcdf5d6 100644 --- a/packages/sthrift/event-handler/src/handlers/integration/index.ts +++ b/packages/sthrift/event-handler/src/handlers/integration/index.ts @@ -1,7 +1,10 @@ import type { DomainDataSource } from '@sthrift/domain'; +import type { SendGrid } from '@sthrift/service-sendgrid'; +import { registerReservationRequestCreatedHandler } from './reservation-request-created--notify-sharer.ts'; export const RegisterIntegrationEventHandlers = ( domainDataSource: DomainDataSource, + sendGridService: SendGrid, ): void => { - console.log(domainDataSource); + registerReservationRequestCreatedHandler(domainDataSource, sendGridService); }; diff --git a/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts b/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts new file mode 100644 index 000000000..8c0c94c14 --- /dev/null +++ b/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts @@ -0,0 +1,114 @@ +import type { DomainDataSource } from '@sthrift/domain'; +import { ReservationRequestCreatedEvent } from '@sthrift/domain'; +import type { SendGrid } from '@sthrift/service-sendgrid'; + +export const ReservationRequestCreatedNotifySharerHandler = ( + domainDataSource: DomainDataSource, + sendGridService: SendGrid, +) => { + return async (payload: { + reservationRequestId: string; + listingId: string; + reserverId: string; + sharerId: string; + reservationPeriodStart: Date; + reservationPeriodEnd: Date; + }) => { + try { + console.log( + 'ReservationRequestCreatedNotifySharerHandler: Processing event for reservation request', + payload.reservationRequestId, + ); + + // Get the readonly datasource to fetch user information + const { readonlyDataSource } = + domainDataSource.withSystemPassport().dataSourcesFactory(); + + // Fetch the sharer (listing owner) details + const sharer = await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( + payload.sharerId, + ); + + if (!sharer) { + console.error( + `Sharer with ID ${payload.sharerId} not found for reservation request ${payload.reservationRequestId}`, + ); + return; + } + + // Fetch the reserver details + const reserver = + await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( + payload.reserverId, + ); + + if (!reserver) { + console.error( + `Reserver with ID ${payload.reserverId} not found for reservation request ${payload.reservationRequestId}`, + ); + return; + } + + // Fetch the listing details + const listing = + await readonlyDataSource.Listing.ItemListing.ItemListingReadRepo.getById( + payload.listingId, + ); + + if (!listing) { + console.error( + `Listing with ID ${payload.listingId} not found for reservation request ${payload.reservationRequestId}`, + ); + return; + } + + // Prepare email notification data + const sharerEmail = sharer.account.email; + const reserverName = reserver.account.username || reserver.account.email; + const listingTitle = listing.title; + const reservationStart = payload.reservationPeriodStart.toLocaleDateString(); + const reservationEnd = payload.reservationPeriodEnd.toLocaleDateString(); + + console.log( + `Sending reservation notification email to ${sharerEmail} for listing "${listingTitle}"`, + ); + + // TODO: Create a proper sendReservationNotification method in SendGrid service + // For now, we'll log that the email would be sent + console.log('Email would be sent with the following details:'); + console.log(` To: ${sharerEmail}`); + console.log(` Reserver: ${reserverName}`); + console.log(` Listing: ${listingTitle}`); + console.log(` Period: ${reservationStart} - ${reservationEnd}`); + + // Actual email sending would look like: + // await sendGridService.sendReservationNotification( + // sharerEmail, + // reserverName, + // listingTitle, + // reservationStart, + // reservationEnd + // ); + } catch (error) { + console.error( + 'Error in ReservationRequestCreatedNotifySharerHandler:', + error, + ); + // Don't throw - integration events should not affect the main transaction + } + }; +}; + +export const registerReservationRequestCreatedHandler = ( + domainDataSource: DomainDataSource, + sendGridService: SendGrid, +) => { + const eventBus = domainDataSource.withSystemPassport().eventBus(); + eventBus.register( + ReservationRequestCreatedEvent, + ReservationRequestCreatedNotifySharerHandler( + domainDataSource, + sendGridService, + ), + ); +}; From 1051f815f2b2970645342458e4008fc01436d008 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:06:34 +0000 Subject: [PATCH 3/5] Fix event handler implementation and dependencies Co-authored-by: rohit-r-kumar <175348946+rohit-r-kumar@users.noreply.github.com> --- apps/api/src/index.ts | 3 +- packages/sthrift/domain/src/domain/index.ts | 1 + packages/sthrift/event-handler/package.json | 5 +- .../src/handlers/domain/index.ts | 4 +- .../event-handler/src/handlers/index.ts | 8 +- .../src/handlers/integration/index.ts | 6 +- ...ervation-request-created--notify-sharer.ts | 177 +++++++++--------- pnpm-lock.yaml | 9 + 8 files changed, 112 insertions(+), 101 deletions(-) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index b3780c369..6dd76a66b 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -63,8 +63,7 @@ Cellix.initializeInfrastructureServices( const sendGridService = serviceRegistry.getInfrastructureService(SendGrid); - const { domainDataSource } = dataSourcesFactory.withSystemPassport(); - RegisterEventHandlers(domainDataSource, sendGridService); + RegisterEventHandlers(dataSourcesFactory, sendGridService); return { dataSourcesFactory, diff --git a/packages/sthrift/domain/src/domain/index.ts b/packages/sthrift/domain/src/domain/index.ts index 217e5d920..a6ce21215 100644 --- a/packages/sthrift/domain/src/domain/index.ts +++ b/packages/sthrift/domain/src/domain/index.ts @@ -2,3 +2,4 @@ export * as Contexts from './contexts/index.ts'; export type { Services } from './services/index.ts'; export { type Passport, PassportFactory } from './contexts/passport.ts'; +export * from './events/index.ts'; diff --git a/packages/sthrift/event-handler/package.json b/packages/sthrift/event-handler/package.json index ac7422cb0..3aeaf93ac 100644 --- a/packages/sthrift/event-handler/package.json +++ b/packages/sthrift/event-handler/package.json @@ -20,7 +20,10 @@ "clean": "rimraf dist" }, "dependencies": { - "@sthrift/domain": "workspace:*" + "@sthrift/domain": "workspace:*", + "@sthrift/persistence": "workspace:*", + "@sthrift/service-sendgrid": "workspace:*", + "@cellix/event-bus-seedwork-node": "workspace:*" }, "devDependencies": { "@cellix/typescript-config": "workspace:*", diff --git a/packages/sthrift/event-handler/src/handlers/domain/index.ts b/packages/sthrift/event-handler/src/handlers/domain/index.ts index 134f339f0..cb75d4761 100644 --- a/packages/sthrift/event-handler/src/handlers/domain/index.ts +++ b/packages/sthrift/event-handler/src/handlers/domain/index.ts @@ -1,7 +1,7 @@ -import type { DomainDataSource } from '@sthrift/domain'; +import type { DataSourcesFactory } from '@sthrift/persistence'; export const RegisterDomainEventHandlers = ( - _domainDataSource: DomainDataSource + _dataSourcesFactory: DataSourcesFactory ): void => { /* Register domain event handlers */ }; diff --git a/packages/sthrift/event-handler/src/handlers/index.ts b/packages/sthrift/event-handler/src/handlers/index.ts index a1d614f56..4a8a07101 100644 --- a/packages/sthrift/event-handler/src/handlers/index.ts +++ b/packages/sthrift/event-handler/src/handlers/index.ts @@ -1,12 +1,12 @@ -import type { DomainDataSource } from "@sthrift/domain"; +import type { DataSourcesFactory } from '@sthrift/persistence'; import type { SendGrid } from '@sthrift/service-sendgrid'; import { RegisterDomainEventHandlers } from "./domain/index.ts"; import { RegisterIntegrationEventHandlers } from "./integration/index.ts"; export const RegisterEventHandlers = ( - domainDataSource: DomainDataSource, + dataSourcesFactory: DataSourcesFactory, sendGridService: SendGrid ) => { - RegisterDomainEventHandlers(domainDataSource); - RegisterIntegrationEventHandlers(domainDataSource, sendGridService); + RegisterDomainEventHandlers(dataSourcesFactory); + RegisterIntegrationEventHandlers(dataSourcesFactory, sendGridService); } \ No newline at end of file diff --git a/packages/sthrift/event-handler/src/handlers/integration/index.ts b/packages/sthrift/event-handler/src/handlers/integration/index.ts index 41dcdf5d6..a941feeba 100644 --- a/packages/sthrift/event-handler/src/handlers/integration/index.ts +++ b/packages/sthrift/event-handler/src/handlers/integration/index.ts @@ -1,10 +1,10 @@ -import type { DomainDataSource } from '@sthrift/domain'; +import type { DataSourcesFactory } from '@sthrift/persistence'; import type { SendGrid } from '@sthrift/service-sendgrid'; import { registerReservationRequestCreatedHandler } from './reservation-request-created--notify-sharer.ts'; export const RegisterIntegrationEventHandlers = ( - domainDataSource: DomainDataSource, + dataSourcesFactory: DataSourcesFactory, sendGridService: SendGrid, ): void => { - registerReservationRequestCreatedHandler(domainDataSource, sendGridService); + registerReservationRequestCreatedHandler(dataSourcesFactory, sendGridService); }; diff --git a/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts b/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts index 8c0c94c14..48db57f0c 100644 --- a/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts +++ b/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts @@ -1,113 +1,112 @@ -import type { DomainDataSource } from '@sthrift/domain'; -import { ReservationRequestCreatedEvent } from '@sthrift/domain'; +import { Domain } from '@sthrift/domain'; +import type { DataSourcesFactory } from '@sthrift/persistence'; import type { SendGrid } from '@sthrift/service-sendgrid'; +import { NodeEventBusInstance } from '@cellix/event-bus-seedwork-node'; export const ReservationRequestCreatedNotifySharerHandler = ( - domainDataSource: DomainDataSource, - sendGridService: SendGrid, +dataSourcesFactory: DataSourcesFactory, +_sendGridService: SendGrid, ) => { - return async (payload: { - reservationRequestId: string; - listingId: string; - reserverId: string; - sharerId: string; - reservationPeriodStart: Date; - reservationPeriodEnd: Date; - }) => { - try { - console.log( - 'ReservationRequestCreatedNotifySharerHandler: Processing event for reservation request', - payload.reservationRequestId, - ); +return async (payload: { +reservationRequestId: string; +listingId: string; +reserverId: string; +sharerId: string; +reservationPeriodStart: Date; +reservationPeriodEnd: Date; +}) => { +try { +console.log( +'ReservationRequestCreatedNotifySharerHandler: Processing event for reservation request', +payload.reservationRequestId, +); - // Get the readonly datasource to fetch user information - const { readonlyDataSource } = - domainDataSource.withSystemPassport().dataSourcesFactory(); +// Get the datasources with system passport to fetch user information +const { readonlyDataSource } = dataSourcesFactory.withSystemPassport(); - // Fetch the sharer (listing owner) details - const sharer = await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( - payload.sharerId, - ); +// Fetch the sharer (listing owner) details +const sharer = await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( +payload.sharerId, +); - if (!sharer) { - console.error( - `Sharer with ID ${payload.sharerId} not found for reservation request ${payload.reservationRequestId}`, - ); - return; - } +if (!sharer) { +console.error( +`Sharer with ID ${payload.sharerId} not found for reservation request ${payload.reservationRequestId}`, +); +return; +} - // Fetch the reserver details - const reserver = - await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( - payload.reserverId, - ); +// Fetch the reserver details +const reserver = +await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( +payload.reserverId, +); - if (!reserver) { - console.error( - `Reserver with ID ${payload.reserverId} not found for reservation request ${payload.reservationRequestId}`, - ); - return; - } +if (!reserver) { +console.error( +`Reserver with ID ${payload.reserverId} not found for reservation request ${payload.reservationRequestId}`, +); +return; +} - // Fetch the listing details - const listing = - await readonlyDataSource.Listing.ItemListing.ItemListingReadRepo.getById( - payload.listingId, - ); +// Fetch the listing details +const listing = +await readonlyDataSource.Listing.ItemListing.ItemListingReadRepo.getById( +payload.listingId, +); - if (!listing) { - console.error( - `Listing with ID ${payload.listingId} not found for reservation request ${payload.reservationRequestId}`, - ); - return; - } +if (!listing) { +console.error( +`Listing with ID ${payload.listingId} not found for reservation request ${payload.reservationRequestId}`, +); +return; +} - // Prepare email notification data - const sharerEmail = sharer.account.email; - const reserverName = reserver.account.username || reserver.account.email; - const listingTitle = listing.title; - const reservationStart = payload.reservationPeriodStart.toLocaleDateString(); - const reservationEnd = payload.reservationPeriodEnd.toLocaleDateString(); +// Prepare email notification data +const sharerEmail = sharer.account.email; +const reserverName = reserver.account.username || reserver.account.email; +const listingTitle = listing.title; +const reservationStart = payload.reservationPeriodStart.toLocaleDateString(); +const reservationEnd = payload.reservationPeriodEnd.toLocaleDateString(); - console.log( - `Sending reservation notification email to ${sharerEmail} for listing "${listingTitle}"`, - ); +console.log( +`Sending reservation notification email to ${sharerEmail} for listing "${listingTitle}"`, +); - // TODO: Create a proper sendReservationNotification method in SendGrid service - // For now, we'll log that the email would be sent - console.log('Email would be sent with the following details:'); - console.log(` To: ${sharerEmail}`); - console.log(` Reserver: ${reserverName}`); - console.log(` Listing: ${listingTitle}`); - console.log(` Period: ${reservationStart} - ${reservationEnd}`); +// TODO: Create a proper sendReservationNotification method in SendGrid service +// For now, we'll log that the email would be sent +console.log('Email would be sent with the following details:'); +console.log(` To: ${sharerEmail}`); +console.log(` Reserver: ${reserverName}`); +console.log(` Listing: ${listingTitle}`); +console.log(` Period: ${reservationStart} - ${reservationEnd}`); - // Actual email sending would look like: - // await sendGridService.sendReservationNotification( - // sharerEmail, - // reserverName, - // listingTitle, - // reservationStart, - // reservationEnd - // ); - } catch (error) { - console.error( - 'Error in ReservationRequestCreatedNotifySharerHandler:', - error, - ); - // Don't throw - integration events should not affect the main transaction - } - }; +// Actual email sending would look like: +// await sendGridService.sendReservationNotification( +// sharerEmail, +// reserverName, +// listingTitle, +// reservationStart, +// reservationEnd +// ); +} catch (error) { +console.error( +'Error in ReservationRequestCreatedNotifySharerHandler:', +error, +); +// Don't throw - integration events should not affect the main transaction +} +}; }; export const registerReservationRequestCreatedHandler = ( - domainDataSource: DomainDataSource, + dataSourcesFactory: DataSourcesFactory, sendGridService: SendGrid, ) => { - const eventBus = domainDataSource.withSystemPassport().eventBus(); - eventBus.register( - ReservationRequestCreatedEvent, + NodeEventBusInstance.register( + Domain.ReservationRequestCreatedEvent, ReservationRequestCreatedNotifySharerHandler( - domainDataSource, + dataSourcesFactory, sendGridService, ), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fba730e4..7a312485c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -756,9 +756,18 @@ importers: packages/sthrift/event-handler: dependencies: + '@cellix/event-bus-seedwork-node': + specifier: workspace:* + version: link:../../cellix/event-bus-seedwork-node '@sthrift/domain': specifier: workspace:* version: link:../domain + '@sthrift/persistence': + specifier: workspace:* + version: link:../persistence + '@sthrift/service-sendgrid': + specifier: workspace:* + version: link:../service-sendgrid devDependencies: '@cellix/typescript-config': specifier: workspace:* From ca83cced2cb57cfbbb75cea9e3240ca354dcc7b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:14:10 +0000 Subject: [PATCH 4/5] Add SendGrid service dependency to API and fix service registration Co-authored-by: rohit-r-kumar <175348946+rohit-r-kumar@users.noreply.github.com> --- apps/api/package.json | 1 + apps/api/src/index.ts | 5 ++--- pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index f5d1586ba..c5df752fd 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -36,6 +36,7 @@ "@sthrift/service-cybersource": "workspace:*", "@sthrift/service-mongoose": "workspace:*", "@sthrift/service-otel": "workspace:*", + "@sthrift/service-sendgrid": "workspace:*", "@sthrift/service-token-validation": "workspace:*", "@sthrift/messaging-service-twilio": "workspace:*", "twilio": "^5.8.0" diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 6dd76a66b..98e78e705 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -46,8 +46,7 @@ Cellix.initializeInfrastructureServices( .registerInfrastructureService( isDevelopment ? new ServiceMessagingMock() : new ServiceMessagingTwilio(), ) - .registerInfrastructureService(new ServiceCybersource()) - .registerInfrastructureService(new SendGrid('magic-link-email')); + .registerInfrastructureService(new ServiceCybersource()); }, ) .setContext((serviceRegistry) => { @@ -61,7 +60,7 @@ Cellix.initializeInfrastructureServices( ? serviceRegistry.getInfrastructureService(ServiceMessagingMock) : serviceRegistry.getInfrastructureService(ServiceMessagingTwilio); - const sendGridService = serviceRegistry.getInfrastructureService(SendGrid); + const sendGridService = new SendGrid('magic-link-email'); RegisterEventHandlers(dataSourcesFactory, sendGridService); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a312485c..c16d73be8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,9 @@ importers: '@sthrift/service-otel': specifier: workspace:* version: link:../../packages/sthrift/service-otel + '@sthrift/service-sendgrid': + specifier: workspace:* + version: link:../../packages/sthrift/service-sendgrid '@sthrift/service-token-validation': specifier: workspace:* version: link:../../packages/sthrift/service-token-validation From d9097ad695efa738f91afcf70bf432f25eb8312d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:51:21 +0000 Subject: [PATCH 5/5] Use generic TransactionalEmailService interface instead of concrete SendGrid type Co-authored-by: rohit-r-kumar <175348946+rohit-r-kumar@users.noreply.github.com> --- .../domain/src/domain/services/index.ts | 2 + .../domain/services/transactional-email.ts | 10 ++ .../event-handler/src/handlers/index.ts | 6 +- .../src/handlers/integration/index.ts | 6 +- ...ervation-request-created--notify-sharer.ts | 161 +++++++++--------- .../sthrift/service-sendgrid/package.json | 3 +- .../sthrift/service-sendgrid/src/sendgrid.ts | 63 ++++++- pnpm-lock.yaml | 3 + 8 files changed, 161 insertions(+), 93 deletions(-) create mode 100644 packages/sthrift/domain/src/domain/services/transactional-email.ts diff --git a/packages/sthrift/domain/src/domain/services/index.ts b/packages/sthrift/domain/src/domain/services/index.ts index edce8fd61..ca6f8052c 100644 --- a/packages/sthrift/domain/src/domain/services/index.ts +++ b/packages/sthrift/domain/src/domain/services/index.ts @@ -1,5 +1,7 @@ import type { BlobStorage } from './blob-storage.ts'; +import type { TransactionalEmailService } from './transactional-email.ts'; export interface Services { BlobStorage: BlobStorage; + TransactionalEmailService: TransactionalEmailService; } \ No newline at end of file diff --git a/packages/sthrift/domain/src/domain/services/transactional-email.ts b/packages/sthrift/domain/src/domain/services/transactional-email.ts new file mode 100644 index 000000000..279a9dd79 --- /dev/null +++ b/packages/sthrift/domain/src/domain/services/transactional-email.ts @@ -0,0 +1,10 @@ + +export interface TransactionalEmailService { + sendReservationNotification( + recipientEmail: string, + reserverName: string, + listingTitle: string, + reservationStart: string, + reservationEnd: string, + ): Promise; +} diff --git a/packages/sthrift/event-handler/src/handlers/index.ts b/packages/sthrift/event-handler/src/handlers/index.ts index 4a8a07101..533ddccda 100644 --- a/packages/sthrift/event-handler/src/handlers/index.ts +++ b/packages/sthrift/event-handler/src/handlers/index.ts @@ -1,12 +1,12 @@ +import { Domain } from '@sthrift/domain'; import type { DataSourcesFactory } from '@sthrift/persistence'; -import type { SendGrid } from '@sthrift/service-sendgrid'; import { RegisterDomainEventHandlers } from "./domain/index.ts"; import { RegisterIntegrationEventHandlers } from "./integration/index.ts"; export const RegisterEventHandlers = ( dataSourcesFactory: DataSourcesFactory, - sendGridService: SendGrid + emailService: Domain.Services['TransactionalEmailService'] ) => { RegisterDomainEventHandlers(dataSourcesFactory); - RegisterIntegrationEventHandlers(dataSourcesFactory, sendGridService); + RegisterIntegrationEventHandlers(dataSourcesFactory, emailService); } \ No newline at end of file diff --git a/packages/sthrift/event-handler/src/handlers/integration/index.ts b/packages/sthrift/event-handler/src/handlers/integration/index.ts index a941feeba..53eb55106 100644 --- a/packages/sthrift/event-handler/src/handlers/integration/index.ts +++ b/packages/sthrift/event-handler/src/handlers/integration/index.ts @@ -1,10 +1,10 @@ +import { Domain } from '@sthrift/domain'; import type { DataSourcesFactory } from '@sthrift/persistence'; -import type { SendGrid } from '@sthrift/service-sendgrid'; import { registerReservationRequestCreatedHandler } from './reservation-request-created--notify-sharer.ts'; export const RegisterIntegrationEventHandlers = ( dataSourcesFactory: DataSourcesFactory, - sendGridService: SendGrid, + emailService: Domain.Services['TransactionalEmailService'], ): void => { - registerReservationRequestCreatedHandler(dataSourcesFactory, sendGridService); + registerReservationRequestCreatedHandler(dataSourcesFactory, emailService); }; diff --git a/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts b/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts index 48db57f0c..2d4b7874f 100644 --- a/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts +++ b/packages/sthrift/event-handler/src/handlers/integration/reservation-request-created--notify-sharer.ts @@ -1,113 +1,104 @@ import { Domain } from '@sthrift/domain'; import type { DataSourcesFactory } from '@sthrift/persistence'; -import type { SendGrid } from '@sthrift/service-sendgrid'; import { NodeEventBusInstance } from '@cellix/event-bus-seedwork-node'; export const ReservationRequestCreatedNotifySharerHandler = ( -dataSourcesFactory: DataSourcesFactory, -_sendGridService: SendGrid, + dataSourcesFactory: DataSourcesFactory, + emailService: Domain.Services['TransactionalEmailService'], ) => { -return async (payload: { -reservationRequestId: string; -listingId: string; -reserverId: string; -sharerId: string; -reservationPeriodStart: Date; -reservationPeriodEnd: Date; -}) => { -try { -console.log( -'ReservationRequestCreatedNotifySharerHandler: Processing event for reservation request', -payload.reservationRequestId, -); - -// Get the datasources with system passport to fetch user information -const { readonlyDataSource } = dataSourcesFactory.withSystemPassport(); + return async (payload: { + reservationRequestId: string; + listingId: string; + reserverId: string; + sharerId: string; + reservationPeriodStart: Date; + reservationPeriodEnd: Date; + }) => { + try { + console.log( + 'ReservationRequestCreatedNotifySharerHandler: Processing event for reservation request', + payload.reservationRequestId, + ); -// Fetch the sharer (listing owner) details -const sharer = await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( -payload.sharerId, -); + // Get the datasources with system passport to fetch user information + const { readonlyDataSource } = dataSourcesFactory.withSystemPassport(); -if (!sharer) { -console.error( -`Sharer with ID ${payload.sharerId} not found for reservation request ${payload.reservationRequestId}`, -); -return; -} + // Fetch the sharer (listing owner) details + const sharer = await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( + payload.sharerId, + ); -// Fetch the reserver details -const reserver = -await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( -payload.reserverId, -); + if (!sharer) { + console.error( + `Sharer with ID ${payload.sharerId} not found for reservation request ${payload.reservationRequestId}`, + ); + return; + } -if (!reserver) { -console.error( -`Reserver with ID ${payload.reserverId} not found for reservation request ${payload.reservationRequestId}`, -); -return; -} + // Fetch the reserver details + const reserver = + await readonlyDataSource.User.PersonalUser.PersonalUserReadRepo.getById( + payload.reserverId, + ); -// Fetch the listing details -const listing = -await readonlyDataSource.Listing.ItemListing.ItemListingReadRepo.getById( -payload.listingId, -); + if (!reserver) { + console.error( + `Reserver with ID ${payload.reserverId} not found for reservation request ${payload.reservationRequestId}`, + ); + return; + } -if (!listing) { -console.error( -`Listing with ID ${payload.listingId} not found for reservation request ${payload.reservationRequestId}`, -); -return; -} + // Fetch the listing details + const listing = + await readonlyDataSource.Listing.ItemListing.ItemListingReadRepo.getById( + payload.listingId, + ); -// Prepare email notification data -const sharerEmail = sharer.account.email; -const reserverName = reserver.account.username || reserver.account.email; -const listingTitle = listing.title; -const reservationStart = payload.reservationPeriodStart.toLocaleDateString(); -const reservationEnd = payload.reservationPeriodEnd.toLocaleDateString(); + if (!listing) { + console.error( + `Listing with ID ${payload.listingId} not found for reservation request ${payload.reservationRequestId}`, + ); + return; + } -console.log( -`Sending reservation notification email to ${sharerEmail} for listing "${listingTitle}"`, -); + // Prepare email notification data + const sharerEmail = sharer.account.email; + const reserverName = reserver.account.username || reserver.account.email; + const listingTitle = listing.title; + const reservationStart = payload.reservationPeriodStart.toLocaleDateString(); + const reservationEnd = payload.reservationPeriodEnd.toLocaleDateString(); -// TODO: Create a proper sendReservationNotification method in SendGrid service -// For now, we'll log that the email would be sent -console.log('Email would be sent with the following details:'); -console.log(` To: ${sharerEmail}`); -console.log(` Reserver: ${reserverName}`); -console.log(` Listing: ${listingTitle}`); -console.log(` Period: ${reservationStart} - ${reservationEnd}`); + console.log( + `Sending reservation notification email to ${sharerEmail} for listing "${listingTitle}"`, + ); -// Actual email sending would look like: -// await sendGridService.sendReservationNotification( -// sharerEmail, -// reserverName, -// listingTitle, -// reservationStart, -// reservationEnd -// ); -} catch (error) { -console.error( -'Error in ReservationRequestCreatedNotifySharerHandler:', -error, -); -// Don't throw - integration events should not affect the main transaction -} -}; + // Call the email service to send the notification + await emailService.sendReservationNotification( + sharerEmail, + reserverName, + listingTitle, + reservationStart, + reservationEnd, + ); + } catch (error) { + console.error( + 'Error in ReservationRequestCreatedNotifySharerHandler:', + error, + ); + // Don't throw - integration events should not affect the main transaction + } + }; }; export const registerReservationRequestCreatedHandler = ( dataSourcesFactory: DataSourcesFactory, - sendGridService: SendGrid, + emailService: Domain.Services['TransactionalEmailService'], ) => { NodeEventBusInstance.register( Domain.ReservationRequestCreatedEvent, ReservationRequestCreatedNotifySharerHandler( dataSourcesFactory, - sendGridService, + emailService, ), ); }; diff --git a/packages/sthrift/service-sendgrid/package.json b/packages/sthrift/service-sendgrid/package.json index 87c5e57d4..7fde2be93 100644 --- a/packages/sthrift/service-sendgrid/package.json +++ b/packages/sthrift/service-sendgrid/package.json @@ -30,6 +30,7 @@ "@cellix/typescript-config": "workspace:*" }, "dependencies": { - "@sendgrid/mail": "^8.0.0" + "@sendgrid/mail": "^8.0.0", + "@sthrift/domain": "workspace:*" } } \ No newline at end of file diff --git a/packages/sthrift/service-sendgrid/src/sendgrid.ts b/packages/sthrift/service-sendgrid/src/sendgrid.ts index fde349972..288875c60 100644 --- a/packages/sthrift/service-sendgrid/src/sendgrid.ts +++ b/packages/sthrift/service-sendgrid/src/sendgrid.ts @@ -3,7 +3,18 @@ import { readHtmlFile } from './get-email-template.js'; import fs from 'fs'; import path from 'path'; -export default class SendGrid { +// TransactionalEmailService interface definition +interface TransactionalEmailService { + sendReservationNotification( + recipientEmail: string, + reserverName: string, + listingTitle: string, + reservationStart: string, + reservationEnd: string, + ): Promise; +} + +export default class SendGrid implements TransactionalEmailService { emailTemplateName: string; constructor(emailTemplateName: string) { @@ -15,6 +26,56 @@ export default class SendGrid { this.emailTemplateName = emailTemplateName; } + async sendReservationNotification( + recipientEmail: string, + reserverName: string, + listingTitle: string, + reservationStart: string, + reservationEnd: string, + ): Promise { + console.log('SendGrid.sendReservationNotification()'); + console.log(` To: ${recipientEmail}`); + console.log(` Reserver: ${reserverName}`); + console.log(` Listing: ${listingTitle}`); + console.log(` Period: ${reservationStart} - ${reservationEnd}`); + + if (process.env["NODE_ENV"] === 'development') { + const outDir = path.join(process.cwd(), 'tmp-emails'); + if (!fs.existsSync(outDir)) fs.mkdirSync(outDir); + + const sanitizedEmail = recipientEmail.replace(/[@/\\:*?"<>|]/g, '_'); + const outFile = path.join(outDir, `reservation_${sanitizedEmail}_${Date.now()}.txt`); + const content = ` +Reservation Notification +======================== +To: ${recipientEmail} +Reserver: ${reserverName} +Listing: ${listingTitle} +Start Date: ${reservationStart} +End Date: ${reservationEnd} +`; + fs.writeFileSync(outFile, content, 'utf-8'); + console.log(`Email saved to ${outFile}`); + return; + } + + // TODO: Implement actual email sending with template + // For production, this should use a proper email template + try { + await sendgrid.send({ + to: recipientEmail, + from: process.env['SENDGRID_FROM_EMAIL'] || 'noreply@sharethrift.com', + subject: `New reservation request for ${listingTitle}`, + text: `${reserverName} has requested to reserve ${listingTitle} from ${reservationStart} to ${reservationEnd}.`, + html: `

${reserverName} has requested to reserve ${listingTitle} from ${reservationStart} to ${reservationEnd}.

`, + }); + console.log('Reservation notification email sent successfully'); + } catch (error) { + console.error('Error sending reservation notification email:', error); + throw error; + } + } + sendEmailWithMagicLink = async (userEmail: string, magicLink: string) => { console.log('SendGrid.sendEmail() - email: ', userEmail); let template: { fromEmail: string; subject: string; body: string }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c16d73be8..555553d60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1158,6 +1158,9 @@ importers: '@sendgrid/mail': specifier: ^8.0.0 version: 8.1.6 + '@sthrift/domain': + specifier: workspace:* + version: link:../domain react: specifier: '>=17.0.0' version: 19.2.0