From 74b52751d2f98494467de9b0872b99f168284a01 Mon Sep 17 00:00:00 2001 From: abdulmujibOladayo Date: Thu, 26 Mar 2026 15:29:44 +0100 Subject: [PATCH] notifications service --- .../notifications/notifications.service.ts | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 backend/src/notifications/notifications.service.ts diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts new file mode 100644 index 00000000..0af1c356 --- /dev/null +++ b/backend/src/notifications/notifications.service.ts @@ -0,0 +1,306 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { MailerService } from '@nestjs-modules/mailer'; +import { + SHIPMENT_ACCEPTED, + SHIPMENT_IN_TRANSIT, + SHIPMENT_DELIVERED, + SHIPMENT_COMPLETED, + SHIPMENT_CANCELLED, + SHIPMENT_DISPUTED, + SHIPMENT_DISPUTE_RESOLVED, + ShipmentEvent, +} from '../shipments/events/shipment.events'; +import { Shipment } from '../shipments/entities/shipment.entity'; + +@Injectable() +export class NotificationsService { + private readonly logger = new Logger(NotificationsService.name); + + constructor(private readonly mailerService: MailerService) {} + + // ── Helpers ───────────────────────────────────────────────────────────────── + + private async sendSafe( + to: string, + subject: string, + html: string, + ): Promise { + try { + await this.mailerService.sendMail({ to, subject, html }); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + this.logger.warn(`Failed to send email to ${to}: ${msg}`); + } + } + + private shipmentSummary(s: Shipment): string { + return ` + + + + + + +
Tracking #${s.trackingNumber}
Route${s.origin} → ${s.destination}
Cargo${s.cargoDescription}
Weight${s.weightKg} kg
Value${s.currency} ${Number(s.price).toLocaleString()}
+ `; + } + + private baseTemplate(title: string, body: string): string { + return ` +
+

${title}

+ ${body} +
+

FreightFlow — Decentralized Freight Management

+
+ `; + } + + // ── Event Handlers ─────────────────────────────────────────────────────────── + + @OnEvent(SHIPMENT_ACCEPTED) + async onShipmentAccepted({ shipment }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + if (!shipper || !carrier) return; + + // Notify shipper: a carrier accepted their shipment + await this.sendSafe( + shipper.email, + `✅ Carrier found for your shipment ${shipment.trackingNumber}`, + this.baseTemplate( + 'Your shipment has been accepted!', + ` +

Hi ${shipper.firstName},

+

Great news! ${carrier.firstName} ${carrier.lastName} has accepted your shipment and will be handling the delivery.

+ ${this.shipmentSummary(shipment)} +

You will receive another update when your cargo is picked up.

+ `, + ), + ); + + // Notify carrier: confirm they accepted + await this.sendSafe( + carrier.email, + `📦 You accepted shipment ${shipment.trackingNumber}`, + this.baseTemplate( + 'Shipment accepted — pickup next', + ` +

Hi ${carrier.firstName},

+

You have successfully accepted a shipment. Please proceed to pick up the cargo.

+ ${this.shipmentSummary(shipment)} +

Pickup location: ${shipment.origin}

+ `, + ), + ); + } + + @OnEvent(SHIPMENT_IN_TRANSIT) + async onShipmentInTransit({ shipment }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + if (!shipper || !carrier) return; + + await this.sendSafe( + shipper.email, + `🚚 Your shipment ${shipment.trackingNumber} is on the way`, + this.baseTemplate( + 'Shipment picked up — in transit', + ` +

Hi ${shipper.firstName},

+

Your cargo has been picked up by ${carrier.firstName} ${carrier.lastName} and is now in transit.

+ ${this.shipmentSummary(shipment)} + ${shipment.estimatedDeliveryDate ? `

Estimated delivery: ${new Date(shipment.estimatedDeliveryDate).toDateString()}

` : ''} + `, + ), + ); + } + + @OnEvent(SHIPMENT_DELIVERED) + async onShipmentDelivered({ shipment }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + if (!shipper || !carrier) return; + + // Notify shipper: please confirm delivery + await this.sendSafe( + shipper.email, + `📬 Your shipment ${shipment.trackingNumber} has been delivered — action required`, + this.baseTemplate( + 'Delivery reported — please confirm', + ` +

Hi ${shipper.firstName},

+

${carrier.firstName} ${carrier.lastName} has marked your shipment as delivered on ${new Date().toDateString()}.

+ ${this.shipmentSummary(shipment)} +

Please log in to FreightFlow to confirm delivery and complete the transaction, or raise a dispute if there is an issue.

+ `, + ), + ); + + // Notify carrier: delivery marked, waiting for shipper confirmation + await this.sendSafe( + carrier.email, + `✔️ Delivery marked for ${shipment.trackingNumber} — awaiting confirmation`, + this.baseTemplate( + 'Delivery marked — awaiting shipper confirmation', + ` +

Hi ${carrier.firstName},

+

You have successfully marked shipment ${shipment.trackingNumber} as delivered. The shipper has been notified and will confirm receipt shortly.

+ ${this.shipmentSummary(shipment)} + `, + ), + ); + } + + @OnEvent(SHIPMENT_COMPLETED) + async onShipmentCompleted({ shipment }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + if (!shipper || !carrier) return; + + // Notify carrier: delivery confirmed, job done + await this.sendSafe( + carrier.email, + `🎉 Shipment ${shipment.trackingNumber} completed — delivery confirmed`, + this.baseTemplate( + 'Job complete — delivery confirmed by shipper', + ` +

Hi ${carrier.firstName},

+

The shipper has confirmed receipt of shipment ${shipment.trackingNumber}. This job is now complete.

+ ${this.shipmentSummary(shipment)} +

Thank you for your service on FreightFlow!

+ `, + ), + ); + + // Notify shipper: transaction complete + await this.sendSafe( + shipper.email, + `✅ Shipment ${shipment.trackingNumber} completed successfully`, + this.baseTemplate( + 'Shipment completed', + ` +

Hi ${shipper.firstName},

+

Your shipment ${shipment.trackingNumber} has been completed successfully. Thank you for using FreightFlow!

+ ${this.shipmentSummary(shipment)} + `, + ), + ); + } + + @OnEvent(SHIPMENT_CANCELLED) + async onShipmentCancelled({ + shipment, + reason, + }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + const reasonNote = reason + ? `

Reason: ${reason}

` + : ''; + + if (shipper) { + await this.sendSafe( + shipper.email, + `❌ Shipment ${shipment.trackingNumber} has been cancelled`, + this.baseTemplate( + 'Shipment cancelled', + ` +

Hi ${shipper.firstName},

+

Shipment ${shipment.trackingNumber} has been cancelled.

+ ${reasonNote} + ${this.shipmentSummary(shipment)} + `, + ), + ); + } + + if (carrier) { + await this.sendSafe( + carrier.email, + `❌ Shipment ${shipment.trackingNumber} has been cancelled`, + this.baseTemplate( + 'Shipment cancelled', + ` +

Hi ${carrier.firstName},

+

Shipment ${shipment.trackingNumber} that you were assigned to has been cancelled.

+ ${reasonNote} + ${this.shipmentSummary(shipment)} + `, + ), + ); + } + } + + @OnEvent(SHIPMENT_DISPUTED) + async onShipmentDisputed({ shipment, reason }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + const reasonNote = reason + ? `

Reason for dispute: ${reason}

` + : ''; + + if (shipper) { + await this.sendSafe( + shipper.email, + `⚠️ Dispute raised on shipment ${shipment.trackingNumber}`, + this.baseTemplate( + 'A dispute has been raised', + ` +

Hi ${shipper.firstName},

+

A dispute has been raised on shipment ${shipment.trackingNumber}. Our team will review and resolve it.

+ ${reasonNote} + ${this.shipmentSummary(shipment)} + `, + ), + ); + } + + if (carrier) { + await this.sendSafe( + carrier.email, + `⚠️ Dispute raised on shipment ${shipment.trackingNumber}`, + this.baseTemplate( + 'A dispute has been raised', + ` +

Hi ${carrier.firstName},

+

A dispute has been raised on shipment ${shipment.trackingNumber}. Our team will review and resolve it.

+ ${reasonNote} + ${this.shipmentSummary(shipment)} + `, + ), + ); + } + } + + @OnEvent(SHIPMENT_DISPUTE_RESOLVED) + async onDisputeResolved({ shipment, reason }: ShipmentEvent): Promise { + const { shipper, carrier } = shipment; + const outcome = shipment.status.toUpperCase(); + const reasonNote = reason + ? `

Resolution note: ${reason}

` + : ''; + + const body = (firstName: string) => + this.baseTemplate( + `Dispute resolved — ${outcome}`, + ` +

Hi ${firstName},

+

The dispute on shipment ${shipment.trackingNumber} has been reviewed and resolved by our admin team.

+

Outcome: ${outcome}

+ ${reasonNote} + ${this.shipmentSummary(shipment)} + `, + ); + + if (shipper) { + await this.sendSafe( + shipper.email, + `🔔 Dispute resolved for shipment ${shipment.trackingNumber}`, + body(shipper.firstName), + ); + } + if (carrier) { + await this.sendSafe( + carrier.email, + `🔔 Dispute resolved for shipment ${shipment.trackingNumber}`, + body(carrier.firstName), + ); + } + } +}