xpeditis2.0/apps/backend/src/application/services/export.service.ts
David 4b00ee2601
Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m53s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Has been cancelled
fix: replace relative domain imports with TypeScript path aliases
- Replace all ../../domain/ imports with @domain/ across 67 files
- Configure NestJS to use tsconfig.build.json with rootDir
- Add tsc-alias to resolve path aliases after build
- This fixes 'Cannot find module' TypeScript compilation errors

Fixed files:
- 30 files in application layer
- 37 files in infrastructure layer
2025-11-16 19:20:58 +01:00

259 lines
7.7 KiB
TypeScript

/**
* Export Service
*
* Handles booking data export to various formats (CSV, Excel, JSON)
*/
import { Injectable, Logger } from '@nestjs/common';
import { Booking } from '@domain/entities/booking.entity';
import { RateQuote } from '@domain/entities/rate-quote.entity';
import { ExportFormat, ExportField } from '../dto/booking-export.dto';
import * as ExcelJS from 'exceljs';
interface BookingExportData {
booking: Booking;
rateQuote: RateQuote;
}
@Injectable()
export class ExportService {
private readonly logger = new Logger(ExportService.name);
/**
* Export bookings to specified format
*/
async exportBookings(
data: BookingExportData[],
format: ExportFormat,
fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
this.logger.log(
`Exporting ${data.length} bookings to ${format} format with ${fields?.length || 'all'} fields`
);
switch (format) {
case ExportFormat.CSV:
return this.exportToCSV(data, fields);
case ExportFormat.EXCEL:
return this.exportToExcel(data, fields);
case ExportFormat.JSON:
return this.exportToJSON(data, fields);
default:
throw new Error(`Unsupported export format: ${format}`);
}
}
/**
* Export to CSV format
*/
private async exportToCSV(
data: BookingExportData[],
fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
const selectedFields = fields || Object.values(ExportField);
const rows = data.map(item => this.extractFields(item, selectedFields));
// Build CSV header
const header = selectedFields.map(field => this.getFieldLabel(field)).join(',');
// Build CSV rows
const csvRows = rows.map(row =>
selectedFields.map(field => this.escapeCSVValue(row[field] || '')).join(',')
);
const csv = [header, ...csvRows].join('\n');
const buffer = Buffer.from(csv, 'utf-8');
const timestamp = new Date().toISOString().split('T')[0];
const filename = `bookings_export_${timestamp}.csv`;
return {
buffer,
contentType: 'text/csv',
filename,
};
}
/**
* Export to Excel format
*/
private async exportToExcel(
data: BookingExportData[],
fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
const selectedFields = fields || Object.values(ExportField);
const rows = data.map(item => this.extractFields(item, selectedFields));
const workbook = new ExcelJS.Workbook();
workbook.creator = 'Xpeditis';
workbook.created = new Date();
const worksheet = workbook.addWorksheet('Bookings');
// Add header row with styling
const headerRow = worksheet.addRow(selectedFields.map(field => this.getFieldLabel(field)));
headerRow.font = { bold: true };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFE0E0E0' },
};
// Add data rows
rows.forEach(row => {
const values = selectedFields.map(field => row[field] || '');
worksheet.addRow(values);
});
// Auto-fit columns
worksheet.columns.forEach(column => {
let maxLength = 10;
column.eachCell?.({ includeEmpty: false }, cell => {
const columnLength = cell.value ? String(cell.value).length : 10;
if (columnLength > maxLength) {
maxLength = columnLength;
}
});
column.width = Math.min(maxLength + 2, 50);
});
const buffer = await workbook.xlsx.writeBuffer();
const timestamp = new Date().toISOString().split('T')[0];
const filename = `bookings_export_${timestamp}.xlsx`;
return {
buffer: Buffer.from(buffer),
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
filename,
};
}
/**
* Export to JSON format
*/
private async exportToJSON(
data: BookingExportData[],
fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
const selectedFields = fields || Object.values(ExportField);
const rows = data.map(item => this.extractFields(item, selectedFields));
const json = JSON.stringify(
{
exportedAt: new Date().toISOString(),
totalBookings: rows.length,
bookings: rows,
},
null,
2
);
const buffer = Buffer.from(json, 'utf-8');
const timestamp = new Date().toISOString().split('T')[0];
const filename = `bookings_export_${timestamp}.json`;
return {
buffer,
contentType: 'application/json',
filename,
};
}
/**
* Extract specified fields from booking data
*/
private extractFields(data: BookingExportData, fields: ExportField[]): Record<string, any> {
const { booking, rateQuote } = data;
const result: Record<string, any> = {};
fields.forEach(field => {
switch (field) {
case ExportField.BOOKING_NUMBER:
result[field] = booking.bookingNumber.value;
break;
case ExportField.STATUS:
result[field] = booking.status.value;
break;
case ExportField.CREATED_AT:
result[field] = booking.createdAt.toISOString();
break;
case ExportField.CARRIER:
result[field] = rateQuote.carrierName;
break;
case ExportField.ORIGIN:
result[field] = `${rateQuote.origin.name} (${rateQuote.origin.code})`;
break;
case ExportField.DESTINATION:
result[field] = `${rateQuote.destination.name} (${rateQuote.destination.code})`;
break;
case ExportField.ETD:
result[field] = rateQuote.etd.toISOString();
break;
case ExportField.ETA:
result[field] = rateQuote.eta.toISOString();
break;
case ExportField.SHIPPER:
result[field] = booking.shipper.name;
break;
case ExportField.CONSIGNEE:
result[field] = booking.consignee.name;
break;
case ExportField.CONTAINER_TYPE:
result[field] = booking.containers.map(c => c.type).join(', ');
break;
case ExportField.CONTAINER_COUNT:
result[field] = booking.containers.length;
break;
case ExportField.TOTAL_TEUS:
result[field] = booking.containers.reduce((total, c) => {
return total + (c.type.startsWith('20') ? 1 : 2);
}, 0);
break;
case ExportField.PRICE:
result[field] = `${rateQuote.pricing.currency} ${rateQuote.pricing.totalAmount.toFixed(
2
)}`;
break;
}
});
return result;
}
/**
* Get human-readable field label
*/
private getFieldLabel(field: ExportField): string {
const labels: Record<ExportField, string> = {
[ExportField.BOOKING_NUMBER]: 'Booking Number',
[ExportField.STATUS]: 'Status',
[ExportField.CREATED_AT]: 'Created At',
[ExportField.CARRIER]: 'Carrier',
[ExportField.ORIGIN]: 'Origin',
[ExportField.DESTINATION]: 'Destination',
[ExportField.ETD]: 'ETD',
[ExportField.ETA]: 'ETA',
[ExportField.SHIPPER]: 'Shipper',
[ExportField.CONSIGNEE]: 'Consignee',
[ExportField.CONTAINER_TYPE]: 'Container Type',
[ExportField.CONTAINER_COUNT]: 'Container Count',
[ExportField.TOTAL_TEUS]: 'Total TEUs',
[ExportField.PRICE]: 'Price',
};
return labels[field];
}
/**
* Escape CSV value (handle commas, quotes, newlines)
*/
private escapeCSVValue(value: string): string {
const stringValue = String(value);
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}
}