Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m50s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Successful in 10m56s
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Has been cancelled
Replace all @domain/ports/out/* imports with relative paths to fix TypeScript compilation errors in CI/CD environment. The issue was that TypeScript compiler (tsc) used by nest build doesn't resolve path aliases by default. While tsconfig-paths works at runtime and in development, it doesn't help during compilation. Changes: - Convert @domain/ports/out/* to relative paths (../../domain/ports/out/, etc.) - Remove tsc-alias dependency (no longer needed) - Revert build script to "nest build" only This ensures the build works consistently in both local and CI/CD environments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|
|
}
|