xpeditis2.0/apps/backend/src/application/services/booking-automation.service.ts
David 2c2b7b2a11
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
fix: convert TypeScript path aliases to relative imports for CI/CD compatibility
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>
2025-11-16 02:59:52 +01:00

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);
}
}
}