/** * Booking Automation Service * * Handles post-booking automation (emails, PDFs, storage) */ import { Injectable, Logger, Inject } from '@nestjs/common'; import { Booking } from '../../domain/entities/booking.entity'; import { EmailPort, EMAIL_PORT } from '../../domain/ports/out/email.port'; import { PdfPort, PDF_PORT, BookingPdfData } from '../../domain/ports/out/pdf.port'; import { StoragePort, STORAGE_PORT } from '../../domain/ports/out/storage.port'; import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository'; import { RateQuoteRepository, RATE_QUOTE_REPOSITORY, } from '../../domain/ports/out/rate-quote.repository'; @Injectable() export class BookingAutomationService { private readonly logger = new Logger(BookingAutomationService.name); constructor( @Inject(EMAIL_PORT) private readonly emailPort: EmailPort, @Inject(PDF_PORT) private readonly pdfPort: PdfPort, @Inject(STORAGE_PORT) private readonly storagePort: StoragePort, @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, @Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository ) {} /** * Execute all post-booking automation tasks */ async executePostBookingTasks(booking: Booking): Promise { this.logger.log(`Starting post-booking automation for booking: ${booking.bookingNumber.value}`); try { // Get user and rate quote details const user = await this.userRepository.findById(booking.userId); if (!user) { throw new Error(`User not found: ${booking.userId}`); } const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); if (!rateQuote) { throw new Error(`Rate quote not found: ${booking.rateQuoteId}`); } // Generate booking confirmation PDF const pdfData: BookingPdfData = { bookingNumber: booking.bookingNumber.value, bookingDate: booking.createdAt, origin: { code: rateQuote.origin.code, name: rateQuote.origin.name, }, destination: { code: rateQuote.destination.code, name: rateQuote.destination.name, }, carrier: { name: rateQuote.carrierName, logo: undefined, // TODO: Add carrierLogoUrl to RateQuote entity }, shipper: { name: booking.shipper.name, address: this.formatAddress(booking.shipper.address), contact: booking.shipper.contactName, email: booking.shipper.contactEmail, phone: booking.shipper.contactPhone, }, consignee: { name: booking.consignee.name, address: this.formatAddress(booking.consignee.address), contact: booking.consignee.contactName, email: booking.consignee.contactEmail, phone: booking.consignee.contactPhone, }, containers: booking.containers.map(c => ({ type: c.type, quantity: 1, containerNumber: c.containerNumber, sealNumber: c.sealNumber, })), cargoDescription: booking.cargoDescription, specialInstructions: booking.specialInstructions, etd: rateQuote.etd, eta: rateQuote.eta, transitDays: rateQuote.transitDays, price: { amount: rateQuote.pricing.totalAmount, currency: rateQuote.pricing.currency, }, }; const pdfBuffer = await this.pdfPort.generateBookingConfirmation(pdfData); // Store PDF in S3 const storageKey = `bookings/${booking.id}/${booking.bookingNumber.value}.pdf`; await this.storagePort.upload({ bucket: 'xpeditis-bookings', key: storageKey, body: pdfBuffer, contentType: 'application/pdf', metadata: { bookingId: booking.id, bookingNumber: booking.bookingNumber.value, userId: user.id, }, }); this.logger.log( `Stored booking PDF: ${storageKey} for booking ${booking.bookingNumber.value}` ); // Send confirmation email with PDF attachment await this.emailPort.sendBookingConfirmation( user.email, booking.bookingNumber.value, { origin: rateQuote.origin.name, destination: rateQuote.destination.name, carrier: rateQuote.carrierName, etd: rateQuote.etd, eta: rateQuote.eta, }, pdfBuffer ); this.logger.log( `Post-booking automation completed successfully for booking: ${booking.bookingNumber.value}` ); } catch (error) { this.logger.error( `Post-booking automation failed for booking: ${booking.bookingNumber.value}`, error ); // Don't throw - we don't want to fail the booking creation if email/PDF fails // TODO: Implement retry mechanism with queue (Bull/BullMQ) } } /** * Format address for PDF */ private formatAddress(address: { street: string; city: string; postalCode: string; country: string; }): string { return `${address.street}, ${address.city}, ${address.postalCode}, ${address.country}`; } /** * Send booking update notification */ async sendBookingUpdateNotification( booking: Booking, updateType: 'confirmed' | 'delayed' | 'arrived' ): Promise { try { const user = await this.userRepository.findById(booking.userId); if (!user) { throw new Error(`User not found: ${booking.userId}`); } // TODO: Send update email based on updateType this.logger.log( `Sent ${updateType} notification for booking: ${booking.bookingNumber.value}` ); } catch (error) { this.logger.error(`Failed to send booking update notification`, error); } } }