format prettier

This commit is contained in:
David 2025-10-27 20:54:01 +01:00
parent 07b08e3014
commit d809feecef
166 changed files with 13053 additions and 13332 deletions

View File

@ -31,9 +31,7 @@ import { CustomThrottlerGuard } from './application/guards/throttle.guard';
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
validationSchema: Joi.object({ validationSchema: Joi.object({
NODE_ENV: Joi.string() NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(4000), PORT: Joi.number().default(4000),
DATABASE_HOST: Joi.string().required(), DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().default(5432), DATABASE_PORT: Joi.number().default(5432),

View File

@ -1,4 +1,10 @@
import { Injectable, UnauthorizedException, ConflictException, Logger, Inject } from '@nestjs/common'; import {
Injectable,
UnauthorizedException,
ConflictException,
Logger,
Inject,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
@ -22,7 +28,7 @@ export class AuthService {
@Inject(USER_REPOSITORY) @Inject(USER_REPOSITORY)
private readonly userRepository: UserRepository, // ✅ Correct injection private readonly userRepository: UserRepository, // ✅ Correct injection
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly configService: ConfigService, private readonly configService: ConfigService
) {} ) {}
/** /**
@ -33,7 +39,7 @@ export class AuthService {
password: string, password: string,
firstName: string, firstName: string,
lastName: string, lastName: string,
organizationId?: string, organizationId?: string
): Promise<{ accessToken: string; refreshToken: string; user: any }> { ): Promise<{ accessToken: string; refreshToken: string; user: any }> {
this.logger.log(`Registering new user: ${email}`); this.logger.log(`Registering new user: ${email}`);
@ -87,7 +93,7 @@ export class AuthService {
*/ */
async login( async login(
email: string, email: string,
password: string, password: string
): Promise<{ accessToken: string; refreshToken: string; user: any }> { ): Promise<{ accessToken: string; refreshToken: string; user: any }> {
this.logger.log(`Login attempt for: ${email}`); this.logger.log(`Login attempt for: ${email}`);
@ -127,7 +133,9 @@ export class AuthService {
/** /**
* Refresh access token using refresh token * Refresh access token using refresh token
*/ */
async refreshAccessToken(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> { async refreshAccessToken(
refreshToken: string
): Promise<{ accessToken: string; refreshToken: string }> {
try { try {
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, { const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
secret: this.configService.get('JWT_SECRET'), secret: this.configService.get('JWT_SECRET'),

View File

@ -32,7 +32,7 @@ export interface JwtPayload {
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly authService: AuthService, private readonly authService: AuthService
) { ) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

View File

@ -55,7 +55,7 @@ export class CsvRatesAdminController {
constructor( constructor(
private readonly csvLoader: CsvRateLoaderAdapter, private readonly csvLoader: CsvRateLoaderAdapter,
private readonly csvConfigRepository: TypeOrmCsvRateConfigRepository, private readonly csvConfigRepository: TypeOrmCsvRateConfigRepository,
private readonly csvRateMapper: CsvRateMapper, private readonly csvRateMapper: CsvRateMapper
) {} ) {}
/** /**
@ -88,7 +88,7 @@ export class CsvRatesAdminController {
limits: { limits: {
fileSize: 10 * 1024 * 1024, // 10MB max fileSize: 10 * 1024 * 1024, // 10MB max
}, },
}), })
) )
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiOperation({ @ApiOperation({
@ -130,11 +130,9 @@ export class CsvRatesAdminController {
async uploadCsv( async uploadCsv(
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
@Body() dto: CsvRateUploadDto, @Body() dto: CsvRateUploadDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<CsvRateUploadResponseDto> { ): Promise<CsvRateUploadResponseDto> {
this.logger.log( this.logger.log(`[Admin: ${user.email}] Uploading CSV for company: ${dto.companyName}`);
`[Admin: ${user.email}] Uploading CSV for company: ${dto.companyName}`,
);
if (!file) { if (!file) {
throw new BadRequestException('File is required'); throw new BadRequestException('File is required');
@ -146,7 +144,7 @@ export class CsvRatesAdminController {
if (!validation.valid) { if (!validation.valid) {
this.logger.error( this.logger.error(
`CSV validation failed for ${dto.companyName}: ${validation.errors.join(', ')}`, `CSV validation failed for ${dto.companyName}: ${validation.errors.join(', ')}`
); );
throw new BadRequestException({ throw new BadRequestException({
message: 'CSV validation failed', message: 'CSV validation failed',
@ -158,14 +156,10 @@ export class CsvRatesAdminController {
const rates = await this.csvLoader.loadRatesFromCsv(file.filename); const rates = await this.csvLoader.loadRatesFromCsv(file.filename);
const ratesCount = rates.length; const ratesCount = rates.length;
this.logger.log( this.logger.log(`Successfully parsed ${ratesCount} rates from ${file.filename}`);
`Successfully parsed ${ratesCount} rates from ${file.filename}`,
);
// Check if config exists for this company // Check if config exists for this company
const existingConfig = await this.csvConfigRepository.findByCompanyName( const existingConfig = await this.csvConfigRepository.findByCompanyName(dto.companyName);
dto.companyName,
);
if (existingConfig) { if (existingConfig) {
// Update existing configuration // Update existing configuration
@ -185,9 +179,7 @@ export class CsvRatesAdminController {
}, },
}); });
this.logger.log( this.logger.log(`Updated CSV config for company: ${dto.companyName}`);
`Updated CSV config for company: ${dto.companyName}`,
);
} else { } else {
// Create new configuration // Create new configuration
await this.csvConfigRepository.create({ await this.csvConfigRepository.create({
@ -207,9 +199,7 @@ export class CsvRatesAdminController {
}, },
}); });
this.logger.log( this.logger.log(`Created new CSV config for company: ${dto.companyName}`);
`Created new CSV config for company: ${dto.companyName}`,
);
} }
return { return {
@ -220,10 +210,7 @@ export class CsvRatesAdminController {
uploadedAt: new Date(), uploadedAt: new Date(),
}; };
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(`CSV upload failed: ${error?.message || 'Unknown error'}`, error?.stack);
`CSV upload failed: ${error?.message || 'Unknown error'}`,
error?.stack,
);
throw error; throw error;
} }
} }
@ -267,17 +254,13 @@ export class CsvRatesAdminController {
status: 404, status: 404,
description: 'Company configuration not found', description: 'Company configuration not found',
}) })
async getConfigByCompany( async getConfigByCompany(@Param('companyName') companyName: string): Promise<CsvRateConfigDto> {
@Param('companyName') companyName: string,
): Promise<CsvRateConfigDto> {
this.logger.log(`Fetching CSV config for company: ${companyName}`); this.logger.log(`Fetching CSV config for company: ${companyName}`);
const config = await this.csvConfigRepository.findByCompanyName(companyName); const config = await this.csvConfigRepository.findByCompanyName(companyName);
if (!config) { if (!config) {
throw new BadRequestException( throw new BadRequestException(`No CSV configuration found for company: ${companyName}`);
`No CSV configuration found for company: ${companyName}`,
);
} }
return this.csvRateMapper.mapConfigEntityToDto(config); return this.csvRateMapper.mapConfigEntityToDto(config);
@ -298,28 +281,20 @@ export class CsvRatesAdminController {
description: 'Validation result', description: 'Validation result',
type: CsvFileValidationDto, type: CsvFileValidationDto,
}) })
async validateCsvFile( async validateCsvFile(@Param('companyName') companyName: string): Promise<CsvFileValidationDto> {
@Param('companyName') companyName: string,
): Promise<CsvFileValidationDto> {
this.logger.log(`Validating CSV file for company: ${companyName}`); this.logger.log(`Validating CSV file for company: ${companyName}`);
const config = await this.csvConfigRepository.findByCompanyName(companyName); const config = await this.csvConfigRepository.findByCompanyName(companyName);
if (!config) { if (!config) {
throw new BadRequestException( throw new BadRequestException(`No CSV configuration found for company: ${companyName}`);
`No CSV configuration found for company: ${companyName}`,
);
} }
const result = await this.csvLoader.validateCsvFile(config.csvFilePath); const result = await this.csvLoader.validateCsvFile(config.csvFilePath);
// Update validation timestamp // Update validation timestamp
if (result.valid && result.rowCount) { if (result.valid && result.rowCount) {
await this.csvConfigRepository.updateValidationInfo( await this.csvConfigRepository.updateValidationInfo(companyName, result.rowCount, result);
companyName,
result.rowCount,
result,
);
} }
return result; return result;
@ -345,11 +320,9 @@ export class CsvRatesAdminController {
}) })
async deleteConfig( async deleteConfig(
@Param('companyName') companyName: string, @Param('companyName') companyName: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<void> { ): Promise<void> {
this.logger.warn( this.logger.warn(`[Admin: ${user.email}] Deleting CSV config for company: ${companyName}`);
`[Admin: ${user.email}] Deleting CSV config for company: ${companyName}`,
);
await this.csvConfigRepository.delete(companyName); await this.csvConfigRepository.delete(companyName);

View File

@ -66,8 +66,18 @@ export class AuditController {
@ApiOperation({ summary: 'Get audit logs with filters' }) @ApiOperation({ summary: 'Get audit logs with filters' })
@ApiResponse({ status: 200, description: 'Audit logs retrieved successfully' }) @ApiResponse({ status: 200, description: 'Audit logs retrieved successfully' })
@ApiQuery({ name: 'userId', required: false, description: 'Filter by user ID' }) @ApiQuery({ name: 'userId', required: false, description: 'Filter by user ID' })
@ApiQuery({ name: 'action', required: false, description: 'Filter by action (comma-separated)', isArray: true }) @ApiQuery({
@ApiQuery({ name: 'status', required: false, description: 'Filter by status (comma-separated)', isArray: true }) name: 'action',
required: false,
description: 'Filter by action (comma-separated)',
isArray: true,
})
@ApiQuery({
name: 'status',
required: false,
description: 'Filter by status (comma-separated)',
isArray: true,
})
@ApiQuery({ name: 'resourceType', required: false, description: 'Filter by resource type' }) @ApiQuery({ name: 'resourceType', required: false, description: 'Filter by resource type' })
@ApiQuery({ name: 'resourceId', required: false, description: 'Filter by resource ID' }) @ApiQuery({ name: 'resourceId', required: false, description: 'Filter by resource ID' })
@ApiQuery({ name: 'startDate', required: false, description: 'Filter by start date (ISO 8601)' }) @ApiQuery({ name: 'startDate', required: false, description: 'Filter by start date (ISO 8601)' })
@ -84,7 +94,7 @@ export class AuditController {
@Query('startDate') startDate?: string, @Query('startDate') startDate?: string,
@Query('endDate') endDate?: string, @Query('endDate') endDate?: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number, @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number
): Promise<{ logs: AuditLogResponseDto[]; total: number; page: number; pageSize: number }> { ): Promise<{ logs: AuditLogResponseDto[]; total: number; page: number; pageSize: number }> {
page = page || 1; page = page || 1;
limit = limit || 50; limit = limit || 50;
@ -104,7 +114,7 @@ export class AuditController {
const { logs, total } = await this.auditService.getAuditLogs(filters); const { logs, total } = await this.auditService.getAuditLogs(filters);
return { return {
logs: logs.map((log) => this.mapToDto(log)), logs: logs.map(log => this.mapToDto(log)),
total, total,
page, page,
pageSize: limit, pageSize: limit,
@ -121,7 +131,7 @@ export class AuditController {
@ApiResponse({ status: 404, description: 'Audit log not found' }) @ApiResponse({ status: 404, description: 'Audit log not found' })
async getAuditLogById( async getAuditLogById(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<AuditLogResponseDto> { ): Promise<AuditLogResponseDto> {
const log = await this.auditService.getAuditLogs({ const log = await this.auditService.getAuditLogs({
organizationId: user.organizationId, organizationId: user.organizationId,
@ -145,14 +155,14 @@ export class AuditController {
async getResourceAuditTrail( async getResourceAuditTrail(
@Param('type') resourceType: string, @Param('type') resourceType: string,
@Param('id') resourceId: string, @Param('id') resourceId: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<AuditLogResponseDto[]> { ): Promise<AuditLogResponseDto[]> {
const logs = await this.auditService.getResourceAuditTrail(resourceType, resourceId); const logs = await this.auditService.getResourceAuditTrail(resourceType, resourceId);
// Filter by organization for security // Filter by organization for security
const filteredLogs = logs.filter((log) => log.organizationId === user.organizationId); const filteredLogs = logs.filter(log => log.organizationId === user.organizationId);
return filteredLogs.map((log) => this.mapToDto(log)); return filteredLogs.map(log => this.mapToDto(log));
} }
/** /**
@ -165,11 +175,11 @@ export class AuditController {
@ApiQuery({ name: 'limit', required: false, description: 'Number of recent logs (default: 50)' }) @ApiQuery({ name: 'limit', required: false, description: 'Number of recent logs (default: 50)' })
async getOrganizationActivity( async getOrganizationActivity(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number, @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number
): Promise<AuditLogResponseDto[]> { ): Promise<AuditLogResponseDto[]> {
limit = limit || 50; limit = limit || 50;
const logs = await this.auditService.getOrganizationActivity(user.organizationId, limit); const logs = await this.auditService.getOrganizationActivity(user.organizationId, limit);
return logs.map((log) => this.mapToDto(log)); return logs.map(log => this.mapToDto(log));
} }
/** /**
@ -183,15 +193,15 @@ export class AuditController {
async getUserActivity( async getUserActivity(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Param('userId') userId: string, @Param('userId') userId: string,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number, @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number
): Promise<AuditLogResponseDto[]> { ): Promise<AuditLogResponseDto[]> {
limit = limit || 50; limit = limit || 50;
const logs = await this.auditService.getUserActivity(userId, limit); const logs = await this.auditService.getUserActivity(userId, limit);
// Filter by organization for security // Filter by organization for security
const filteredLogs = logs.filter((log) => log.organizationId === user.organizationId); const filteredLogs = logs.filter(log => log.organizationId === user.organizationId);
return filteredLogs.map((log) => this.mapToDto(log)); return filteredLogs.map(log => this.mapToDto(log));
} }
/** /**

View File

@ -1,25 +1,7 @@
import { import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get } from '@nestjs/common';
Controller, import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
Post,
Body,
HttpCode,
HttpStatus,
UseGuards,
Get,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto } from '../dto/auth-login.dto';
LoginDto,
RegisterDto,
AuthResponseDto,
RefreshTokenDto,
} from '../dto/auth-login.dto';
import { Public } from '../decorators/public.decorator'; import { Public } from '../decorators/public.decorator';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
@ -52,8 +34,7 @@ export class AuthController {
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ @ApiOperation({
summary: 'Register new user', summary: 'Register new user',
description: description: 'Create a new user account with email and password. Returns JWT tokens.',
'Create a new user account with email and password. Returns JWT tokens.',
}) })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
@ -74,7 +55,7 @@ export class AuthController {
dto.password, dto.password,
dto.firstName, dto.firstName,
dto.lastName, dto.lastName,
dto.organizationId, dto.organizationId
); );
return { return {
@ -147,11 +128,8 @@ export class AuthController {
status: 401, status: 401,
description: 'Invalid or expired refresh token', description: 'Invalid or expired refresh token',
}) })
async refresh( async refresh(@Body() dto: RefreshTokenDto): Promise<{ accessToken: string }> {
@Body() dto: RefreshTokenDto, const result = await this.authService.refreshAccessToken(dto.refreshToken);
): Promise<{ accessToken: string }> {
const result =
await this.authService.refreshAccessToken(dto.refreshToken);
return { accessToken: result.accessToken }; return { accessToken: result.accessToken };
} }
@ -170,8 +148,7 @@ export class AuthController {
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: 'Logout', summary: 'Logout',
description: description: 'Logout the current user. Currently handled client-side by removing tokens.',
'Logout the current user. Currently handled client-side by removing tokens.',
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@ -32,17 +32,16 @@ import {
ApiProduces, ApiProduces,
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto } from '../dto';
CreateBookingRequestDto,
BookingResponseDto,
BookingListResponseDto,
} from '../dto';
import { BookingFilterDto } from '../dto/booking-filter.dto'; import { BookingFilterDto } from '../dto/booking-filter.dto';
import { BookingExportDto, ExportFormat } from '../dto/booking-export.dto'; import { BookingExportDto, ExportFormat } from '../dto/booking-export.dto';
import { BookingMapper } from '../mappers'; import { BookingMapper } from '../mappers';
import { BookingService } from '../../domain/services/booking.service'; import { BookingService } from '../../domain/services/booking.service';
import { BookingRepository, BOOKING_REPOSITORY } from '../../domain/ports/out/booking.repository'; import { BookingRepository, BOOKING_REPOSITORY } from '../../domain/ports/out/booking.repository';
import { RateQuoteRepository, RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.repository'; import {
RateQuoteRepository,
RATE_QUOTE_REPOSITORY,
} from '../../domain/ports/out/rate-quote.repository';
import { BookingNumber } from '../../domain/value-objects/booking-number.vo'; import { BookingNumber } from '../../domain/value-objects/booking-number.vo';
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
@ -71,7 +70,7 @@ export class BookingsController {
private readonly auditService: AuditService, private readonly auditService: AuditService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly notificationsGateway: NotificationsGateway, private readonly notificationsGateway: NotificationsGateway,
private readonly webhookService: WebhookService, private readonly webhookService: WebhookService
) {} ) {}
@Post() @Post()
@ -102,11 +101,9 @@ export class BookingsController {
}) })
async createBooking( async createBooking(
@Body() dto: CreateBookingRequestDto, @Body() dto: CreateBookingRequestDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<BookingResponseDto> { ): Promise<BookingResponseDto> {
this.logger.log( this.logger.log(`[User: ${user.email}] Creating booking for rate quote: ${dto.rateQuoteId}`);
`[User: ${user.email}] Creating booking for rate quote: ${dto.rateQuoteId}`,
);
try { try {
// Convert DTO to domain input, using authenticated user's data // Convert DTO to domain input, using authenticated user's data
@ -129,7 +126,7 @@ export class BookingsController {
const response = BookingMapper.toDto(booking, rateQuote); const response = BookingMapper.toDto(booking, rateQuote);
this.logger.log( this.logger.log(
`Booking created successfully: ${booking.bookingNumber.value} (${booking.id})`, `Booking created successfully: ${booking.bookingNumber.value} (${booking.id})`
); );
// Audit log: Booking created // Audit log: Booking created
@ -147,7 +144,7 @@ export class BookingsController {
status: booking.status.value, status: booking.status.value,
carrier: rateQuote.carrierName, carrier: rateQuote.carrierName,
}, },
}, }
); );
// Send real-time notification // Send real-time notification
@ -156,7 +153,7 @@ export class BookingsController {
user.id, user.id,
user.organizationId, user.organizationId,
booking.bookingNumber.value, booking.bookingNumber.value,
booking.id, booking.id
); );
await this.notificationsGateway.sendNotificationToUser(user.id, notification); await this.notificationsGateway.sendNotificationToUser(user.id, notification);
} catch (error: any) { } catch (error: any) {
@ -181,7 +178,7 @@ export class BookingsController {
etd: rateQuote.etd?.toISOString(), etd: rateQuote.etd?.toISOString(),
eta: rateQuote.eta?.toISOString(), eta: rateQuote.eta?.toISOString(),
createdAt: booking.createdAt.toISOString(), createdAt: booking.createdAt.toISOString(),
}, }
); );
} catch (error: any) { } catch (error: any) {
// Don't fail the booking creation if webhook fails // Don't fail the booking creation if webhook fails
@ -192,7 +189,7 @@ export class BookingsController {
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Booking creation failed: ${error?.message || 'Unknown error'}`, `Booking creation failed: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
// Audit log: Booking creation failed // Audit log: Booking creation failed
@ -207,7 +204,7 @@ export class BookingsController {
metadata: { metadata: {
rateQuoteId: dto.rateQuoteId, rateQuoteId: dto.rateQuoteId,
}, },
}, }
); );
throw error; throw error;
@ -217,8 +214,7 @@ export class BookingsController {
@Get(':id') @Get(':id')
@ApiOperation({ @ApiOperation({
summary: 'Get booking by ID', summary: 'Get booking by ID',
description: description: 'Retrieve detailed information about a specific booking. Requires authentication.',
'Retrieve detailed information about a specific booking. Requires authentication.',
}) })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@ -239,7 +235,7 @@ export class BookingsController {
}) })
async getBooking( async getBooking(
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<BookingResponseDto> { ): Promise<BookingResponseDto> {
this.logger.log(`[User: ${user.email}] Fetching booking: ${id}`); this.logger.log(`[User: ${user.email}] Fetching booking: ${id}`);
@ -287,15 +283,12 @@ export class BookingsController {
}) })
async getBookingByNumber( async getBookingByNumber(
@Param('bookingNumber') bookingNumber: string, @Param('bookingNumber') bookingNumber: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<BookingResponseDto> { ): Promise<BookingResponseDto> {
this.logger.log( this.logger.log(`[User: ${user.email}] Fetching booking by number: ${bookingNumber}`);
`[User: ${user.email}] Fetching booking by number: ${bookingNumber}`,
);
const bookingNumberVo = BookingNumber.fromString(bookingNumber); const bookingNumberVo = BookingNumber.fromString(bookingNumber);
const booking = const booking = await this.bookingRepository.findByBookingNumber(bookingNumberVo);
await this.bookingRepository.findByBookingNumber(bookingNumberVo);
if (!booking) { if (!booking) {
throw new NotFoundException(`Booking ${bookingNumber} not found`); throw new NotFoundException(`Booking ${bookingNumber} not found`);
@ -337,14 +330,7 @@ export class BookingsController {
name: 'status', name: 'status',
required: false, required: false,
description: 'Filter by booking status', description: 'Filter by booking status',
enum: [ enum: ['draft', 'pending_confirmation', 'confirmed', 'in_transit', 'delivered', 'cancelled'],
'draft',
'pending_confirmation',
'confirmed',
'in_transit',
'delivered',
'cancelled',
],
}) })
@ApiResponse({ @ApiResponse({
status: HttpStatus.OK, status: HttpStatus.OK,
@ -359,18 +345,17 @@ export class BookingsController {
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
@Query('status') status: string | undefined, @Query('status') status: string | undefined,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<BookingListResponseDto> { ): Promise<BookingListResponseDto> {
this.logger.log( this.logger.log(
`[User: ${user.email}] Listing bookings: page=${page}, pageSize=${pageSize}, status=${status}`, `[User: ${user.email}] Listing bookings: page=${page}, pageSize=${pageSize}, status=${status}`
); );
// Use authenticated user's organization ID // Use authenticated user's organization ID
const organizationId = user.organizationId; const organizationId = user.organizationId;
// Fetch bookings for the user's organization // Fetch bookings for the user's organization
const bookings = const bookings = await this.bookingRepository.findByOrganization(organizationId);
await this.bookingRepository.findByOrganization(organizationId);
// Filter by status if provided // Filter by status if provided
const filteredBookings = status const filteredBookings = status
@ -385,11 +370,9 @@ export class BookingsController {
// Fetch rate quotes for all bookings // Fetch rate quotes for all bookings
const bookingsWithQuotes = await Promise.all( const bookingsWithQuotes = await Promise.all(
paginatedBookings.map(async (booking: any) => { paginatedBookings.map(async (booking: any) => {
const rateQuote = await this.rateQuoteRepository.findById( const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
booking.rateQuoteId,
);
return { booking, rateQuote: rateQuote! }; return { booking, rateQuote: rateQuote! };
}), })
); );
// Convert to DTOs // Convert to DTOs
@ -436,7 +419,7 @@ export class BookingsController {
async fuzzySearch( async fuzzySearch(
@Query('q') searchTerm: string, @Query('q') searchTerm: string,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number, @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<BookingResponseDto[]> { ): Promise<BookingResponseDto[]> {
this.logger.log(`[User: ${user.email}] Fuzzy search: "${searchTerm}"`); this.logger.log(`[User: ${user.email}] Fuzzy search: "${searchTerm}"`);
@ -448,21 +431,21 @@ export class BookingsController {
const bookingOrms = await this.fuzzySearchService.search( const bookingOrms = await this.fuzzySearchService.search(
searchTerm, searchTerm,
user.organizationId, user.organizationId,
limit, limit
); );
// Map ORM entities to domain and fetch rate quotes // Map ORM entities to domain and fetch rate quotes
const bookingsWithQuotes = await Promise.all( const bookingsWithQuotes = await Promise.all(
bookingOrms.map(async (bookingOrm) => { bookingOrms.map(async bookingOrm => {
const booking = await this.bookingRepository.findById(bookingOrm.id); const booking = await this.bookingRepository.findById(bookingOrm.id);
const rateQuote = await this.rateQuoteRepository.findById(bookingOrm.rateQuoteId); const rateQuote = await this.rateQuoteRepository.findById(bookingOrm.rateQuoteId);
return { booking: booking!, rateQuote: rateQuote! }; return { booking: booking!, rateQuote: rateQuote! };
}), })
); );
// Convert to DTOs // Convert to DTOs
const bookingDtos = bookingsWithQuotes.map(({ booking, rateQuote }) => const bookingDtos = bookingsWithQuotes.map(({ booking, rateQuote }) =>
BookingMapper.toDto(booking, rateQuote), BookingMapper.toDto(booking, rateQuote)
); );
this.logger.log(`Fuzzy search returned ${bookingDtos.length} results`); this.logger.log(`Fuzzy search returned ${bookingDtos.length} results`);
@ -487,10 +470,10 @@ export class BookingsController {
}) })
async advancedSearch( async advancedSearch(
@Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto, @Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<BookingListResponseDto> { ): Promise<BookingListResponseDto> {
this.logger.log( this.logger.log(
`[User: ${user.email}] Advanced search with filters: ${JSON.stringify(filter)}`, `[User: ${user.email}] Advanced search with filters: ${JSON.stringify(filter)}`
); );
// Fetch all bookings for organization // Fetch all bookings for organization
@ -512,10 +495,10 @@ export class BookingsController {
// Fetch rate quotes // Fetch rate quotes
const bookingsWithQuotes = await Promise.all( const bookingsWithQuotes = await Promise.all(
paginatedBookings.map(async (booking) => { paginatedBookings.map(async booking => {
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
return { booking, rateQuote: rateQuote! }; return { booking, rateQuote: rateQuote! };
}), })
); );
// Convert to DTOs // Convert to DTOs
@ -539,7 +522,11 @@ export class BookingsController {
description: description:
'Export bookings with optional filtering. Supports CSV, Excel (xlsx), and JSON formats.', 'Export bookings with optional filtering. Supports CSV, Excel (xlsx), and JSON formats.',
}) })
@ApiProduces('text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/json') @ApiProduces(
'text/csv',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/json'
)
@ApiResponse({ @ApiResponse({
status: HttpStatus.OK, status: HttpStatus.OK,
description: 'Export file generated successfully', description: 'Export file generated successfully',
@ -552,20 +539,18 @@ export class BookingsController {
@Body(new ValidationPipe({ transform: true })) exportDto: BookingExportDto, @Body(new ValidationPipe({ transform: true })) exportDto: BookingExportDto,
@Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto, @Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response
): Promise<StreamableFile> { ): Promise<StreamableFile> {
this.logger.log( this.logger.log(`[User: ${user.email}] Exporting bookings to ${exportDto.format}`);
`[User: ${user.email}] Exporting bookings to ${exportDto.format}`,
);
let bookings: any[]; let bookings: any[];
// If specific booking IDs provided, use those // If specific booking IDs provided, use those
if (exportDto.bookingIds && exportDto.bookingIds.length > 0) { if (exportDto.bookingIds && exportDto.bookingIds.length > 0) {
bookings = await Promise.all( bookings = await Promise.all(
exportDto.bookingIds.map((id) => this.bookingRepository.findById(id)), exportDto.bookingIds.map(id => this.bookingRepository.findById(id))
); );
bookings = bookings.filter((b) => b !== null && b.organizationId === user.organizationId); bookings = bookings.filter(b => b !== null && b.organizationId === user.organizationId);
} else { } else {
// Otherwise, use filter criteria // Otherwise, use filter criteria
bookings = await this.bookingRepository.findByOrganization(user.organizationId); bookings = await this.bookingRepository.findByOrganization(user.organizationId);
@ -574,17 +559,17 @@ export class BookingsController {
// Fetch rate quotes // Fetch rate quotes
const bookingsWithQuotes = await Promise.all( const bookingsWithQuotes = await Promise.all(
bookings.map(async (booking) => { bookings.map(async booking => {
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId); const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
return { booking, rateQuote: rateQuote! }; return { booking, rateQuote: rateQuote! };
}), })
); );
// Generate export file // Generate export file
const exportResult = await this.exportService.exportBookings( const exportResult = await this.exportService.exportBookings(
bookingsWithQuotes, bookingsWithQuotes,
exportDto.format, exportDto.format,
exportDto.fields, exportDto.fields
); );
// Set response headers // Set response headers
@ -607,7 +592,7 @@ export class BookingsController {
fields: exportDto.fields?.join(', ') || 'all', fields: exportDto.fields?.join(', ') || 'all',
filename: exportResult.filename, filename: exportResult.filename,
}, },
}, }
); );
return new StreamableFile(exportResult.buffer); return new StreamableFile(exportResult.buffer);
@ -621,41 +606,35 @@ export class BookingsController {
// Filter by status // Filter by status
if (filter.status && filter.status.length > 0) { if (filter.status && filter.status.length > 0) {
filtered = filtered.filter((b) => filter.status!.includes(b.status.value)); filtered = filtered.filter(b => filter.status!.includes(b.status.value));
} }
// Filter by search (booking number partial match) // Filter by search (booking number partial match)
if (filter.search) { if (filter.search) {
const searchLower = filter.search.toLowerCase(); const searchLower = filter.search.toLowerCase();
filtered = filtered.filter((b) => filtered = filtered.filter(b => b.bookingNumber.value.toLowerCase().includes(searchLower));
b.bookingNumber.value.toLowerCase().includes(searchLower),
);
} }
// Filter by shipper // Filter by shipper
if (filter.shipper) { if (filter.shipper) {
const shipperLower = filter.shipper.toLowerCase(); const shipperLower = filter.shipper.toLowerCase();
filtered = filtered.filter((b) => filtered = filtered.filter(b => b.shipper.name.toLowerCase().includes(shipperLower));
b.shipper.name.toLowerCase().includes(shipperLower),
);
} }
// Filter by consignee // Filter by consignee
if (filter.consignee) { if (filter.consignee) {
const consigneeLower = filter.consignee.toLowerCase(); const consigneeLower = filter.consignee.toLowerCase();
filtered = filtered.filter((b) => filtered = filtered.filter(b => b.consignee.name.toLowerCase().includes(consigneeLower));
b.consignee.name.toLowerCase().includes(consigneeLower),
);
} }
// Filter by creation date range // Filter by creation date range
if (filter.createdFrom) { if (filter.createdFrom) {
const fromDate = new Date(filter.createdFrom); const fromDate = new Date(filter.createdFrom);
filtered = filtered.filter((b) => b.createdAt >= fromDate); filtered = filtered.filter(b => b.createdAt >= fromDate);
} }
if (filter.createdTo) { if (filter.createdTo) {
const toDate = new Date(filter.createdTo); const toDate = new Date(filter.createdTo);
filtered = filtered.filter((b) => b.createdAt <= toDate); filtered = filtered.filter(b => b.createdAt <= toDate);
} }
return filtered; return filtered;

View File

@ -41,17 +41,14 @@ export class GDPRController {
status: 200, status: 200,
description: 'Data export successful', description: 'Data export successful',
}) })
async exportData( async exportData(@CurrentUser() user: UserPayload, @Res() res: Response): Promise<void> {
@CurrentUser() user: UserPayload,
@Res() res: Response,
): Promise<void> {
const exportData = await this.gdprService.exportUserData(user.id); const exportData = await this.gdprService.exportUserData(user.id);
// Set headers for file download // Set headers for file download
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.setHeader( res.setHeader(
'Content-Disposition', 'Content-Disposition',
`attachment; filename="xpeditis-data-export-${user.id}-${Date.now()}.json"`, `attachment; filename="xpeditis-data-export-${user.id}-${Date.now()}.json"`
); );
res.json(exportData); res.json(exportData);
@ -69,10 +66,7 @@ export class GDPRController {
status: 200, status: 200,
description: 'CSV export successful', description: 'CSV export successful',
}) })
async exportDataCSV( async exportDataCSV(@CurrentUser() user: UserPayload, @Res() res: Response): Promise<void> {
@CurrentUser() user: UserPayload,
@Res() res: Response,
): Promise<void> {
const exportData = await this.gdprService.exportUserData(user.id); const exportData = await this.gdprService.exportUserData(user.id);
// Convert to CSV (simplified version) // Convert to CSV (simplified version)
@ -87,7 +81,7 @@ export class GDPRController {
res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Type', 'text/csv');
res.setHeader( res.setHeader(
'Content-Disposition', 'Content-Disposition',
`attachment; filename="xpeditis-data-export-${user.id}-${Date.now()}.csv"`, `attachment; filename="xpeditis-data-export-${user.id}-${Date.now()}.csv"`
); );
res.send(csv); res.send(csv);
@ -108,7 +102,7 @@ export class GDPRController {
}) })
async deleteAccount( async deleteAccount(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Body() body: { reason?: string; confirmEmail: string }, @Body() body: { reason?: string; confirmEmail: string }
): Promise<void> { ): Promise<void> {
// Verify email confirmation (security measure) // Verify email confirmation (security measure)
if (body.confirmEmail !== user.email) { if (body.confirmEmail !== user.email) {
@ -133,7 +127,7 @@ export class GDPRController {
}) })
async recordConsent( async recordConsent(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Body() body: Omit<ConsentData, 'userId'>, @Body() body: Omit<ConsentData, 'userId'>
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
await this.gdprService.recordConsent({ await this.gdprService.recordConsent({
...body, ...body,
@ -158,7 +152,7 @@ export class GDPRController {
}) })
async withdrawConsent( async withdrawConsent(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Body() body: { consentType: 'marketing' | 'analytics' }, @Body() body: { consentType: 'marketing' | 'analytics' }
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
await this.gdprService.withdrawConsent(user.id, body.consentType); await this.gdprService.withdrawConsent(user.id, body.consentType);
@ -177,9 +171,7 @@ export class GDPRController {
status: 200, status: 200,
description: 'Consent status retrieved', description: 'Consent status retrieved',
}) })
async getConsentStatus( async getConsentStatus(@CurrentUser() user: UserPayload): Promise<any> {
@CurrentUser() user: UserPayload,
): Promise<any> {
return this.gdprService.getConsentStatus(user.id); return this.gdprService.getConsentStatus(user.id);
} }
} }

View File

@ -17,13 +17,7 @@ import {
DefaultValuePipe, DefaultValuePipe,
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import { NotificationService } from '../services/notification.service'; import { NotificationService } from '../services/notification.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
@ -62,7 +56,7 @@ export class NotificationsController {
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Query('read') read?: string, @Query('read') read?: string,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page?: number,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit?: number, @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit?: number
): Promise<{ ): Promise<{
notifications: NotificationResponseDto[]; notifications: NotificationResponseDto[];
total: number; total: number;
@ -82,7 +76,7 @@ export class NotificationsController {
const { notifications, total } = await this.notificationService.getNotifications(filters); const { notifications, total } = await this.notificationService.getNotifications(filters);
return { return {
notifications: notifications.map((n) => this.mapToDto(n)), notifications: notifications.map(n => this.mapToDto(n)),
total, total,
page, page,
pageSize: limit, pageSize: limit,
@ -95,14 +89,18 @@ export class NotificationsController {
@Get('unread') @Get('unread')
@ApiOperation({ summary: 'Get unread notifications' }) @ApiOperation({ summary: 'Get unread notifications' })
@ApiResponse({ status: 200, description: 'Unread notifications retrieved successfully' }) @ApiResponse({ status: 200, description: 'Unread notifications retrieved successfully' })
@ApiQuery({ name: 'limit', required: false, description: 'Number of notifications (default: 50)' }) @ApiQuery({
name: 'limit',
required: false,
description: 'Number of notifications (default: 50)',
})
async getUnreadNotifications( async getUnreadNotifications(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number, @Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number
): Promise<NotificationResponseDto[]> { ): Promise<NotificationResponseDto[]> {
limit = limit || 50; limit = limit || 50;
const notifications = await this.notificationService.getUnreadNotifications(user.id, limit); const notifications = await this.notificationService.getUnreadNotifications(user.id, limit);
return notifications.map((n) => this.mapToDto(n)); return notifications.map(n => this.mapToDto(n));
} }
/** /**
@ -125,7 +123,7 @@ export class NotificationsController {
@ApiResponse({ status: 404, description: 'Notification not found' }) @ApiResponse({ status: 404, description: 'Notification not found' })
async getNotificationById( async getNotificationById(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Param('id') id: string, @Param('id') id: string
): Promise<NotificationResponseDto> { ): Promise<NotificationResponseDto> {
const notification = await this.notificationService.getNotificationById(id); const notification = await this.notificationService.getNotificationById(id);
@ -145,7 +143,7 @@ export class NotificationsController {
@ApiResponse({ status: 404, description: 'Notification not found' }) @ApiResponse({ status: 404, description: 'Notification not found' })
async markAsRead( async markAsRead(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Param('id') id: string, @Param('id') id: string
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
const notification = await this.notificationService.getNotificationById(id); const notification = await this.notificationService.getNotificationById(id);
@ -177,7 +175,7 @@ export class NotificationsController {
@ApiResponse({ status: 404, description: 'Notification not found' }) @ApiResponse({ status: 404, description: 'Notification not found' })
async deleteNotification( async deleteNotification(
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload,
@Param('id') id: string, @Param('id') id: string
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
const notification = await this.notificationService.getNotificationById(id); const notification = await this.notificationService.getNotificationById(id);

View File

@ -36,7 +36,10 @@ import {
OrganizationListResponseDto, OrganizationListResponseDto,
} from '../dto/organization.dto'; } from '../dto/organization.dto';
import { OrganizationMapper } from '../mappers/organization.mapper'; import { OrganizationMapper } from '../mappers/organization.mapper';
import { OrganizationRepository, ORGANIZATION_REPOSITORY } from '../../domain/ports/out/organization.repository'; import {
OrganizationRepository,
ORGANIZATION_REPOSITORY,
} from '../../domain/ports/out/organization.repository';
import { Organization, OrganizationType } from '../../domain/entities/organization.entity'; import { Organization, OrganizationType } from '../../domain/entities/organization.entity';
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard'; import { RolesGuard } from '../guards/roles.guard';
@ -61,7 +64,7 @@ export class OrganizationsController {
private readonly logger = new Logger(OrganizationsController.name); private readonly logger = new Logger(OrganizationsController.name);
constructor( constructor(
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository, @Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository
) {} ) {}
/** /**
@ -75,8 +78,7 @@ export class OrganizationsController {
@UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@ApiOperation({ @ApiOperation({
summary: 'Create new organization', summary: 'Create new organization',
description: description: 'Create a new organization (freight forwarder, carrier, or shipper). Admin-only.',
'Create a new organization (freight forwarder, carrier, or shipper). Admin-only.',
}) })
@ApiResponse({ @ApiResponse({
status: HttpStatus.CREATED, status: HttpStatus.CREATED,
@ -96,28 +98,22 @@ export class OrganizationsController {
}) })
async createOrganization( async createOrganization(
@Body() dto: CreateOrganizationDto, @Body() dto: CreateOrganizationDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<OrganizationResponseDto> { ): Promise<OrganizationResponseDto> {
this.logger.log( this.logger.log(`[Admin: ${user.email}] Creating organization: ${dto.name} (${dto.type})`);
`[Admin: ${user.email}] Creating organization: ${dto.name} (${dto.type})`,
);
try { try {
// Check for duplicate name // Check for duplicate name
const existingByName = await this.organizationRepository.findByName(dto.name); const existingByName = await this.organizationRepository.findByName(dto.name);
if (existingByName) { if (existingByName) {
throw new ForbiddenException( throw new ForbiddenException(`Organization with name "${dto.name}" already exists`);
`Organization with name "${dto.name}" already exists`,
);
} }
// Check for duplicate SCAC if provided // Check for duplicate SCAC if provided
if (dto.scac) { if (dto.scac) {
const existingBySCAC = await this.organizationRepository.findBySCAC(dto.scac); const existingBySCAC = await this.organizationRepository.findBySCAC(dto.scac);
if (existingBySCAC) { if (existingBySCAC) {
throw new ForbiddenException( throw new ForbiddenException(`Organization with SCAC "${dto.scac}" already exists`);
`Organization with SCAC "${dto.scac}" already exists`,
);
} }
} }
@ -136,15 +132,13 @@ export class OrganizationsController {
// Save to database // Save to database
const savedOrg = await this.organizationRepository.save(organization); const savedOrg = await this.organizationRepository.save(organization);
this.logger.log( this.logger.log(`Organization created successfully: ${savedOrg.name} (${savedOrg.id})`);
`Organization created successfully: ${savedOrg.name} (${savedOrg.id})`,
);
return OrganizationMapper.toDto(savedOrg); return OrganizationMapper.toDto(savedOrg);
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Organization creation failed: ${error?.message || 'Unknown error'}`, `Organization creation failed: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
throw error; throw error;
} }
@ -181,7 +175,7 @@ export class OrganizationsController {
}) })
async getOrganization( async getOrganization(
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<OrganizationResponseDto> { ): Promise<OrganizationResponseDto> {
this.logger.log(`[User: ${user.email}] Fetching organization: ${id}`); this.logger.log(`[User: ${user.email}] Fetching organization: ${id}`);
@ -235,11 +229,9 @@ export class OrganizationsController {
async updateOrganization( async updateOrganization(
@Param('id', ParseUUIDPipe) id: string, @Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateOrganizationDto, @Body() dto: UpdateOrganizationDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<OrganizationResponseDto> { ): Promise<OrganizationResponseDto> {
this.logger.log( this.logger.log(`[User: ${user.email}] Updating organization: ${id}`);
`[User: ${user.email}] Updating organization: ${id}`,
);
const organization = await this.organizationRepository.findById(id); const organization = await this.organizationRepository.findById(id);
if (!organization) { if (!organization) {
@ -323,10 +315,10 @@ export class OrganizationsController {
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
@Query('type') type: OrganizationType | undefined, @Query('type') type: OrganizationType | undefined,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<OrganizationListResponseDto> { ): Promise<OrganizationListResponseDto> {
this.logger.log( this.logger.log(
`[User: ${user.email}] Listing organizations: page=${page}, pageSize=${pageSize}, type=${type}`, `[User: ${user.email}] Listing organizations: page=${page}, pageSize=${pageSize}, type=${type}`
); );
// Fetch organizations // Fetch organizations
@ -342,9 +334,7 @@ export class OrganizationsController {
} }
// Filter by type if provided // Filter by type if provided
const filteredOrgs = type const filteredOrgs = type ? organizations.filter(org => org.type === type) : organizations;
? organizations.filter(org => org.type === type)
: organizations;
// Paginate // Paginate
const startIndex = (page - 1) * pageSize; const startIndex = (page - 1) * pageSize;

View File

@ -37,7 +37,7 @@ export class RatesController {
constructor( constructor(
private readonly rateSearchService: RateSearchService, private readonly rateSearchService: RateSearchService,
private readonly csvRateSearchService: CsvRateSearchService, private readonly csvRateSearchService: CsvRateSearchService,
private readonly csvRateMapper: CsvRateMapper, private readonly csvRateMapper: CsvRateMapper
) {} ) {}
@Post('search') @Post('search')
@ -73,11 +73,11 @@ export class RatesController {
}) })
async searchRates( async searchRates(
@Body() dto: RateSearchRequestDto, @Body() dto: RateSearchRequestDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<RateSearchResponseDto> { ): Promise<RateSearchResponseDto> {
const startTime = Date.now(); const startTime = Date.now();
this.logger.log( this.logger.log(
`[User: ${user.email}] Searching rates: ${dto.origin}${dto.destination}, ${dto.containerType}`, `[User: ${user.email}] Searching rates: ${dto.origin}${dto.destination}, ${dto.containerType}`
); );
try { try {
@ -102,9 +102,7 @@ export class RatesController {
const quoteDtos = RateQuoteMapper.toDtoArray(result.quotes); const quoteDtos = RateQuoteMapper.toDtoArray(result.quotes);
const responseTimeMs = Date.now() - startTime; const responseTimeMs = Date.now() - startTime;
this.logger.log( this.logger.log(`Rate search completed: ${quoteDtos.length} quotes, ${responseTimeMs}ms`);
`Rate search completed: ${quoteDtos.length} quotes, ${responseTimeMs}ms`,
);
return { return {
quotes: quoteDtos, quotes: quoteDtos,
@ -118,10 +116,7 @@ export class RatesController {
responseTimeMs, responseTimeMs,
}; };
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(`Rate search failed: ${error?.message || 'Unknown error'}`, error?.stack);
`Rate search failed: ${error?.message || 'Unknown error'}`,
error?.stack,
);
throw error; throw error;
} }
} }
@ -152,11 +147,11 @@ export class RatesController {
}) })
async searchCsvRates( async searchCsvRates(
@Body() dto: CsvRateSearchDto, @Body() dto: CsvRateSearchDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<CsvRateSearchResponseDto> { ): Promise<CsvRateSearchResponseDto> {
const startTime = Date.now(); const startTime = Date.now();
this.logger.log( this.logger.log(
`[User: ${user.email}] Searching CSV rates: ${dto.origin}${dto.destination}, ${dto.volumeCBM} CBM, ${dto.weightKG} kg`, `[User: ${user.email}] Searching CSV rates: ${dto.origin}${dto.destination}, ${dto.volumeCBM} CBM, ${dto.weightKG} kg`
); );
try { try {
@ -179,14 +174,14 @@ export class RatesController {
const responseTimeMs = Date.now() - startTime; const responseTimeMs = Date.now() - startTime;
this.logger.log( this.logger.log(
`CSV rate search completed: ${response.totalResults} results, ${responseTimeMs}ms`, `CSV rate search completed: ${response.totalResults} results, ${responseTimeMs}ms`
); );
return response; return response;
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`CSV rate search failed: ${error?.message || 'Unknown error'}`, `CSV rate search failed: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
throw error; throw error;
} }
@ -220,7 +215,7 @@ export class RatesController {
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Failed to fetch companies: ${error?.message || 'Unknown error'}`, `Failed to fetch companies: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
throw error; throw error;
} }
@ -259,7 +254,7 @@ export class RatesController {
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Failed to fetch filter options: ${error?.message || 'Unknown error'}`, `Failed to fetch filter options: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
throw error; throw error;
} }

View File

@ -16,13 +16,12 @@ import {
NotFoundException, NotFoundException,
ForbiddenException, ForbiddenException,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { import {
ApiTags, WebhookService,
ApiOperation, CreateWebhookInput,
ApiResponse, UpdateWebhookInput,
ApiBearerAuth, } from '../services/webhook.service';
} from '@nestjs/swagger';
import { WebhookService, CreateWebhookInput, UpdateWebhookInput } from '../services/webhook.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard'; import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator'; import { Roles } from '../decorators/roles.decorator';
@ -74,7 +73,7 @@ export class WebhooksController {
@ApiResponse({ status: 201, description: 'Webhook created successfully' }) @ApiResponse({ status: 201, description: 'Webhook created successfully' })
async createWebhook( async createWebhook(
@Body() dto: CreateWebhookDto, @Body() dto: CreateWebhookDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<WebhookResponseDto> { ): Promise<WebhookResponseDto> {
const input: CreateWebhookInput = { const input: CreateWebhookInput = {
organizationId: user.organizationId, organizationId: user.organizationId,
@ -96,10 +95,8 @@ export class WebhooksController {
@ApiOperation({ summary: 'Get all webhooks for organization' }) @ApiOperation({ summary: 'Get all webhooks for organization' })
@ApiResponse({ status: 200, description: 'Webhooks retrieved successfully' }) @ApiResponse({ status: 200, description: 'Webhooks retrieved successfully' })
async getWebhooks(@CurrentUser() user: UserPayload): Promise<WebhookResponseDto[]> { async getWebhooks(@CurrentUser() user: UserPayload): Promise<WebhookResponseDto[]> {
const webhooks = await this.webhookService.getWebhooksByOrganization( const webhooks = await this.webhookService.getWebhooksByOrganization(user.organizationId);
user.organizationId, return webhooks.map(w => this.mapToDto(w));
);
return webhooks.map((w) => this.mapToDto(w));
} }
/** /**
@ -112,7 +109,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' }) @ApiResponse({ status: 404, description: 'Webhook not found' })
async getWebhookById( async getWebhookById(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<WebhookResponseDto> { ): Promise<WebhookResponseDto> {
const webhook = await this.webhookService.getWebhookById(id); const webhook = await this.webhookService.getWebhookById(id);
@ -139,7 +136,7 @@ export class WebhooksController {
async updateWebhook( async updateWebhook(
@Param('id') id: string, @Param('id') id: string,
@Body() dto: UpdateWebhookDto, @Body() dto: UpdateWebhookDto,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<WebhookResponseDto> { ): Promise<WebhookResponseDto> {
const webhook = await this.webhookService.getWebhookById(id); const webhook = await this.webhookService.getWebhookById(id);
@ -166,7 +163,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' }) @ApiResponse({ status: 404, description: 'Webhook not found' })
async activateWebhook( async activateWebhook(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
const webhook = await this.webhookService.getWebhookById(id); const webhook = await this.webhookService.getWebhookById(id);
@ -193,7 +190,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' }) @ApiResponse({ status: 404, description: 'Webhook not found' })
async deactivateWebhook( async deactivateWebhook(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
const webhook = await this.webhookService.getWebhookById(id); const webhook = await this.webhookService.getWebhookById(id);
@ -220,7 +217,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' }) @ApiResponse({ status: 404, description: 'Webhook not found' })
async deleteWebhook( async deleteWebhook(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: UserPayload, @CurrentUser() user: UserPayload
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
const webhook = await this.webhookService.getWebhookById(id); const webhook = await this.webhookService.getWebhookById(id);

View File

@ -38,5 +38,5 @@ export const CurrentUser = createParamDecorator(
// If a specific property is requested, return only that property // If a specific property is requested, return only that property
return data ? user?.[data] : user; return data ? user?.[data] : user;
}, }
); );

View File

@ -1,4 +1,13 @@
import { IsString, IsUUID, IsOptional, ValidateNested, IsArray, IsEmail, Matches, MinLength } from 'class-validator'; import {
IsString,
IsUUID,
IsOptional,
ValidateNested,
IsArray,
IsEmail,
Matches,
MinLength,
} from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
@ -45,7 +54,9 @@ export class PartyDto {
@ApiProperty({ example: '+31612345678' }) @ApiProperty({ example: '+31612345678' })
@IsString() @IsString()
@Matches(/^\+?[1-9]\d{1,14}$/, { message: 'Contact phone must be a valid international phone number' }) @Matches(/^\+?[1-9]\d{1,14}$/, {
message: 'Contact phone must be a valid international phone number',
})
contactPhone: string; contactPhone: string;
} }
@ -57,14 +68,19 @@ export class ContainerDto {
@ApiPropertyOptional({ example: 'ABCU1234567', description: 'Container number (11 characters)' }) @ApiPropertyOptional({ example: 'ABCU1234567', description: 'Container number (11 characters)' })
@IsOptional() @IsOptional()
@IsString() @IsString()
@Matches(/^[A-Z]{4}\d{7}$/, { message: 'Container number must be 4 letters followed by 7 digits' }) @Matches(/^[A-Z]{4}\d{7}$/, {
message: 'Container number must be 4 letters followed by 7 digits',
})
containerNumber?: string; containerNumber?: string;
@ApiPropertyOptional({ example: 22000, description: 'Verified Gross Mass in kg' }) @ApiPropertyOptional({ example: 22000, description: 'Verified Gross Mass in kg' })
@IsOptional() @IsOptional()
vgm?: number; vgm?: number;
@ApiPropertyOptional({ example: -18, description: 'Temperature in Celsius (for reefer containers)' }) @ApiPropertyOptional({
example: -18,
description: 'Temperature in Celsius (for reefer containers)',
})
@IsOptional() @IsOptional()
temperature?: number; temperature?: number;
@ -77,7 +93,7 @@ export class ContainerDto {
export class CreateBookingRequestDto { export class CreateBookingRequestDto {
@ApiProperty({ @ApiProperty({
example: '550e8400-e29b-41d4-a716-446655440000', example: '550e8400-e29b-41d4-a716-446655440000',
description: 'Rate quote ID from previous search' description: 'Rate quote ID from previous search',
}) })
@IsUUID(4, { message: 'Rate quote ID must be a valid UUID' }) @IsUUID(4, { message: 'Rate quote ID must be a valid UUID' })
rateQuoteId: string; rateQuoteId: string;
@ -94,7 +110,7 @@ export class CreateBookingRequestDto {
@ApiProperty({ @ApiProperty({
example: 'Electronics and consumer goods', example: 'Electronics and consumer goods',
description: 'Cargo description' description: 'Cargo description',
}) })
@IsString() @IsString()
@MinLength(10, { message: 'Cargo description must be at least 10 characters' }) @MinLength(10, { message: 'Cargo description must be at least 10 characters' })
@ -102,7 +118,7 @@ export class CreateBookingRequestDto {
@ApiProperty({ @ApiProperty({
type: [ContainerDto], type: [ContainerDto],
description: 'Container details (can be empty for initial booking)' description: 'Container details (can be empty for initial booking)',
}) })
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@ -111,7 +127,7 @@ export class CreateBookingRequestDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
example: 'Please handle with care. Delivery before 5 PM.', example: 'Please handle with care. Delivery before 5 PM.',
description: 'Special instructions for the carrier' description: 'Special instructions for the carrier',
}) })
@IsOptional() @IsOptional()
@IsString() @IsString()

View File

@ -1,12 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { import { IsNotEmpty, IsString, IsNumber, Min, IsOptional, ValidateNested } from 'class-validator';
IsNotEmpty,
IsString,
IsNumber,
Min,
IsOptional,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { RateSearchFiltersDto } from './rate-search-filters.dto'; import { RateSearchFiltersDto } from './rate-search-filters.dto';
@ -152,7 +145,7 @@ export class CsvRateResultDto {
@ApiProperty({ @ApiProperty({
description: 'Calculated price in USD', description: 'Calculated price in USD',
example: 1850.50, example: 1850.5,
}) })
priceUSD: number; priceUSD: number;

View File

@ -1,4 +1,13 @@
import { IsString, IsDateString, IsEnum, IsOptional, IsInt, Min, IsBoolean, Matches } from 'class-validator'; import {
IsString,
IsDateString,
IsEnum,
IsOptional,
IsInt,
Min,
IsBoolean,
Matches,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RateSearchRequestDto { export class RateSearchRequestDto {
@ -17,7 +26,9 @@ export class RateSearchRequestDto {
pattern: '^[A-Z]{5}$', pattern: '^[A-Z]{5}$',
}) })
@IsString() @IsString()
@Matches(/^[A-Z]{5}$/, { message: 'Destination must be a valid 5-character UN/LOCODE (e.g., CNSHA)' }) @Matches(/^[A-Z]{5}$/, {
message: 'Destination must be a valid 5-character UN/LOCODE (e.g., CNSHA)',
})
destination: string; destination: string;
@ApiProperty({ @ApiProperty({
@ -92,6 +103,8 @@ export class RateSearchRequestDto {
}) })
@IsOptional() @IsOptional()
@IsString() @IsString()
@Matches(/^[1-9](\.[1-9])?$/, { message: 'IMO class must be in format X or X.Y (e.g., 3 or 3.1)' }) @Matches(/^[1-9](\.[1-9])?$/, {
message: 'IMO class must be in format X or X.Y (e.g., 3 or 3.1)',
})
imoClass?: string; imoClass?: string;
} }

View File

@ -67,7 +67,8 @@ export class CreateUserDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
example: 'TempPassword123!', example: 'TempPassword123!',
description: 'Temporary password (min 12 characters). If not provided, a random one will be generated.', description:
'Temporary password (min 12 characters). If not provided, a random one will be generated.',
minLength: 12, minLength: 12,
}) })
@IsString() @IsString()

View File

@ -39,7 +39,7 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
constructor( constructor(
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService
) {} ) {}
/** /**
@ -81,12 +81,12 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
// Send recent notifications on connection // Send recent notifications on connection
const recentNotifications = await this.notificationService.getRecentNotifications(userId, 10); const recentNotifications = await this.notificationService.getRecentNotifications(userId, 10);
client.emit('recent_notifications', { client.emit('recent_notifications', {
notifications: recentNotifications.map((n) => this.mapNotificationToDto(n)), notifications: recentNotifications.map(n => this.mapNotificationToDto(n)),
}); });
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Error during client connection: ${error?.message || 'Unknown error'}`, `Error during client connection: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
client.disconnect(); client.disconnect();
} }
@ -112,7 +112,7 @@ export class NotificationsGateway implements OnGatewayConnection, OnGatewayDisco
@SubscribeMessage('mark_as_read') @SubscribeMessage('mark_as_read')
async handleMarkAsRead( async handleMarkAsRead(
@ConnectedSocket() client: Socket, @ConnectedSocket() client: Socket,
@MessageBody() data: { notificationId: string }, @MessageBody() data: { notificationId: string }
) { ) {
try { try {
const userId = client.data.userId; const userId = client.data.userId;

View File

@ -23,11 +23,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
/** /**
* Custom error message (override for new API) * Custom error message (override for new API)
*/ */
protected async throwThrottlingException( protected async throwThrottlingException(context: ExecutionContext): Promise<void> {
context: ExecutionContext, throw new ThrottlerException('Too many requests. Please try again later.');
): Promise<void> {
throw new ThrottlerException(
'Too many requests. Please try again later.',
);
} }
} }

View File

@ -4,13 +4,7 @@
* Tracks request duration and logs metrics * Tracks request duration and logs metrics
*/ */
import { import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators'; import { tap, catchError } from 'rxjs/operators';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
@ -25,33 +19,31 @@ export class PerformanceMonitoringInterceptor implements NestInterceptor {
const startTime = Date.now(); const startTime = Date.now();
return next.handle().pipe( return next.handle().pipe(
tap((data) => { tap(data => {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
const response = context.switchToHttp().getResponse(); const response = context.switchToHttp().getResponse();
// Log performance // Log performance
if (duration > 1000) { if (duration > 1000) {
this.logger.warn( this.logger.warn(
`Slow request: ${method} ${url} took ${duration}ms (userId: ${user?.sub || 'anonymous'})`, `Slow request: ${method} ${url} took ${duration}ms (userId: ${user?.sub || 'anonymous'})`
); );
} }
// Log successful request // Log successful request
this.logger.log( this.logger.log(`${method} ${url} - ${response.statusCode} - ${duration}ms`);
`${method} ${url} - ${response.statusCode} - ${duration}ms`,
);
}), }),
catchError((error) => { catchError(error => {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
// Log error // Log error
this.logger.error( this.logger.error(
`Request error: ${method} ${url} (${duration}ms) - ${error.message}`, `Request error: ${method} ${url} (${duration}ms) - ${error.message}`,
error.stack, error.stack
); );
// Capture exception in Sentry // Capture exception in Sentry
Sentry.withScope((scope) => { Sentry.withScope(scope => {
scope.setContext('request', { scope.setContext('request', {
method, method,
url, url,
@ -62,7 +54,7 @@ export class PerformanceMonitoringInterceptor implements NestInterceptor {
}); });
throw error; throw error;
}), })
); );
} }
} }

View File

@ -47,7 +47,7 @@ export class BookingMapper {
contactPhone: dto.consignee.contactPhone, contactPhone: dto.consignee.contactPhone,
}, },
cargoDescription: dto.cargoDescription, cargoDescription: dto.cargoDescription,
containers: dto.containers.map((c) => ({ containers: dto.containers.map(c => ({
type: c.type, type: c.type,
containerNumber: c.containerNumber, containerNumber: c.containerNumber,
vgm: c.vgm, vgm: c.vgm,
@ -91,7 +91,7 @@ export class BookingMapper {
contactPhone: booking.consignee.contactPhone, contactPhone: booking.consignee.contactPhone,
}, },
cargoDescription: booking.cargoDescription, cargoDescription: booking.cargoDescription,
containers: booking.containers.map((c) => ({ containers: booking.containers.map(c => ({
id: c.id, id: c.id,
type: c.type, type: c.type,
containerNumber: c.containerNumber, containerNumber: c.containerNumber,
@ -116,7 +116,7 @@ export class BookingMapper {
}, },
pricing: { pricing: {
baseFreight: rateQuote.pricing.baseFreight, baseFreight: rateQuote.pricing.baseFreight,
surcharges: rateQuote.pricing.surcharges.map((s) => ({ surcharges: rateQuote.pricing.surcharges.map(s => ({
type: s.type, type: s.type,
description: s.description, description: s.description,
amount: s.amount, amount: s.amount,

View File

@ -1,10 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { CsvRate } from '@domain/entities/csv-rate.entity'; import { CsvRate } from '@domain/entities/csv-rate.entity';
import { Volume } from '@domain/value-objects/volume.vo'; import { Volume } from '@domain/value-objects/volume.vo';
import { import { CsvRateResultDto, CsvRateSearchResponseDto } from '../dto/csv-rate-search.dto';
CsvRateResultDto,
CsvRateSearchResponseDto,
} from '../dto/csv-rate-search.dto';
import { import {
CsvRateSearchInput, CsvRateSearchInput,
CsvRateSearchOutput, CsvRateSearchOutput,
@ -77,7 +74,7 @@ export class CsvRateMapper {
*/ */
mapSearchOutputToResponseDto(output: CsvRateSearchOutput): CsvRateSearchResponseDto { mapSearchOutputToResponseDto(output: CsvRateSearchOutput): CsvRateSearchResponseDto {
return { return {
results: output.results.map((result) => this.mapSearchResultToDto(result)), results: output.results.map(result => this.mapSearchResultToDto(result)),
totalResults: output.totalResults, totalResults: output.totalResults,
searchedFiles: output.searchedFiles, searchedFiles: output.searchedFiles,
searchedAt: output.searchedAt, searchedAt: output.searchedAt,
@ -107,6 +104,6 @@ export class CsvRateMapper {
* Map multiple config entities to DTOs * Map multiple config entities to DTOs
*/ */
mapConfigEntitiesToDtos(entities: CsvRateConfigOrmEntity[]): CsvRateConfigDto[] { mapConfigEntitiesToDtos(entities: CsvRateConfigOrmEntity[]): CsvRateConfigDto[] {
return entities.map((entity) => this.mapConfigEntityToDto(entity)); return entities.map(entity => this.mapConfigEntityToDto(entity));
} }
} }

View File

@ -56,9 +56,7 @@ export class OrganizationMapper {
/** /**
* Map Document entity to DTO * Map Document entity to DTO
*/ */
private static mapDocumentToDto( private static mapDocumentToDto(document: OrganizationDocument): OrganizationDocumentDto {
document: OrganizationDocument,
): OrganizationDocumentDto {
return { return {
id: document.id, id: document.id,
type: document.type, type: document.type,

View File

@ -29,7 +29,7 @@ export class RateQuoteMapper {
}, },
pricing: { pricing: {
baseFreight: entity.pricing.baseFreight, baseFreight: entity.pricing.baseFreight,
surcharges: entity.pricing.surcharges.map((s) => ({ surcharges: entity.pricing.surcharges.map(s => ({
type: s.type, type: s.type,
description: s.description, description: s.description,
amount: s.amount, amount: s.amount,
@ -43,7 +43,7 @@ export class RateQuoteMapper {
etd: entity.etd.toISOString(), etd: entity.etd.toISOString(),
eta: entity.eta.toISOString(), eta: entity.eta.toISOString(),
transitDays: entity.transitDays, transitDays: entity.transitDays,
route: entity.route.map((segment) => ({ route: entity.route.map(segment => ({
portCode: segment.portCode, portCode: segment.portCode,
portName: segment.portName, portName: segment.portName,
arrival: segment.arrival?.toISOString(), arrival: segment.arrival?.toISOString(),
@ -64,6 +64,6 @@ export class RateQuoteMapper {
* Map array of RateQuote entities to DTOs * Map array of RateQuote entities to DTOs
*/ */
static toDtoArray(entities: RateQuote[]): RateQuoteDto[] { static toDtoArray(entities: RateQuote[]): RateQuoteDto[] {
return entities.map((entity) => this.toDto(entity)); return entities.map(entity => this.toDto(entity));
} }
} }

View File

@ -45,33 +45,14 @@ import { CarrierOrmEntity } from '../../infrastructure/persistence/typeorm/entit
}, },
{ {
provide: RateSearchService, provide: RateSearchService,
useFactory: ( useFactory: (cache: any, rateQuoteRepo: any, portRepo: any, carrierRepo: any) => {
cache: any,
rateQuoteRepo: any,
portRepo: any,
carrierRepo: any,
) => {
// For now, create service with empty connectors array // For now, create service with empty connectors array
// TODO: Inject actual carrier connectors // TODO: Inject actual carrier connectors
return new RateSearchService( return new RateSearchService([], cache, rateQuoteRepo, portRepo, carrierRepo);
[],
cache,
rateQuoteRepo,
portRepo,
carrierRepo,
);
}, },
inject: [ inject: [CACHE_PORT, RATE_QUOTE_REPOSITORY, PORT_REPOSITORY, CARRIER_REPOSITORY],
CACHE_PORT,
RATE_QUOTE_REPOSITORY,
PORT_REPOSITORY,
CARRIER_REPOSITORY,
],
}, },
], ],
exports: [ exports: [RATE_QUOTE_REPOSITORY, RateSearchService],
RATE_QUOTE_REPOSITORY,
RateSearchService,
],
}) })
export class RatesModule {} export class RatesModule {}

View File

@ -53,7 +53,7 @@ export class AnalyticsService {
@Inject(BOOKING_REPOSITORY) @Inject(BOOKING_REPOSITORY)
private readonly bookingRepository: BookingRepository, private readonly bookingRepository: BookingRepository,
@Inject(RATE_QUOTE_REPOSITORY) @Inject(RATE_QUOTE_REPOSITORY)
private readonly rateQuoteRepository: RateQuoteRepository, private readonly rateQuoteRepository: RateQuoteRepository
) {} ) {}
/** /**
@ -70,13 +70,11 @@ export class AnalyticsService {
const allBookings = await this.bookingRepository.findByOrganization(organizationId); const allBookings = await this.bookingRepository.findByOrganization(organizationId);
// This month bookings // This month bookings
const thisMonthBookings = allBookings.filter( const thisMonthBookings = allBookings.filter(b => b.createdAt >= thisMonthStart);
(b) => b.createdAt >= thisMonthStart
);
// Last month bookings // Last month bookings
const lastMonthBookings = allBookings.filter( const lastMonthBookings = allBookings.filter(
(b) => b.createdAt >= lastMonthStart && b.createdAt <= lastMonthEnd b => b.createdAt >= lastMonthStart && b.createdAt <= lastMonthEnd
); );
// Calculate total TEUs (20' = 1 TEU, 40' = 2 TEU) // Calculate total TEUs (20' = 1 TEU, 40' = 2 TEU)
@ -118,10 +116,10 @@ export class AnalyticsService {
// Pending confirmations (status = pending_confirmation) // Pending confirmations (status = pending_confirmation)
const pendingThisMonth = thisMonthBookings.filter( const pendingThisMonth = thisMonthBookings.filter(
(b) => b.status.value === 'pending_confirmation' b => b.status.value === 'pending_confirmation'
).length; ).length;
const pendingLastMonth = lastMonthBookings.filter( const pendingLastMonth = lastMonthBookings.filter(
(b) => b.status.value === 'pending_confirmation' b => b.status.value === 'pending_confirmation'
).length; ).length;
// Calculate percentage changes // Calculate percentage changes
@ -135,15 +133,9 @@ export class AnalyticsService {
totalTEUs: totalTEUsThisMonth, totalTEUs: totalTEUsThisMonth,
estimatedRevenue: estimatedRevenueThisMonth, estimatedRevenue: estimatedRevenueThisMonth,
pendingConfirmations: pendingThisMonth, pendingConfirmations: pendingThisMonth,
bookingsThisMonthChange: calculateChange( bookingsThisMonthChange: calculateChange(thisMonthBookings.length, lastMonthBookings.length),
thisMonthBookings.length,
lastMonthBookings.length
),
totalTEUsChange: calculateChange(totalTEUsThisMonth, totalTEUsLastMonth), totalTEUsChange: calculateChange(totalTEUsThisMonth, totalTEUsLastMonth),
estimatedRevenueChange: calculateChange( estimatedRevenueChange: calculateChange(estimatedRevenueThisMonth, estimatedRevenueLastMonth),
estimatedRevenueThisMonth,
estimatedRevenueLastMonth
),
pendingConfirmationsChange: calculateChange(pendingThisMonth, pendingLastMonth), pendingConfirmationsChange: calculateChange(pendingThisMonth, pendingLastMonth),
}; };
} }
@ -172,7 +164,7 @@ export class AnalyticsService {
// Count bookings in this month // Count bookings in this month
const count = allBookings.filter( const count = allBookings.filter(
(b) => b.createdAt >= monthDate && b.createdAt <= monthEnd b => b.createdAt >= monthDate && b.createdAt <= monthEnd
).length; ).length;
data.push(count); data.push(count);
} }
@ -187,13 +179,16 @@ export class AnalyticsService {
const allBookings = await this.bookingRepository.findByOrganization(organizationId); const allBookings = await this.bookingRepository.findByOrganization(organizationId);
// Group by route (origin-destination) // Group by route (origin-destination)
const routeMap = new Map<string, { const routeMap = new Map<
originPort: string; string,
destinationPort: string; {
bookingCount: number; originPort: string;
totalTEUs: number; destinationPort: string;
totalPrice: number; bookingCount: number;
}>(); totalTEUs: number;
totalPrice: number;
}
>();
for (const booking of allBookings) { for (const booking of allBookings) {
try { try {
@ -231,16 +226,14 @@ export class AnalyticsService {
} }
// Convert to array and sort by booking count // Convert to array and sort by booking count
const tradeLanes: TopTradeLane[] = Array.from(routeMap.entries()).map( const tradeLanes: TopTradeLane[] = Array.from(routeMap.entries()).map(([route, data]) => ({
([route, data]) => ({ route,
route, originPort: data.originPort,
originPort: data.originPort, destinationPort: data.destinationPort,
destinationPort: data.destinationPort, bookingCount: data.bookingCount,
bookingCount: data.bookingCount, totalTEUs: data.totalTEUs,
totalTEUs: data.totalTEUs, avgPrice: data.totalPrice / data.bookingCount,
avgPrice: data.totalPrice / data.bookingCount, }));
})
);
// Sort by booking count and return top 5 // Sort by booking count and return top 5
return tradeLanes.sort((a, b) => b.bookingCount - a.bookingCount).slice(0, 5); return tradeLanes.sort((a, b) => b.bookingCount - a.bookingCount).slice(0, 5);
@ -256,7 +249,7 @@ export class AnalyticsService {
// Check for pending confirmations (older than 24h) // Check for pending confirmations (older than 24h)
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const oldPendingBookings = allBookings.filter( const oldPendingBookings = allBookings.filter(
(b) => b.status.value === 'pending_confirmation' && b.createdAt < oneDayAgo b => b.status.value === 'pending_confirmation' && b.createdAt < oneDayAgo
); );
for (const booking of oldPendingBookings) { for (const booking of oldPendingBookings) {

View File

@ -4,7 +4,10 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { AuditService } from './audit.service'; import { AuditService } from './audit.service';
import { AUDIT_LOG_REPOSITORY, AuditLogRepository } from '../../domain/ports/out/audit-log.repository'; import {
AUDIT_LOG_REPOSITORY,
AuditLogRepository,
} from '../../domain/ports/out/audit-log.repository';
import { AuditAction, AuditStatus, AuditLog } from '../../domain/entities/audit-log.entity'; import { AuditAction, AuditStatus, AuditLog } from '../../domain/entities/audit-log.entity';
describe('AuditService', () => { describe('AuditService', () => {

View File

@ -7,11 +7,7 @@
import { Injectable, Logger, Inject } from '@nestjs/common'; import { Injectable, Logger, Inject } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { import { AuditLog, AuditAction, AuditStatus } from '../../domain/entities/audit-log.entity';
AuditLog,
AuditAction,
AuditStatus,
} from '../../domain/entities/audit-log.entity';
import { import {
AuditLogRepository, AuditLogRepository,
AUDIT_LOG_REPOSITORY, AUDIT_LOG_REPOSITORY,
@ -39,7 +35,7 @@ export class AuditService {
constructor( constructor(
@Inject(AUDIT_LOG_REPOSITORY) @Inject(AUDIT_LOG_REPOSITORY)
private readonly auditLogRepository: AuditLogRepository, private readonly auditLogRepository: AuditLogRepository
) {} ) {}
/** /**
@ -54,14 +50,12 @@ export class AuditService {
await this.auditLogRepository.save(auditLog); await this.auditLogRepository.save(auditLog);
this.logger.log( this.logger.log(`Audit log created: ${input.action} by ${input.userEmail} (${input.status})`);
`Audit log created: ${input.action} by ${input.userEmail} (${input.status})`,
);
} catch (error: any) { } catch (error: any) {
// Never throw on audit logging failure - log the error and continue // Never throw on audit logging failure - log the error and continue
this.logger.error( this.logger.error(
`Failed to create audit log: ${error?.message || 'Unknown error'}`, `Failed to create audit log: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
} }
} }
@ -81,7 +75,7 @@ export class AuditService {
metadata?: Record<string, any>; metadata?: Record<string, any>;
ipAddress?: string; ipAddress?: string;
userAgent?: string; userAgent?: string;
}, }
): Promise<void> { ): Promise<void> {
await this.log({ await this.log({
action, action,
@ -108,7 +102,7 @@ export class AuditService {
metadata?: Record<string, any>; metadata?: Record<string, any>;
ipAddress?: string; ipAddress?: string;
userAgent?: string; userAgent?: string;
}, }
): Promise<void> { ): Promise<void> {
await this.log({ await this.log({
action, action,
@ -139,20 +133,14 @@ export class AuditService {
/** /**
* Get audit trail for a specific resource * Get audit trail for a specific resource
*/ */
async getResourceAuditTrail( async getResourceAuditTrail(resourceType: string, resourceId: string): Promise<AuditLog[]> {
resourceType: string,
resourceId: string,
): Promise<AuditLog[]> {
return this.auditLogRepository.findByResource(resourceType, resourceId); return this.auditLogRepository.findByResource(resourceType, resourceId);
} }
/** /**
* Get recent activity for an organization * Get recent activity for an organization
*/ */
async getOrganizationActivity( async getOrganizationActivity(organizationId: string, limit: number = 50): Promise<AuditLog[]> {
organizationId: string,
limit: number = 50,
): Promise<AuditLog[]> {
return this.auditLogRepository.findRecentByOrganization(organizationId, limit); return this.auditLogRepository.findRecentByOrganization(organizationId, limit);
} }

View File

@ -8,12 +8,12 @@ import { Injectable, Logger, Inject } from '@nestjs/common';
import { Booking } from '../../domain/entities/booking.entity'; import { Booking } from '../../domain/entities/booking.entity';
import { EmailPort, EMAIL_PORT } from '../../domain/ports/out/email.port'; import { EmailPort, EMAIL_PORT } from '../../domain/ports/out/email.port';
import { PdfPort, PDF_PORT, BookingPdfData } from '../../domain/ports/out/pdf.port'; import { PdfPort, PDF_PORT, BookingPdfData } from '../../domain/ports/out/pdf.port';
import { import { StoragePort, STORAGE_PORT } from '../../domain/ports/out/storage.port';
StoragePort,
STORAGE_PORT,
} from '../../domain/ports/out/storage.port';
import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository'; import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository';
import { RateQuoteRepository, RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.repository'; import {
RateQuoteRepository,
RATE_QUOTE_REPOSITORY,
} from '../../domain/ports/out/rate-quote.repository';
@Injectable() @Injectable()
export class BookingAutomationService { export class BookingAutomationService {
@ -24,16 +24,14 @@ export class BookingAutomationService {
@Inject(PDF_PORT) private readonly pdfPort: PdfPort, @Inject(PDF_PORT) private readonly pdfPort: PdfPort,
@Inject(STORAGE_PORT) private readonly storagePort: StoragePort, @Inject(STORAGE_PORT) private readonly storagePort: StoragePort,
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
@Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository, @Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository
) {} ) {}
/** /**
* Execute all post-booking automation tasks * Execute all post-booking automation tasks
*/ */
async executePostBookingTasks(booking: Booking): Promise<void> { async executePostBookingTasks(booking: Booking): Promise<void> {
this.logger.log( this.logger.log(`Starting post-booking automation for booking: ${booking.bookingNumber.value}`);
`Starting post-booking automation for booking: ${booking.bookingNumber.value}`
);
try { try {
// Get user and rate quote details // Get user and rate quote details
@ -42,9 +40,7 @@ export class BookingAutomationService {
throw new Error(`User not found: ${booking.userId}`); throw new Error(`User not found: ${booking.userId}`);
} }
const rateQuote = await this.rateQuoteRepository.findById( const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
booking.rateQuoteId
);
if (!rateQuote) { if (!rateQuote) {
throw new Error(`Rate quote not found: ${booking.rateQuoteId}`); throw new Error(`Rate quote not found: ${booking.rateQuoteId}`);
} }
@ -79,7 +75,7 @@ export class BookingAutomationService {
email: booking.consignee.contactEmail, email: booking.consignee.contactEmail,
phone: booking.consignee.contactPhone, phone: booking.consignee.contactPhone,
}, },
containers: booking.containers.map((c) => ({ containers: booking.containers.map(c => ({
type: c.type, type: c.type,
quantity: 1, quantity: 1,
containerNumber: c.containerNumber, containerNumber: c.containerNumber,
@ -173,10 +169,7 @@ export class BookingAutomationService {
`Sent ${updateType} notification for booking: ${booking.bookingNumber.value}` `Sent ${updateType} notification for booking: ${booking.bookingNumber.value}`
); );
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(`Failed to send booking update notification`, error);
`Failed to send booking update notification`,
error
);
} }
} }
} }

View File

@ -38,13 +38,11 @@ export class BruteForceProtectionService {
// Calculate block time with exponential backoff // Calculate block time with exponential backoff
if (existing.count > bruteForceConfig.freeRetries) { if (existing.count > bruteForceConfig.freeRetries) {
const waitTime = this.calculateWaitTime( const waitTime = this.calculateWaitTime(existing.count - bruteForceConfig.freeRetries);
existing.count - bruteForceConfig.freeRetries,
);
existing.blockedUntil = new Date(now.getTime() + waitTime); existing.blockedUntil = new Date(now.getTime() + waitTime);
this.logger.warn( this.logger.warn(
`Brute force detected for ${identifier}. Blocked until ${existing.blockedUntil.toISOString()}`, `Brute force detected for ${identifier}. Blocked until ${existing.blockedUntil.toISOString()}`
); );
} }
@ -99,7 +97,7 @@ export class BruteForceProtectionService {
const now = new Date(); const now = new Date();
const remaining = Math.max( const remaining = Math.max(
0, 0,
Math.floor((attempt.blockedUntil.getTime() - now.getTime()) / 1000), Math.floor((attempt.blockedUntil.getTime() - now.getTime()) / 1000)
); );
return remaining; return remaining;
@ -116,8 +114,7 @@ export class BruteForceProtectionService {
* Calculate wait time with exponential backoff * Calculate wait time with exponential backoff
*/ */
private calculateWaitTime(failedAttempts: number): number { private calculateWaitTime(failedAttempts: number): number {
const waitTime = const waitTime = bruteForceConfig.minWait * Math.pow(2, failedAttempts - 1);
bruteForceConfig.minWait * Math.pow(2, failedAttempts - 1);
return Math.min(waitTime, bruteForceConfig.maxWait); return Math.min(waitTime, bruteForceConfig.maxWait);
} }
@ -163,10 +160,7 @@ export class BruteForceProtectionService {
return { return {
totalAttempts, totalAttempts,
currentlyBlocked, currentlyBlocked,
averageAttempts: averageAttempts: this.attempts.size > 0 ? Math.round(totalAttempts / this.attempts.size) : 0,
this.attempts.size > 0
? Math.round(totalAttempts / this.attempts.size)
: 0,
}; };
} }
@ -190,9 +184,7 @@ export class BruteForceProtectionService {
}); });
} }
this.logger.warn( this.logger.warn(`Manually blocked ${identifier} for ${durationMs / 1000} seconds`);
`Manually blocked ${identifier} for ${durationMs / 1000} seconds`,
);
} }
/** /**

View File

@ -25,10 +25,10 @@ export class ExportService {
async exportBookings( async exportBookings(
data: BookingExportData[], data: BookingExportData[],
format: ExportFormat, format: ExportFormat,
fields?: ExportField[], fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> { ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
this.logger.log( this.logger.log(
`Exporting ${data.length} bookings to ${format} format with ${fields?.length || 'all'} fields`, `Exporting ${data.length} bookings to ${format} format with ${fields?.length || 'all'} fields`
); );
switch (format) { switch (format) {
@ -48,17 +48,17 @@ export class ExportService {
*/ */
private async exportToCSV( private async exportToCSV(
data: BookingExportData[], data: BookingExportData[],
fields?: ExportField[], fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> { ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
const selectedFields = fields || Object.values(ExportField); const selectedFields = fields || Object.values(ExportField);
const rows = data.map((item) => this.extractFields(item, selectedFields)); const rows = data.map(item => this.extractFields(item, selectedFields));
// Build CSV header // Build CSV header
const header = selectedFields.map((field) => this.getFieldLabel(field)).join(','); const header = selectedFields.map(field => this.getFieldLabel(field)).join(',');
// Build CSV rows // Build CSV rows
const csvRows = rows.map((row) => const csvRows = rows.map(row =>
selectedFields.map((field) => this.escapeCSVValue(row[field] || '')).join(','), selectedFields.map(field => this.escapeCSVValue(row[field] || '')).join(',')
); );
const csv = [header, ...csvRows].join('\n'); const csv = [header, ...csvRows].join('\n');
@ -79,10 +79,10 @@ export class ExportService {
*/ */
private async exportToExcel( private async exportToExcel(
data: BookingExportData[], data: BookingExportData[],
fields?: ExportField[], fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> { ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
const selectedFields = fields || Object.values(ExportField); const selectedFields = fields || Object.values(ExportField);
const rows = data.map((item) => this.extractFields(item, selectedFields)); const rows = data.map(item => this.extractFields(item, selectedFields));
const workbook = new ExcelJS.Workbook(); const workbook = new ExcelJS.Workbook();
workbook.creator = 'Xpeditis'; workbook.creator = 'Xpeditis';
@ -91,9 +91,7 @@ export class ExportService {
const worksheet = workbook.addWorksheet('Bookings'); const worksheet = workbook.addWorksheet('Bookings');
// Add header row with styling // Add header row with styling
const headerRow = worksheet.addRow( const headerRow = worksheet.addRow(selectedFields.map(field => this.getFieldLabel(field)));
selectedFields.map((field) => this.getFieldLabel(field)),
);
headerRow.font = { bold: true }; headerRow.font = { bold: true };
headerRow.fill = { headerRow.fill = {
type: 'pattern', type: 'pattern',
@ -102,15 +100,15 @@ export class ExportService {
}; };
// Add data rows // Add data rows
rows.forEach((row) => { rows.forEach(row => {
const values = selectedFields.map((field) => row[field] || ''); const values = selectedFields.map(field => row[field] || '');
worksheet.addRow(values); worksheet.addRow(values);
}); });
// Auto-fit columns // Auto-fit columns
worksheet.columns.forEach((column) => { worksheet.columns.forEach(column => {
let maxLength = 10; let maxLength = 10;
column.eachCell?.({ includeEmpty: false }, (cell) => { column.eachCell?.({ includeEmpty: false }, cell => {
const columnLength = cell.value ? String(cell.value).length : 10; const columnLength = cell.value ? String(cell.value).length : 10;
if (columnLength > maxLength) { if (columnLength > maxLength) {
maxLength = columnLength; maxLength = columnLength;
@ -136,10 +134,10 @@ export class ExportService {
*/ */
private async exportToJSON( private async exportToJSON(
data: BookingExportData[], data: BookingExportData[],
fields?: ExportField[], fields?: ExportField[]
): Promise<{ buffer: Buffer; contentType: string; filename: string }> { ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
const selectedFields = fields || Object.values(ExportField); const selectedFields = fields || Object.values(ExportField);
const rows = data.map((item) => this.extractFields(item, selectedFields)); const rows = data.map(item => this.extractFields(item, selectedFields));
const json = JSON.stringify( const json = JSON.stringify(
{ {
@ -148,7 +146,7 @@ export class ExportService {
bookings: rows, bookings: rows,
}, },
null, null,
2, 2
); );
const buffer = Buffer.from(json, 'utf-8'); const buffer = Buffer.from(json, 'utf-8');
@ -166,14 +164,11 @@ export class ExportService {
/** /**
* Extract specified fields from booking data * Extract specified fields from booking data
*/ */
private extractFields( private extractFields(data: BookingExportData, fields: ExportField[]): Record<string, any> {
data: BookingExportData,
fields: ExportField[],
): Record<string, any> {
const { booking, rateQuote } = data; const { booking, rateQuote } = data;
const result: Record<string, any> = {}; const result: Record<string, any> = {};
fields.forEach((field) => { fields.forEach(field => {
switch (field) { switch (field) {
case ExportField.BOOKING_NUMBER: case ExportField.BOOKING_NUMBER:
result[field] = booking.bookingNumber.value; result[field] = booking.bookingNumber.value;
@ -206,7 +201,7 @@ export class ExportService {
result[field] = booking.consignee.name; result[field] = booking.consignee.name;
break; break;
case ExportField.CONTAINER_TYPE: case ExportField.CONTAINER_TYPE:
result[field] = booking.containers.map((c) => c.type).join(', '); result[field] = booking.containers.map(c => c.type).join(', ');
break; break;
case ExportField.CONTAINER_COUNT: case ExportField.CONTAINER_COUNT:
result[field] = booking.containers.length; result[field] = booking.containers.length;
@ -217,7 +212,8 @@ export class ExportService {
}, 0); }, 0);
break; break;
case ExportField.PRICE: case ExportField.PRICE:
result[field] = `${rateQuote.pricing.currency} ${rateQuote.pricing.totalAmount.toFixed(2)}`; result[field] =
`${rateQuote.pricing.currency} ${rateQuote.pricing.totalAmount.toFixed(2)}`;
break; break;
} }
}); });
@ -253,11 +249,7 @@ export class ExportService {
*/ */
private escapeCSVValue(value: string): string { private escapeCSVValue(value: string): string {
const stringValue = String(value); const stringValue = String(value);
if ( if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
stringValue.includes(',') ||
stringValue.includes('"') ||
stringValue.includes('\n')
) {
return `"${stringValue.replace(/"/g, '""')}"`; return `"${stringValue.replace(/"/g, '""')}"`;
} }
return stringValue; return stringValue;

View File

@ -32,14 +32,14 @@ export class FileValidationService {
// Validate file size // Validate file size
if (file.size > fileUploadConfig.maxFileSize) { if (file.size > fileUploadConfig.maxFileSize) {
errors.push( errors.push(
`File size exceeds maximum allowed size of ${fileUploadConfig.maxFileSize / 1024 / 1024}MB`, `File size exceeds maximum allowed size of ${fileUploadConfig.maxFileSize / 1024 / 1024}MB`
); );
} }
// Validate MIME type // Validate MIME type
if (!fileUploadConfig.allowedMimeTypes.includes(file.mimetype)) { if (!fileUploadConfig.allowedMimeTypes.includes(file.mimetype)) {
errors.push( errors.push(
`File type ${file.mimetype} is not allowed. Allowed types: ${fileUploadConfig.allowedMimeTypes.join(', ')}`, `File type ${file.mimetype} is not allowed. Allowed types: ${fileUploadConfig.allowedMimeTypes.join(', ')}`
); );
} }
@ -47,7 +47,7 @@ export class FileValidationService {
const ext = path.extname(file.originalname).toLowerCase(); const ext = path.extname(file.originalname).toLowerCase();
if (!fileUploadConfig.allowedExtensions.includes(ext)) { if (!fileUploadConfig.allowedExtensions.includes(ext)) {
errors.push( errors.push(
`File extension ${ext} is not allowed. Allowed extensions: ${fileUploadConfig.allowedExtensions.join(', ')}`, `File extension ${ext} is not allowed. Allowed extensions: ${fileUploadConfig.allowedExtensions.join(', ')}`
); );
} }
@ -129,7 +129,7 @@ export class FileValidationService {
]; ];
const lowerFilename = filename.toLowerCase(); const lowerFilename = filename.toLowerCase();
return dangerousExtensions.some((ext) => lowerFilename.includes(ext)); return dangerousExtensions.some(ext => lowerFilename.includes(ext));
} }
/** /**
@ -180,9 +180,7 @@ export class FileValidationService {
// TODO: Integrate with ClamAV or similar virus scanner // TODO: Integrate with ClamAV or similar virus scanner
// For now, just log // For now, just log
this.logger.log( this.logger.log(`Virus scan requested for file: ${file.originalname} (not implemented)`);
`Virus scan requested for file: ${file.originalname} (not implemented)`,
);
return true; return true;
} }
@ -190,9 +188,7 @@ export class FileValidationService {
/** /**
* Validate multiple files * Validate multiple files
*/ */
async validateFiles( async validateFiles(files: Express.Multer.File[]): Promise<FileValidationResult> {
files: Express.Multer.File[],
): Promise<FileValidationResult> {
const allErrors: string[] = []; const allErrors: string[] = [];
for (const file of files) { for (const file of files) {

View File

@ -16,7 +16,7 @@ export class FuzzySearchService {
constructor( constructor(
@InjectRepository(BookingOrmEntity) @InjectRepository(BookingOrmEntity)
private readonly bookingOrmRepository: Repository<BookingOrmEntity>, private readonly bookingOrmRepository: Repository<BookingOrmEntity>
) {} ) {}
/** /**
@ -26,15 +26,13 @@ export class FuzzySearchService {
async fuzzySearchBookings( async fuzzySearchBookings(
searchTerm: string, searchTerm: string,
organizationId: string, organizationId: string,
limit: number = 20, limit: number = 20
): Promise<BookingOrmEntity[]> { ): Promise<BookingOrmEntity[]> {
if (!searchTerm || searchTerm.length < 2) { if (!searchTerm || searchTerm.length < 2) {
return []; return [];
} }
this.logger.log( this.logger.log(`Fuzzy search for "${searchTerm}" in organization ${organizationId}`);
`Fuzzy search for "${searchTerm}" in organization ${organizationId}`,
);
// Use PostgreSQL full-text search with similarity // Use PostgreSQL full-text search with similarity
// This requires pg_trgm extension to be enabled // This requires pg_trgm extension to be enabled
@ -54,7 +52,7 @@ export class FuzzySearchService {
{ {
searchTerm, searchTerm,
likeTerm: `%${searchTerm}%`, likeTerm: `%${searchTerm}%`,
}, }
) )
.orderBy( .orderBy(
`GREATEST( `GREATEST(
@ -62,7 +60,7 @@ export class FuzzySearchService {
similarity(booking.shipper_name, :searchTerm), similarity(booking.shipper_name, :searchTerm),
similarity(booking.consignee_name, :searchTerm) similarity(booking.consignee_name, :searchTerm)
)`, )`,
'DESC', 'DESC'
) )
.setParameter('searchTerm', searchTerm) .setParameter('searchTerm', searchTerm)
.limit(limit) .limit(limit)
@ -80,21 +78,19 @@ export class FuzzySearchService {
async fullTextSearch( async fullTextSearch(
searchTerm: string, searchTerm: string,
organizationId: string, organizationId: string,
limit: number = 20, limit: number = 20
): Promise<BookingOrmEntity[]> { ): Promise<BookingOrmEntity[]> {
if (!searchTerm || searchTerm.length < 2) { if (!searchTerm || searchTerm.length < 2) {
return []; return [];
} }
this.logger.log( this.logger.log(`Full-text search for "${searchTerm}" in organization ${organizationId}`);
`Full-text search for "${searchTerm}" in organization ${organizationId}`,
);
// Convert search term to tsquery format // Convert search term to tsquery format
const tsquery = searchTerm const tsquery = searchTerm
.split(/\s+/) .split(/\s+/)
.filter((term) => term.length > 0) .filter(term => term.length > 0)
.map((term) => `${term}:*`) .map(term => `${term}:*`)
.join(' & '); .join(' & ');
const results = await this.bookingOrmRepository const results = await this.bookingOrmRepository
@ -111,7 +107,7 @@ export class FuzzySearchService {
{ {
tsquery, tsquery,
likeTerm: `%${searchTerm}%`, likeTerm: `%${searchTerm}%`,
}, }
) )
.orderBy('booking.created_at', 'DESC') .orderBy('booking.created_at', 'DESC')
.limit(limit) .limit(limit)
@ -128,7 +124,7 @@ export class FuzzySearchService {
async search( async search(
searchTerm: string, searchTerm: string,
organizationId: string, organizationId: string,
limit: number = 20, limit: number = 20
): Promise<BookingOrmEntity[]> { ): Promise<BookingOrmEntity[]> {
// Try fuzzy search first (more tolerant to typos) // Try fuzzy search first (more tolerant to typos)
let results = await this.fuzzySearchBookings(searchTerm, organizationId, limit); let results = await this.fuzzySearchBookings(searchTerm, organizationId, limit);

View File

@ -31,7 +31,7 @@ export class GDPRService {
constructor( constructor(
@InjectRepository(UserOrmEntity) @InjectRepository(UserOrmEntity)
private readonly userRepository: Repository<UserOrmEntity>, private readonly userRepository: Repository<UserOrmEntity>
) {} ) {}
/** /**
@ -63,7 +63,8 @@ export class GDPRService {
exportDate: new Date().toISOString(), exportDate: new Date().toISOString(),
userId, userId,
userData: sanitizedUser, userData: sanitizedUser,
message: 'User data exported successfully. Additional data (bookings, notifications) can be exported from respective endpoints.', message:
'User data exported successfully. Additional data (bookings, notifications) can be exported from respective endpoints.',
}; };
this.logger.log(`Data export completed for user ${userId}`); this.logger.log(`Data export completed for user ${userId}`);
@ -76,7 +77,9 @@ export class GDPRService {
* Note: This is a simplified version. In production, implement full anonymization logic. * Note: This is a simplified version. In production, implement full anonymization logic.
*/ */
async deleteUserData(userId: string, reason?: string): Promise<void> { async deleteUserData(userId: string, reason?: string): Promise<void> {
this.logger.warn(`Initiating data deletion for user ${userId}. Reason: ${reason || 'User request'}`); this.logger.warn(
`Initiating data deletion for user ${userId}. Reason: ${reason || 'User request'}`
);
// Verify user exists // Verify user exists
const user = await this.userRepository.findOne({ where: { id: userId } }); const user = await this.userRepository.findOne({ where: { id: userId } });
@ -117,7 +120,9 @@ export class GDPRService {
// In production, store in separate consent table // In production, store in separate consent table
// For now, just log the consent // For now, just log the consent
this.logger.log(`Consent recorded: marketing=${consentData.marketing}, analytics=${consentData.analytics}`); this.logger.log(
`Consent recorded: marketing=${consentData.marketing}, analytics=${consentData.analytics}`
);
} }
/** /**

View File

@ -4,8 +4,15 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { NotificationService } from './notification.service'; import { NotificationService } from './notification.service';
import { NOTIFICATION_REPOSITORY, NotificationRepository } from '../../domain/ports/out/notification.repository'; import {
import { Notification, NotificationType, NotificationPriority } from '../../domain/entities/notification.entity'; NOTIFICATION_REPOSITORY,
NotificationRepository,
} from '../../domain/ports/out/notification.repository';
import {
Notification,
NotificationType,
NotificationPriority,
} from '../../domain/entities/notification.entity';
describe('NotificationService', () => { describe('NotificationService', () => {
let service: NotificationService; let service: NotificationService;

View File

@ -34,7 +34,7 @@ export class NotificationService {
constructor( constructor(
@Inject(NOTIFICATION_REPOSITORY) @Inject(NOTIFICATION_REPOSITORY)
private readonly notificationRepository: NotificationRepository, private readonly notificationRepository: NotificationRepository
) {} ) {}
/** /**
@ -50,14 +50,14 @@ export class NotificationService {
await this.notificationRepository.save(notification); await this.notificationRepository.save(notification);
this.logger.log( this.logger.log(
`Notification created: ${input.type} for user ${input.userId} - ${input.title}`, `Notification created: ${input.type} for user ${input.userId} - ${input.title}`
); );
return notification; return notification;
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Failed to create notification: ${error?.message || 'Unknown error'}`, `Failed to create notification: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
throw error; throw error;
} }
@ -147,7 +147,7 @@ export class NotificationService {
userId: string, userId: string,
organizationId: string, organizationId: string,
bookingNumber: string, bookingNumber: string,
bookingId: string, bookingId: string
): Promise<Notification> { ): Promise<Notification> {
return this.createNotification({ return this.createNotification({
userId, userId,
@ -166,7 +166,7 @@ export class NotificationService {
organizationId: string, organizationId: string,
bookingNumber: string, bookingNumber: string,
bookingId: string, bookingId: string,
status: string, status: string
): Promise<Notification> { ): Promise<Notification> {
return this.createNotification({ return this.createNotification({
userId, userId,
@ -184,7 +184,7 @@ export class NotificationService {
userId: string, userId: string,
organizationId: string, organizationId: string,
bookingNumber: string, bookingNumber: string,
bookingId: string, bookingId: string
): Promise<Notification> { ): Promise<Notification> {
return this.createNotification({ return this.createNotification({
userId, userId,
@ -202,7 +202,7 @@ export class NotificationService {
userId: string, userId: string,
organizationId: string, organizationId: string,
documentName: string, documentName: string,
bookingId: string, bookingId: string
): Promise<Notification> { ): Promise<Notification> {
return this.createNotification({ return this.createNotification({
userId, userId,

View File

@ -123,11 +123,9 @@ describe('WebhookService', () => {
of({ status: 200, statusText: 'OK', data: {}, headers: {}, config: {} as any }) of({ status: 200, statusText: 'OK', data: {}, headers: {}, config: {} as any })
); );
await service.triggerWebhooks( await service.triggerWebhooks(WebhookEvent.BOOKING_CREATED, 'org-123', {
WebhookEvent.BOOKING_CREATED, bookingId: 'booking-123',
'org-123', });
{ bookingId: 'booking-123' }
);
expect(httpService.post).toHaveBeenCalledWith( expect(httpService.post).toHaveBeenCalledWith(
'https://example.com/webhook', 'https://example.com/webhook',
@ -151,11 +149,9 @@ describe('WebhookService', () => {
repository.findActiveByEvent.mockResolvedValue([webhook]); repository.findActiveByEvent.mockResolvedValue([webhook]);
httpService.post.mockReturnValue(throwError(() => new Error('Network error'))); httpService.post.mockReturnValue(throwError(() => new Error('Network error')));
await service.triggerWebhooks( await service.triggerWebhooks(WebhookEvent.BOOKING_CREATED, 'org-123', {
WebhookEvent.BOOKING_CREATED, bookingId: 'booking-123',
'org-123', });
{ bookingId: 'booking-123' }
);
// Should be saved as failed after retries // Should be saved as failed after retries
expect(repository.save).toHaveBeenCalledWith( expect(repository.save).toHaveBeenCalledWith(

View File

@ -9,11 +9,7 @@ import { HttpService } from '@nestjs/axios';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { import { Webhook, WebhookEvent, WebhookStatus } from '../../domain/entities/webhook.entity';
Webhook,
WebhookEvent,
WebhookStatus,
} from '../../domain/entities/webhook.entity';
import { import {
WebhookRepository, WebhookRepository,
WEBHOOK_REPOSITORY, WEBHOOK_REPOSITORY,
@ -51,7 +47,7 @@ export class WebhookService {
constructor( constructor(
@Inject(WEBHOOK_REPOSITORY) @Inject(WEBHOOK_REPOSITORY)
private readonly webhookRepository: WebhookRepository, private readonly webhookRepository: WebhookRepository,
private readonly httpService: HttpService, private readonly httpService: HttpService
) {} ) {}
/** /**
@ -72,9 +68,7 @@ export class WebhookService {
await this.webhookRepository.save(webhook); await this.webhookRepository.save(webhook);
this.logger.log( this.logger.log(`Webhook created: ${webhook.id} for organization ${input.organizationId}`);
`Webhook created: ${webhook.id} for organization ${input.organizationId}`,
);
return webhook; return webhook;
} }
@ -158,11 +152,7 @@ export class WebhookService {
/** /**
* Trigger webhooks for an event * Trigger webhooks for an event
*/ */
async triggerWebhooks( async triggerWebhooks(event: WebhookEvent, organizationId: string, data: any): Promise<void> {
event: WebhookEvent,
organizationId: string,
data: any,
): Promise<void> {
try { try {
const webhooks = await this.webhookRepository.findActiveByEvent(event, organizationId); const webhooks = await this.webhookRepository.findActiveByEvent(event, organizationId);
@ -179,17 +169,13 @@ export class WebhookService {
}; };
// Trigger all webhooks in parallel // Trigger all webhooks in parallel
await Promise.allSettled( await Promise.allSettled(webhooks.map(webhook => this.triggerWebhook(webhook, payload)));
webhooks.map((webhook) => this.triggerWebhook(webhook, payload)),
);
this.logger.log( this.logger.log(`Triggered ${webhooks.length} webhooks for event: ${event}`);
`Triggered ${webhooks.length} webhooks for event: ${event}`,
);
} catch (error: any) { } catch (error: any) {
this.logger.error( this.logger.error(
`Error triggering webhooks: ${error?.message || 'Unknown error'}`, `Error triggering webhooks: ${error?.message || 'Unknown error'}`,
error?.stack, error?.stack
); );
} }
} }
@ -197,10 +183,7 @@ export class WebhookService {
/** /**
* Trigger a single webhook with retries * Trigger a single webhook with retries
*/ */
private async triggerWebhook( private async triggerWebhook(webhook: Webhook, payload: WebhookPayload): Promise<void> {
webhook: Webhook,
payload: WebhookPayload,
): Promise<void> {
let lastError: Error | null = null; let lastError: Error | null = null;
for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) { for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) {
@ -226,7 +209,7 @@ export class WebhookService {
this.httpService.post(webhook.url, payload, { this.httpService.post(webhook.url, payload, {
headers, headers,
timeout: 10000, // 10 seconds timeout: 10000, // 10 seconds
}), })
); );
if (response && response.status >= 200 && response.status < 300) { if (response && response.status >= 200 && response.status < 300) {
@ -234,17 +217,17 @@ export class WebhookService {
const updatedWebhook = webhook.recordTrigger(); const updatedWebhook = webhook.recordTrigger();
await this.webhookRepository.save(updatedWebhook); await this.webhookRepository.save(updatedWebhook);
this.logger.log( this.logger.log(`Webhook triggered successfully: ${webhook.id} (attempt ${attempt + 1})`);
`Webhook triggered successfully: ${webhook.id} (attempt ${attempt + 1})`,
);
return; return;
} }
lastError = new Error(`HTTP ${response?.status || 'Unknown'}: ${response?.statusText || 'Unknown error'}`); lastError = new Error(
`HTTP ${response?.status || 'Unknown'}: ${response?.statusText || 'Unknown error'}`
);
} catch (error: any) { } catch (error: any) {
lastError = error; lastError = error;
this.logger.warn( this.logger.warn(
`Webhook trigger attempt ${attempt + 1} failed: ${webhook.id} - ${error?.message}`, `Webhook trigger attempt ${attempt + 1} failed: ${webhook.id} - ${error?.message}`
); );
} }
} }
@ -254,7 +237,7 @@ export class WebhookService {
await this.webhookRepository.save(failedWebhook); await this.webhookRepository.save(failedWebhook);
this.logger.error( this.logger.error(
`Webhook failed after ${this.MAX_RETRIES} attempts: ${webhook.id} - ${lastError?.message}`, `Webhook failed after ${this.MAX_RETRIES} attempts: ${webhook.id} - ${lastError?.message}`
); );
} }
@ -279,16 +262,13 @@ export class WebhookService {
*/ */
verifySignature(payload: any, signature: string, secret: string): boolean { verifySignature(payload: any, signature: string, secret: string): boolean {
const expectedSignature = this.generateSignature(payload, secret); const expectedSignature = this.generateSignature(payload, secret);
return crypto.timingSafeEqual( return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
Buffer.from(signature),
Buffer.from(expectedSignature),
);
} }
/** /**
* Delay helper for retries * Delay helper for retries
*/ */
private delay(ms: number): Promise<void> { private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
} }

View File

@ -174,9 +174,7 @@ export class Booking {
*/ */
updateStatus(newStatus: BookingStatus): Booking { updateStatus(newStatus: BookingStatus): Booking {
if (!this.status.canTransitionTo(newStatus)) { if (!this.status.canTransitionTo(newStatus)) {
throw new Error( throw new Error(`Cannot transition from ${this.status.value} to ${newStatus.value}`);
`Cannot transition from ${this.status.value} to ${newStatus.value}`
);
} }
return new Booking({ return new Booking({
@ -209,7 +207,7 @@ export class Booking {
throw new Error('Cannot modify containers after booking is confirmed'); throw new Error('Cannot modify containers after booking is confirmed');
} }
const containerIndex = this.props.containers.findIndex((c) => c.id === containerId); const containerIndex = this.props.containers.findIndex(c => c.id === containerId);
if (containerIndex === -1) { if (containerIndex === -1) {
throw new Error(`Container ${containerId} not found`); throw new Error(`Container ${containerId} not found`);
} }
@ -237,7 +235,7 @@ export class Booking {
return new Booking({ return new Booking({
...this.props, ...this.props,
containers: this.props.containers.filter((c) => c.id !== containerId), containers: this.props.containers.filter(c => c.id !== containerId),
updatedAt: new Date(), updatedAt: new Date(),
}); });
} }

View File

@ -53,7 +53,9 @@ export class Carrier {
// Validate carrier code // Validate carrier code
if (!Carrier.isValidCarrierCode(props.code)) { if (!Carrier.isValidCarrierCode(props.code)) {
throw new Error('Invalid carrier code format. Must be uppercase letters and underscores only.'); throw new Error(
'Invalid carrier code format. Must be uppercase letters and underscores only.'
);
} }
// Validate API config if carrier supports API // Validate API config if carrier supports API

View File

@ -233,7 +233,10 @@ export class Container {
// Twenty-foot Equivalent Unit // Twenty-foot Equivalent Unit
if (this.props.size === ContainerSize.TWENTY) { if (this.props.size === ContainerSize.TWENTY) {
return 1; return 1;
} else if (this.props.size === ContainerSize.FORTY || this.props.size === ContainerSize.FORTY_FIVE) { } else if (
this.props.size === ContainerSize.FORTY ||
this.props.size === ContainerSize.FORTY_FIVE
) {
return 2; return 2;
} }
return 0; return 0;

View File

@ -55,7 +55,7 @@ export class CsvRate {
public readonly currency: string, // Primary currency (USD or EUR) public readonly currency: string, // Primary currency (USD or EUR)
public readonly surcharges: SurchargeCollection, public readonly surcharges: SurchargeCollection,
public readonly transitDays: number, public readonly transitDays: number,
public readonly validity: DateRange, public readonly validity: DateRange
) { ) {
this.validate(); this.validate();
} }
@ -111,7 +111,7 @@ export class CsvRate {
// Freight class rule: max(volume price, weight price) // Freight class rule: max(volume price, weight price)
const freightPrice = volume.calculateFreightPrice( const freightPrice = volume.calculateFreightPrice(
this.pricing.pricePerCBM, this.pricing.pricePerCBM,
this.pricing.pricePerKG, this.pricing.pricePerKG
); );
// Create Money object in the rate's currency // Create Money object in the rate's currency
@ -138,19 +138,13 @@ export class CsvRate {
// Otherwise, use the pre-calculated base price in target currency // Otherwise, use the pre-calculated base price in target currency
// and recalculate proportionally // and recalculate proportionally
const basePriceInPrimaryCurrency = const basePriceInPrimaryCurrency =
this.currency === 'USD' this.currency === 'USD' ? this.pricing.basePriceUSD : this.pricing.basePriceEUR;
? this.pricing.basePriceUSD
: this.pricing.basePriceEUR;
const basePriceInTargetCurrency = const basePriceInTargetCurrency =
targetCurrency === 'USD' targetCurrency === 'USD' ? this.pricing.basePriceUSD : this.pricing.basePriceEUR;
? this.pricing.basePriceUSD
: this.pricing.basePriceEUR;
// Calculate conversion ratio // Calculate conversion ratio
const ratio = const ratio = basePriceInTargetCurrency.getAmount() / basePriceInPrimaryCurrency.getAmount();
basePriceInTargetCurrency.getAmount() /
basePriceInPrimaryCurrency.getAmount();
// Apply ratio to calculated price // Apply ratio to calculated price
const convertedAmount = price.getAmount() * ratio; const convertedAmount = price.getAmount() * ratio;
@ -179,7 +173,7 @@ export class CsvRate {
this.volumeRange.minCBM, this.volumeRange.minCBM,
this.volumeRange.maxCBM, this.volumeRange.maxCBM,
this.weightRange.minKG, this.weightRange.minKG,
this.weightRange.maxKG, this.weightRange.maxKG
); );
} }

View File

@ -42,7 +42,7 @@ export class Notification {
private constructor(private readonly props: NotificationProps) {} private constructor(private readonly props: NotificationProps) {}
static create( static create(
props: Omit<NotificationProps, 'id' | 'read' | 'createdAt'> & { id: string }, props: Omit<NotificationProps, 'id' | 'read' | 'createdAt'> & { id: string }
): Notification { ): Notification {
return new Notification({ return new Notification({
...props, ...props,

View File

@ -154,8 +154,12 @@ export class Port {
const R = 6371; // Earth's radius in kilometers const R = 6371; // Earth's radius in kilometers
const lat1 = this.toRadians(this.props.coordinates.latitude); const lat1 = this.toRadians(this.props.coordinates.latitude);
const lat2 = this.toRadians(otherPort.coordinates.latitude); const lat2 = this.toRadians(otherPort.coordinates.latitude);
const deltaLat = this.toRadians(otherPort.coordinates.latitude - this.props.coordinates.latitude); const deltaLat = this.toRadians(
const deltaLon = this.toRadians(otherPort.coordinates.longitude - this.props.coordinates.longitude); otherPort.coordinates.latitude - this.props.coordinates.latitude
);
const deltaLon = this.toRadians(
otherPort.coordinates.longitude - this.props.coordinates.longitude
);
const a = const a =
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +

View File

@ -11,10 +11,10 @@
*/ */
export enum UserRole { export enum UserRole {
ADMIN = 'admin', // Full system access ADMIN = 'admin', // Full system access
MANAGER = 'manager', // Manage bookings and users within organization MANAGER = 'manager', // Manage bookings and users within organization
USER = 'user', // Create and view bookings USER = 'user', // Create and view bookings
VIEWER = 'viewer', // Read-only access VIEWER = 'viewer', // Read-only access
} }
export interface UserProps { export interface UserProps {
@ -45,7 +45,10 @@ export class User {
* Factory method to create a new User * Factory method to create a new User
*/ */
static create( static create(
props: Omit<UserProps, 'createdAt' | 'updatedAt' | 'isEmailVerified' | 'isActive' | 'lastLoginAt'> props: Omit<
UserProps,
'createdAt' | 'updatedAt' | 'isEmailVerified' | 'isActive' | 'lastLoginAt'
>
): User { ): User {
const now = new Date(); const now = new Date();

View File

@ -41,7 +41,10 @@ export class Webhook {
private constructor(private readonly props: WebhookProps) {} private constructor(private readonly props: WebhookProps) {}
static create( static create(
props: Omit<WebhookProps, 'id' | 'status' | 'retryCount' | 'failureCount' | 'createdAt' | 'updatedAt'> & { id: string }, props: Omit<
WebhookProps,
'id' | 'status' | 'retryCount' | 'failureCount' | 'createdAt' | 'updatedAt'
> & { id: string }
): Webhook { ): Webhook {
return new Webhook({ return new Webhook({
...props, ...props,

View File

@ -43,9 +43,7 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
// Apply container type filter if specified // Apply container type filter if specified
if (input.containerType) { if (input.containerType) {
const containerType = ContainerType.create(input.containerType); const containerType = ContainerType.create(input.containerType);
matchingRates = matchingRates.filter((rate) => matchingRates = matchingRates.filter(rate => rate.containerType.equals(containerType));
rate.containerType.equals(containerType),
);
} }
// Apply advanced filters // Apply advanced filters
@ -54,7 +52,7 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
} }
// Calculate prices and create results // Calculate prices and create results
const results: CsvRateSearchResult[] = matchingRates.map((rate) => { const results: CsvRateSearchResult[] = matchingRates.map(rate => {
const priceUSD = rate.getPriceInCurrency(volume, 'USD'); const priceUSD = rate.getPriceInCurrency(volume, 'USD');
const priceEUR = rate.getPriceInCurrency(volume, 'EUR'); const priceEUR = rate.getPriceInCurrency(volume, 'EUR');
@ -73,13 +71,9 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
// Sort by price (ascending) in primary currency // Sort by price (ascending) in primary currency
results.sort((a, b) => { results.sort((a, b) => {
const priceA = const priceA =
a.calculatedPrice.primaryCurrency === 'USD' a.calculatedPrice.primaryCurrency === 'USD' ? a.calculatedPrice.usd : a.calculatedPrice.eur;
? a.calculatedPrice.usd
: a.calculatedPrice.eur;
const priceB = const priceB =
b.calculatedPrice.primaryCurrency === 'USD' b.calculatedPrice.primaryCurrency === 'USD' ? b.calculatedPrice.usd : b.calculatedPrice.eur;
? b.calculatedPrice.usd
: b.calculatedPrice.eur;
return priceA - priceB; return priceA - priceB;
}); });
@ -94,13 +88,13 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
async getAvailableCompanies(): Promise<string[]> { async getAvailableCompanies(): Promise<string[]> {
const allRates = await this.loadAllRates(); const allRates = await this.loadAllRates();
const companies = new Set(allRates.map((rate) => rate.companyName)); const companies = new Set(allRates.map(rate => rate.companyName));
return Array.from(companies).sort(); return Array.from(companies).sort();
} }
async getAvailableContainerTypes(): Promise<string[]> { async getAvailableContainerTypes(): Promise<string[]> {
const allRates = await this.loadAllRates(); const allRates = await this.loadAllRates();
const types = new Set(allRates.map((rate) => rate.containerType.getValue())); const types = new Set(allRates.map(rate => rate.containerType.getValue()));
return Array.from(types).sort(); return Array.from(types).sort();
} }
@ -109,9 +103,7 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
*/ */
private async loadAllRates(): Promise<CsvRate[]> { private async loadAllRates(): Promise<CsvRate[]> {
const files = await this.csvRateLoader.getAvailableCsvFiles(); const files = await this.csvRateLoader.getAvailableCsvFiles();
const ratePromises = files.map((file) => const ratePromises = files.map(file => this.csvRateLoader.loadRatesFromCsv(file));
this.csvRateLoader.loadRatesFromCsv(file),
);
const rateArrays = await Promise.all(ratePromises); const rateArrays = await Promise.all(ratePromises);
return rateArrays.flat(); return rateArrays.flat();
} }
@ -119,26 +111,22 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
/** /**
* Filter rates by route (origin/destination) * Filter rates by route (origin/destination)
*/ */
private filterByRoute( private filterByRoute(rates: CsvRate[], origin: PortCode, destination: PortCode): CsvRate[] {
rates: CsvRate[], return rates.filter(rate => rate.matchesRoute(origin, destination));
origin: PortCode,
destination: PortCode,
): CsvRate[] {
return rates.filter((rate) => rate.matchesRoute(origin, destination));
} }
/** /**
* Filter rates by volume/weight range * Filter rates by volume/weight range
*/ */
private filterByVolume(rates: CsvRate[], volume: Volume): CsvRate[] { private filterByVolume(rates: CsvRate[], volume: Volume): CsvRate[] {
return rates.filter((rate) => rate.matchesVolume(volume)); return rates.filter(rate => rate.matchesVolume(volume));
} }
/** /**
* Filter rates by pallet count * Filter rates by pallet count
*/ */
private filterByPalletCount(rates: CsvRate[], palletCount: number): CsvRate[] { private filterByPalletCount(rates: CsvRate[], palletCount: number): CsvRate[] {
return rates.filter((rate) => rate.matchesPalletCount(palletCount)); return rates.filter(rate => rate.matchesPalletCount(palletCount));
} }
/** /**
@ -147,52 +135,40 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
private applyAdvancedFilters( private applyAdvancedFilters(
rates: CsvRate[], rates: CsvRate[],
filters: RateSearchFilters, filters: RateSearchFilters,
volume: Volume, volume: Volume
): CsvRate[] { ): CsvRate[] {
let filtered = rates; let filtered = rates;
// Company filter // Company filter
if (filters.companies && filters.companies.length > 0) { if (filters.companies && filters.companies.length > 0) {
filtered = filtered.filter((rate) => filtered = filtered.filter(rate => filters.companies!.includes(rate.companyName));
filters.companies!.includes(rate.companyName),
);
} }
// Volume CBM filter // Volume CBM filter
if (filters.minVolumeCBM !== undefined) { if (filters.minVolumeCBM !== undefined) {
filtered = filtered.filter( filtered = filtered.filter(rate => rate.volumeRange.maxCBM >= filters.minVolumeCBM!);
(rate) => rate.volumeRange.maxCBM >= filters.minVolumeCBM!,
);
} }
if (filters.maxVolumeCBM !== undefined) { if (filters.maxVolumeCBM !== undefined) {
filtered = filtered.filter( filtered = filtered.filter(rate => rate.volumeRange.minCBM <= filters.maxVolumeCBM!);
(rate) => rate.volumeRange.minCBM <= filters.maxVolumeCBM!,
);
} }
// Weight KG filter // Weight KG filter
if (filters.minWeightKG !== undefined) { if (filters.minWeightKG !== undefined) {
filtered = filtered.filter( filtered = filtered.filter(rate => rate.weightRange.maxKG >= filters.minWeightKG!);
(rate) => rate.weightRange.maxKG >= filters.minWeightKG!,
);
} }
if (filters.maxWeightKG !== undefined) { if (filters.maxWeightKG !== undefined) {
filtered = filtered.filter( filtered = filtered.filter(rate => rate.weightRange.minKG <= filters.maxWeightKG!);
(rate) => rate.weightRange.minKG <= filters.maxWeightKG!,
);
} }
// Pallet count filter // Pallet count filter
if (filters.palletCount !== undefined) { if (filters.palletCount !== undefined) {
filtered = filtered.filter((rate) => filtered = filtered.filter(rate => rate.matchesPalletCount(filters.palletCount!));
rate.matchesPalletCount(filters.palletCount!),
);
} }
// Price filter (calculate price first) // Price filter (calculate price first)
if (filters.minPrice !== undefined || filters.maxPrice !== undefined) { if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
const currency = filters.currency || 'USD'; const currency = filters.currency || 'USD';
filtered = filtered.filter((rate) => { filtered = filtered.filter(rate => {
const price = rate.getPriceInCurrency(volume, currency); const price = rate.getPriceInCurrency(volume, currency);
const amount = price.getAmount(); const amount = price.getAmount();
@ -208,33 +184,27 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
// Transit days filter // Transit days filter
if (filters.minTransitDays !== undefined) { if (filters.minTransitDays !== undefined) {
filtered = filtered.filter( filtered = filtered.filter(rate => rate.transitDays >= filters.minTransitDays!);
(rate) => rate.transitDays >= filters.minTransitDays!,
);
} }
if (filters.maxTransitDays !== undefined) { if (filters.maxTransitDays !== undefined) {
filtered = filtered.filter( filtered = filtered.filter(rate => rate.transitDays <= filters.maxTransitDays!);
(rate) => rate.transitDays <= filters.maxTransitDays!,
);
} }
// Container type filter // Container type filter
if (filters.containerTypes && filters.containerTypes.length > 0) { if (filters.containerTypes && filters.containerTypes.length > 0) {
filtered = filtered.filter((rate) => filtered = filtered.filter(rate =>
filters.containerTypes!.includes(rate.containerType.getValue()), filters.containerTypes!.includes(rate.containerType.getValue())
); );
} }
// All-in prices only filter // All-in prices only filter
if (filters.onlyAllInPrices) { if (filters.onlyAllInPrices) {
filtered = filtered.filter((rate) => rate.isAllInPrice()); filtered = filtered.filter(rate => rate.isAllInPrice());
} }
// Departure date / validity filter // Departure date / validity filter
if (filters.departureDate) { if (filters.departureDate) {
filtered = filtered.filter((rate) => filtered = filtered.filter(rate => rate.isValidForDate(filters.departureDate!));
rate.isValidForDate(filters.departureDate!),
);
} }
return filtered; return filtered;
@ -244,10 +214,7 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
* Calculate match score (0-100) based on how well rate matches input * Calculate match score (0-100) based on how well rate matches input
* Higher score = better match * Higher score = better match
*/ */
private calculateMatchScore( private calculateMatchScore(rate: CsvRate, input: CsvRateSearchInput): number {
rate: CsvRate,
input: CsvRateSearchInput,
): number {
let score = 100; let score = 100;
// Reduce score if volume/weight is near boundaries // Reduce score if volume/weight is near boundaries
@ -270,8 +237,7 @@ export class CsvRateSearchService implements SearchCsvRatesPort {
// Reduce score for rates expiring soon // Reduce score for rates expiring soon
const daysUntilExpiry = Math.floor( const daysUntilExpiry = Math.floor(
(rate.validity.getEndDate().getTime() - Date.now()) / (rate.validity.getEndDate().getTime() - Date.now()) / (1000 * 60 * 60 * 24)
(1000 * 60 * 60 * 24),
); );
if (daysUntilExpiry < 7) { if (daysUntilExpiry < 7) {
score -= 10; score -= 10;

View File

@ -10,7 +10,12 @@
*/ */
import { Port } from '../entities/port.entity'; import { Port } from '../entities/port.entity';
import { GetPortsPort, PortSearchInput, PortSearchOutput, GetPortInput } from '../ports/in/get-ports.port'; import {
GetPortsPort,
PortSearchInput,
PortSearchOutput,
GetPortInput,
} from '../ports/in/get-ports.port';
import { PortRepository } from '../ports/out/port.repository'; import { PortRepository } from '../ports/out/port.repository';
import { PortNotFoundException } from '../exceptions/port-not-found.exception'; import { PortNotFoundException } from '../exceptions/port-not-found.exception';
@ -53,8 +58,8 @@ export class PortSearchService implements GetPortsPort {
const ports = await this.portRepository.findByCodes(portCodes); const ports = await this.portRepository.findByCodes(portCodes);
// Check if all ports were found // Check if all ports were found
const foundCodes = ports.map((p) => p.code); const foundCodes = ports.map(p => p.code);
const missingCodes = portCodes.filter((code) => !foundCodes.includes(code)); const missingCodes = portCodes.filter(code => !foundCodes.includes(code));
if (missingCodes.length > 0) { if (missingCodes.length > 0) {
throw new PortNotFoundException(missingCodes[0]); throw new PortNotFoundException(missingCodes[0]);

View File

@ -52,7 +52,7 @@ export class RateSearchService implements SearchRatesPort {
// Query all carriers in parallel with Promise.allSettled // Query all carriers in parallel with Promise.allSettled
const carrierResults = await Promise.allSettled( const carrierResults = await Promise.allSettled(
connectorsToQuery.map((connector) => this.queryCarrier(connector, input)) connectorsToQuery.map(connector => this.queryCarrier(connector, input))
); );
// Process results // Process results
@ -140,7 +140,7 @@ export class RateSearchService implements SearchRatesPort {
return this.carrierConnectors; return this.carrierConnectors;
} }
return this.carrierConnectors.filter((connector) => return this.carrierConnectors.filter(connector =>
carrierPreferences.includes(connector.getCarrierCode()) carrierPreferences.includes(connector.getCarrierCode())
); );
} }

View File

@ -73,9 +73,7 @@ export class BookingStatus {
*/ */
transitionTo(newStatus: BookingStatus): BookingStatus { transitionTo(newStatus: BookingStatus): BookingStatus {
if (!this.canTransitionTo(newStatus)) { if (!this.canTransitionTo(newStatus)) {
throw new Error( throw new Error(`Invalid status transition from ${this._value} to ${newStatus._value}`);
`Invalid status transition from ${this._value} to ${newStatus._value}`
);
} }
return newStatus; return newStatus;
} }

View File

@ -76,9 +76,7 @@ export class DateRange {
} }
overlaps(other: DateRange): boolean { overlaps(other: DateRange): boolean {
return ( return this.startDate <= other.endDate && this.endDate >= other.startDate;
this.startDate <= other.endDate && this.endDate >= other.startDate
);
} }
isFutureRange(): boolean { isFutureRange(): boolean {

View File

@ -20,7 +20,7 @@ export class Surcharge {
constructor( constructor(
public readonly type: SurchargeType, public readonly type: SurchargeType,
public readonly amount: Money, public readonly amount: Money,
public readonly description?: string, public readonly description?: string
) { ) {
this.validate(); this.validate();
} }
@ -46,10 +46,7 @@ export class Surcharge {
} }
equals(other: Surcharge): boolean { equals(other: Surcharge): boolean {
return ( return this.type === other.type && this.amount.isEqualTo(other.amount);
this.type === other.type &&
this.amount.isEqualTo(other.amount)
);
} }
toString(): string { toString(): string {
@ -70,15 +67,16 @@ export class SurchargeCollection {
* In production, currency conversion would be needed * In production, currency conversion would be needed
*/ */
getTotalAmount(currency: string): Money { getTotalAmount(currency: string): Money {
const relevantSurcharges = this.surcharges const relevantSurcharges = this.surcharges.filter(s => s.amount.getCurrency() === currency);
.filter((s) => s.amount.getCurrency() === currency);
if (relevantSurcharges.length === 0) { if (relevantSurcharges.length === 0) {
return Money.zero(currency); return Money.zero(currency);
} }
return relevantSurcharges return relevantSurcharges.reduce(
.reduce((total, surcharge) => total.add(surcharge.amount), Money.zero(currency)); (total, surcharge) => total.add(surcharge.amount),
Money.zero(currency)
);
} }
/** /**
@ -92,7 +90,7 @@ export class SurchargeCollection {
* Get surcharges by type * Get surcharges by type
*/ */
getByType(type: SurchargeType): Surcharge[] { getByType(type: SurchargeType): Surcharge[] {
return this.surcharges.filter((s) => s.type === type); return this.surcharges.filter(s => s.type === type);
} }
/** /**
@ -102,6 +100,6 @@ export class SurchargeCollection {
if (this.isEmpty()) { if (this.isEmpty()) {
return 'All-in price (no separate surcharges)'; return 'All-in price (no separate surcharges)';
} }
return this.surcharges.map((s) => s.toString()).join(', '); return this.surcharges.map(s => s.toString()).join(', ');
} }
} }

View File

@ -8,7 +8,7 @@
export class Volume { export class Volume {
constructor( constructor(
public readonly cbm: number, public readonly cbm: number,
public readonly weightKG: number, public readonly weightKG: number
) { ) {
this.validate(); this.validate();
} }

View File

@ -31,7 +31,7 @@ export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestr
port, port,
password, password,
db, db,
retryStrategy: (times) => { retryStrategy: times => {
const delay = Math.min(times * 50, 2000); const delay = Math.min(times * 50, 2000);
return delay; return delay;
}, },
@ -42,7 +42,7 @@ export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestr
this.logger.log(`Connected to Redis at ${host}:${port}`); this.logger.log(`Connected to Redis at ${host}:${port}`);
}); });
this.client.on('error', (err) => { this.client.on('error', err => {
this.logger.error(`Redis connection error: ${err.message}`); this.logger.error(`Redis connection error: ${err.message}`);
}); });
@ -112,7 +112,9 @@ export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestr
const result = await this.client.exists(key); const result = await this.client.exists(key);
return result === 1; return result === 1;
} catch (error: any) { } catch (error: any) {
this.logger.error(`Error checking key existence ${key}: ${error?.message || 'Unknown error'}`); this.logger.error(
`Error checking key existence ${key}: ${error?.message || 'Unknown error'}`
);
return false; return false;
} }
} }

View File

@ -44,15 +44,9 @@ import { ONERequestMapper } from './one/one.mapper';
mscConnector: MSCConnectorAdapter, mscConnector: MSCConnectorAdapter,
cmacgmConnector: CMACGMConnectorAdapter, cmacgmConnector: CMACGMConnectorAdapter,
hapagConnector: HapagLloydConnectorAdapter, hapagConnector: HapagLloydConnectorAdapter,
oneConnector: ONEConnectorAdapter, oneConnector: ONEConnectorAdapter
) => { ) => {
return [ return [maerskConnector, mscConnector, cmacgmConnector, hapagConnector, oneConnector];
maerskConnector,
mscConnector,
cmacgmConnector,
hapagConnector,
oneConnector,
];
}, },
inject: [ inject: [
MaerskConnector, MaerskConnector,

View File

@ -9,24 +9,21 @@ import { ConfigService } from '@nestjs/config';
import { import {
CarrierConnectorPort, CarrierConnectorPort,
CarrierRateSearchInput, CarrierRateSearchInput,
CarrierAvailabilityInput CarrierAvailabilityInput,
} from '../../../domain/ports/out/carrier-connector.port'; } from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote } from '../../../domain/entities/rate-quote.entity'; import { RateQuote } from '../../../domain/entities/rate-quote.entity';
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector'; import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
import { CMACGMRequestMapper } from './cma-cgm.mapper'; import { CMACGMRequestMapper } from './cma-cgm.mapper';
@Injectable() @Injectable()
export class CMACGMConnectorAdapter export class CMACGMConnectorAdapter extends BaseCarrierConnector implements CarrierConnectorPort {
extends BaseCarrierConnector
implements CarrierConnectorPort
{
private readonly apiUrl: string; private readonly apiUrl: string;
private readonly clientId: string; private readonly clientId: string;
private readonly clientSecret: string; private readonly clientSecret: string;
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly requestMapper: CMACGMRequestMapper, private readonly requestMapper: CMACGMRequestMapper
) { ) {
const config: CarrierConfig = { const config: CarrierConfig = {
name: 'CMA CGM', name: 'CMA CGM',

View File

@ -30,11 +30,31 @@ export class CMACGMRequestMapper {
return cgmResponse.quotations.map((quotation: any) => { return cgmResponse.quotations.map((quotation: any) => {
const surcharges: Surcharge[] = [ const surcharges: Surcharge[] = [
{ type: 'BAF', description: 'Bunker Surcharge', amount: quotation.charges?.bunker_surcharge || 0, currency: quotation.charges?.currency || 'USD' }, {
{ type: 'CAF', description: 'Currency Surcharge', amount: quotation.charges?.currency_surcharge || 0, currency: quotation.charges?.currency || 'USD' }, type: 'BAF',
{ type: 'PSS', description: 'Peak Season', amount: quotation.charges?.peak_season || 0, currency: quotation.charges?.currency || 'USD' }, description: 'Bunker Surcharge',
{ type: 'THC', description: 'Terminal Handling', amount: quotation.charges?.thc || 0, currency: quotation.charges?.currency || 'USD' }, amount: quotation.charges?.bunker_surcharge || 0,
].filter((s) => s.amount > 0); currency: quotation.charges?.currency || 'USD',
},
{
type: 'CAF',
description: 'Currency Surcharge',
amount: quotation.charges?.currency_surcharge || 0,
currency: quotation.charges?.currency || 'USD',
},
{
type: 'PSS',
description: 'Peak Season',
amount: quotation.charges?.peak_season || 0,
currency: quotation.charges?.currency || 'USD',
},
{
type: 'THC',
description: 'Terminal Handling',
amount: quotation.charges?.thc || 0,
currency: quotation.charges?.currency || 'USD',
},
].filter(s => s.amount > 0);
const baseFreight = quotation.charges?.ocean_freight || 0; const baseFreight = quotation.charges?.ocean_freight || 0;
const totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0); const totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0);
@ -53,7 +73,10 @@ export class CMACGMRequestMapper {
}); });
// Transshipment ports // Transshipment ports
if (quotation.routing?.transshipment_ports && Array.isArray(quotation.routing.transshipment_ports)) { if (
quotation.routing?.transshipment_ports &&
Array.isArray(quotation.routing.transshipment_ports)
) {
quotation.routing.transshipment_ports.forEach((port: any) => { quotation.routing.transshipment_ports.forEach((port: any) => {
route.push({ route.push({
portCode: port.code || port, portCode: port.code || port,
@ -69,7 +92,12 @@ export class CMACGMRequestMapper {
arrival: new Date(quotation.schedule?.arrival_date), arrival: new Date(quotation.schedule?.arrival_date),
}); });
const transitDays = quotation.schedule?.transit_time_days || this.calculateTransitDays(quotation.schedule?.departure_date, quotation.schedule?.arrival_date); const transitDays =
quotation.schedule?.transit_time_days ||
this.calculateTransitDays(
quotation.schedule?.departure_date,
quotation.schedule?.arrival_date
);
return RateQuote.create({ return RateQuote.create({
id: uuidv4(), id: uuidv4(),

View File

@ -65,14 +65,7 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
constructor() { constructor() {
// CSV files are stored in infrastructure/storage/csv-storage/rates/ // CSV files are stored in infrastructure/storage/csv-storage/rates/
this.csvDirectory = path.join( this.csvDirectory = path.join(__dirname, '..', '..', 'storage', 'csv-storage', 'rates');
__dirname,
'..',
'..',
'storage',
'csv-storage',
'rates',
);
} }
async loadRatesFromCsv(filePath: string): Promise<CsvRate[]> { async loadRatesFromCsv(filePath: string): Promise<CsvRate[]> {
@ -104,12 +97,8 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
return this.mapToCsvRate(record); return this.mapToCsvRate(record);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error( this.logger.error(`Error mapping row ${index + 1} in ${filePath}: ${errorMessage}`);
`Error mapping row ${index + 1} in ${filePath}: ${errorMessage}`, throw new Error(`Invalid data in row ${index + 1} of ${filePath}: ${errorMessage}`);
);
throw new Error(
`Invalid data in row ${index + 1} of ${filePath}: ${errorMessage}`,
);
} }
}); });
@ -134,7 +123,7 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
} }
async validateCsvFile( async validateCsvFile(
filePath: string, filePath: string
): Promise<{ valid: boolean; errors: string[]; rowCount?: number }> { ): Promise<{ valid: boolean; errors: string[]; rowCount?: number }> {
const errors: string[] = []; const errors: string[] = [];
@ -205,7 +194,7 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
} }
const files = await fs.readdir(this.csvDirectory); const files = await fs.readdir(this.csvDirectory);
return files.filter((file) => file.endsWith('.csv')); return files.filter(file => file.endsWith('.csv'));
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to list CSV files: ${errorMessage}`); this.logger.error(`Failed to list CSV files: ${errorMessage}`);
@ -243,14 +232,10 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
} }
const firstRecord = records[0]; const firstRecord = records[0];
const missingColumns = requiredColumns.filter( const missingColumns = requiredColumns.filter(col => !(col in firstRecord));
(col) => !(col in firstRecord),
);
if (missingColumns.length > 0) { if (missingColumns.length > 0) {
throw new Error( throw new Error(`Missing required columns: ${missingColumns.join(', ')}`);
`Missing required columns: ${missingColumns.join(', ')}`,
);
} }
} }
@ -284,19 +269,13 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
{ {
pricePerCBM: parseFloat(record.pricePerCBM), pricePerCBM: parseFloat(record.pricePerCBM),
pricePerKG: parseFloat(record.pricePerKG), pricePerKG: parseFloat(record.pricePerKG),
basePriceUSD: Money.create( basePriceUSD: Money.create(parseFloat(record.basePriceUSD), 'USD'),
parseFloat(record.basePriceUSD), basePriceEUR: Money.create(parseFloat(record.basePriceEUR), 'EUR'),
'USD',
),
basePriceEUR: Money.create(
parseFloat(record.basePriceEUR),
'EUR',
),
}, },
record.currency.toUpperCase(), record.currency.toUpperCase(),
new SurchargeCollection(surcharges), new SurchargeCollection(surcharges),
parseInt(record.transitDays, 10), parseInt(record.transitDays, 10),
validity, validity
); );
} }
@ -319,8 +298,8 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
new Surcharge( new Surcharge(
SurchargeType.BAF, SurchargeType.BAF,
Money.create(parseFloat(record.surchargeBAF), currency), Money.create(parseFloat(record.surchargeBAF), currency),
'Bunker Adjustment Factor', 'Bunker Adjustment Factor'
), )
); );
} }
@ -330,8 +309,8 @@ export class CsvRateLoaderAdapter implements CsvRateLoaderPort {
new Surcharge( new Surcharge(
SurchargeType.CAF, SurchargeType.CAF,
Money.create(parseFloat(record.surchargeCAF), currency), Money.create(parseFloat(record.surchargeCAF), currency),
'Currency Adjustment Factor', 'Currency Adjustment Factor'
), )
); );
} }

View File

@ -9,7 +9,7 @@ import { ConfigService } from '@nestjs/config';
import { import {
CarrierConnectorPort, CarrierConnectorPort,
CarrierRateSearchInput, CarrierRateSearchInput,
CarrierAvailabilityInput CarrierAvailabilityInput,
} from '../../../domain/ports/out/carrier-connector.port'; } from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote } from '../../../domain/entities/rate-quote.entity'; import { RateQuote } from '../../../domain/entities/rate-quote.entity';
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector'; import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
@ -25,7 +25,7 @@ export class HapagLloydConnectorAdapter
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly requestMapper: HapagLloydRequestMapper, private readonly requestMapper: HapagLloydRequestMapper
) { ) {
const config: CarrierConfig = { const config: CarrierConfig = {
name: 'Hapag-Lloyd', name: 'Hapag-Lloyd',
@ -91,7 +91,9 @@ export class HapagLloydConnectorAdapter
return (response.data as any).available_capacity || 0; return (response.data as any).available_capacity || 0;
} catch (error: any) { } catch (error: any) {
this.logger.error(`Hapag-Lloyd availability check error: ${error?.message || 'Unknown error'}`); this.logger.error(
`Hapag-Lloyd availability check error: ${error?.message || 'Unknown error'}`
);
return 0; return 0;
} }
} }

View File

@ -91,7 +91,12 @@ export class HapagLloydRequestMapper {
arrival: new Date(quote.estimated_time_of_arrival), arrival: new Date(quote.estimated_time_of_arrival),
}); });
const transitDays = quote.transit_time_days || this.calculateTransitDays(quote.estimated_time_of_departure, quote.estimated_time_of_arrival); const transitDays =
quote.transit_time_days ||
this.calculateTransitDays(
quote.estimated_time_of_departure,
quote.estimated_time_of_arrival
);
return RateQuote.create({ return RateQuote.create({
id: uuidv4(), id: uuidv4(),

View File

@ -17,7 +17,7 @@ export class MaerskResponseMapper {
originCode: string, originCode: string,
destinationCode: string destinationCode: string
): RateQuote[] { ): RateQuote[] {
return response.results.map((result) => this.toRateQuote(result, originCode, destinationCode)); return response.results.map(result => this.toRateQuote(result, originCode, destinationCode));
} }
/** /**
@ -28,16 +28,14 @@ export class MaerskResponseMapper {
originCode: string, originCode: string,
destinationCode: string destinationCode: string
): RateQuote { ): RateQuote {
const surcharges = result.pricing.charges.map((charge) => ({ const surcharges = result.pricing.charges.map(charge => ({
type: charge.chargeCode, type: charge.chargeCode,
description: charge.chargeName, description: charge.chargeName,
amount: charge.amount, amount: charge.amount,
currency: charge.currency, currency: charge.currency,
})); }));
const route = result.schedule.routeSchedule.map((segment) => const route = result.schedule.routeSchedule.map(segment => this.mapRouteSegment(segment));
this.mapRouteSegment(segment)
);
return RateQuote.create({ return RateQuote.create({
id: uuidv4(), id: uuidv4(),

View File

@ -9,23 +9,20 @@ import { ConfigService } from '@nestjs/config';
import { import {
CarrierConnectorPort, CarrierConnectorPort,
CarrierRateSearchInput, CarrierRateSearchInput,
CarrierAvailabilityInput CarrierAvailabilityInput,
} from '../../../domain/ports/out/carrier-connector.port'; } from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote } from '../../../domain/entities/rate-quote.entity'; import { RateQuote } from '../../../domain/entities/rate-quote.entity';
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector'; import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
import { MSCRequestMapper } from './msc.mapper'; import { MSCRequestMapper } from './msc.mapper';
@Injectable() @Injectable()
export class MSCConnectorAdapter export class MSCConnectorAdapter extends BaseCarrierConnector implements CarrierConnectorPort {
extends BaseCarrierConnector
implements CarrierConnectorPort
{
private readonly apiUrl: string; private readonly apiUrl: string;
private readonly apiKey: string; private readonly apiKey: string;
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly requestMapper: MSCRequestMapper, private readonly requestMapper: MSCRequestMapper
) { ) {
const config: CarrierConfig = { const config: CarrierConfig = {
name: 'MSC', name: 'MSC',

View File

@ -58,7 +58,7 @@ export class MSCRequestMapper {
amount: quote.surcharges?.pss || 0, amount: quote.surcharges?.pss || 0,
currency: quote.currency || 'USD', currency: quote.currency || 'USD',
}, },
].filter((s) => s.amount > 0); ].filter(s => s.amount > 0);
const totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0); const totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0);
const baseFreight = quote.ocean_freight || 0; const baseFreight = quote.ocean_freight || 0;

View File

@ -9,24 +9,21 @@ import { ConfigService } from '@nestjs/config';
import { import {
CarrierConnectorPort, CarrierConnectorPort,
CarrierRateSearchInput, CarrierRateSearchInput,
CarrierAvailabilityInput CarrierAvailabilityInput,
} from '../../../domain/ports/out/carrier-connector.port'; } from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote } from '../../../domain/entities/rate-quote.entity'; import { RateQuote } from '../../../domain/entities/rate-quote.entity';
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector'; import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
import { ONERequestMapper } from './one.mapper'; import { ONERequestMapper } from './one.mapper';
@Injectable() @Injectable()
export class ONEConnectorAdapter export class ONEConnectorAdapter extends BaseCarrierConnector implements CarrierConnectorPort {
extends BaseCarrierConnector
implements CarrierConnectorPort
{
private readonly apiUrl: string; private readonly apiUrl: string;
private readonly username: string; private readonly username: string;
private readonly password: string; private readonly password: string;
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly requestMapper: ONERequestMapper, private readonly requestMapper: ONERequestMapper
) { ) {
const config: CarrierConfig = { const config: CarrierConfig = {
name: 'ONE', name: 'ONE',

View File

@ -78,7 +78,8 @@ export class ONERequestMapper {
arrival: new Date(quote.arrival_date), arrival: new Date(quote.arrival_date),
}); });
const transitDays = quote.transit_days || this.calculateTransitDays(quote.departure_date, quote.arrival_date); const transitDays =
quote.transit_days || this.calculateTransitDays(quote.departure_date, quote.arrival_date);
return RateQuote.create({ return RateQuote.create({
id: uuidv4(), id: uuidv4(),
@ -130,7 +131,7 @@ export class ONERequestMapper {
private formatChargeName(key: string): string { private formatChargeName(key: string): string {
return key return key
.split('_') .split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' '); .join(' ');
} }

View File

@ -7,10 +7,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import { import { EmailPort, EmailOptions } from '../../domain/ports/out/email.port';
EmailPort,
EmailOptions,
} from '../../domain/ports/out/email.port';
import { EmailTemplates } from './templates/email-templates'; import { EmailTemplates } from './templates/email-templates';
@Injectable() @Injectable()
@ -39,17 +36,12 @@ export class EmailAdapter implements EmailPort {
auth: user && pass ? { user, pass } : undefined, auth: user && pass ? { user, pass } : undefined,
}); });
this.logger.log( this.logger.log(`Email adapter initialized with SMTP host: ${host}:${port}`);
`Email adapter initialized with SMTP host: ${host}:${port}`
);
} }
async send(options: EmailOptions): Promise<void> { async send(options: EmailOptions): Promise<void> {
try { try {
const from = this.configService.get<string>( const from = this.configService.get<string>('SMTP_FROM', 'noreply@xpeditis.com');
'SMTP_FROM',
'noreply@xpeditis.com'
);
await this.transporter.sendMail({ await this.transporter.sendMail({
from, from,

View File

@ -155,10 +155,7 @@ export class EmailTemplates {
/** /**
* Render welcome email * Render welcome email
*/ */
async renderWelcomeEmail(data: { async renderWelcomeEmail(data: { firstName: string; dashboardUrl: string }): Promise<string> {
firstName: string;
dashboardUrl: string;
}): Promise<string> {
const mjmlTemplate = ` const mjmlTemplate = `
<mjml> <mjml>
<mj-head> <mj-head>

View File

@ -23,9 +23,7 @@ export function initializeSentry(config: SentryConfig): void {
Sentry.init({ Sentry.init({
dsn: config.dsn, dsn: config.dsn,
environment: config.environment, environment: config.environment,
integrations: [ integrations: [nodeProfilingIntegration()],
nodeProfilingIntegration(),
],
// Performance Monitoring // Performance Monitoring
tracesSampleRate: config.tracesSampleRate, tracesSampleRate: config.tracesSampleRate,
// Profiling // Profiling
@ -58,9 +56,7 @@ export function initializeSentry(config: SentryConfig): void {
maxBreadcrumbs: 50, maxBreadcrumbs: 50,
}); });
console.log( console.log(`✅ Sentry monitoring initialized for ${config.environment} environment`);
`✅ Sentry monitoring initialized for ${config.environment} environment`,
);
} }
/** /**
@ -68,7 +64,7 @@ export function initializeSentry(config: SentryConfig): void {
*/ */
export function captureException(error: Error, context?: Record<string, any>) { export function captureException(error: Error, context?: Record<string, any>) {
if (context) { if (context) {
Sentry.withScope((scope) => { Sentry.withScope(scope => {
Object.entries(context).forEach(([key, value]) => { Object.entries(context).forEach(([key, value]) => {
scope.setExtra(key, value); scope.setExtra(key, value);
}); });
@ -85,10 +81,10 @@ export function captureException(error: Error, context?: Record<string, any>) {
export function captureMessage( export function captureMessage(
message: string, message: string,
level: Sentry.SeverityLevel = 'info', level: Sentry.SeverityLevel = 'info',
context?: Record<string, any>, context?: Record<string, any>
) { ) {
if (context) { if (context) {
Sentry.withScope((scope) => { Sentry.withScope(scope => {
Object.entries(context).forEach(([key, value]) => { Object.entries(context).forEach(([key, value]) => {
scope.setExtra(key, value); scope.setExtra(key, value);
}); });
@ -106,7 +102,7 @@ export function addBreadcrumb(
category: string, category: string,
message: string, message: string,
data?: Record<string, any>, data?: Record<string, any>,
level: Sentry.SeverityLevel = 'info', level: Sentry.SeverityLevel = 'info'
) { ) {
Sentry.addBreadcrumb({ Sentry.addBreadcrumb({
category, category,

View File

@ -24,17 +24,12 @@ export class PdfAdapter implements PdfPort {
doc.on('data', buffers.push.bind(buffers)); doc.on('data', buffers.push.bind(buffers));
doc.on('end', () => { doc.on('end', () => {
const pdfBuffer = Buffer.concat(buffers); const pdfBuffer = Buffer.concat(buffers);
this.logger.log( this.logger.log(`Generated booking confirmation PDF for ${data.bookingNumber}`);
`Generated booking confirmation PDF for ${data.bookingNumber}`
);
resolve(pdfBuffer); resolve(pdfBuffer);
}); });
// Header // Header
doc doc.fontSize(24).fillColor('#0066cc').text('BOOKING CONFIRMATION', { align: 'center' });
.fontSize(24)
.fillColor('#0066cc')
.text('BOOKING CONFIRMATION', { align: 'center' });
doc.moveDown(); doc.moveDown();
@ -60,9 +55,7 @@ export class PdfAdapter implements PdfPort {
doc.fontSize(12).fillColor('#333333'); doc.fontSize(12).fillColor('#333333');
doc.text(`Origin: ${data.origin.name} (${data.origin.code})`); doc.text(`Origin: ${data.origin.name} (${data.origin.code})`);
doc.text( doc.text(`Destination: ${data.destination.name} (${data.destination.code})`);
`Destination: ${data.destination.name} (${data.destination.code})`
);
doc.text(`Carrier: ${data.carrier.name}`); doc.text(`Carrier: ${data.carrier.name}`);
doc.text(`ETD: ${data.etd.toLocaleDateString()}`); doc.text(`ETD: ${data.etd.toLocaleDateString()}`);
doc.text(`ETA: ${data.eta.toLocaleDateString()}`); doc.text(`ETA: ${data.eta.toLocaleDateString()}`);
@ -105,9 +98,7 @@ export class PdfAdapter implements PdfPort {
doc.fontSize(12).fillColor('#333333'); doc.fontSize(12).fillColor('#333333');
data.containers.forEach((container, index) => { data.containers.forEach((container, index) => {
doc.text( doc.text(`${index + 1}. Type: ${container.type} | Quantity: ${container.quantity}`);
`${index + 1}. Type: ${container.type} | Quantity: ${container.quantity}`
);
if (container.containerNumber) { if (container.containerNumber) {
doc.text(` Container #: ${container.containerNumber}`); doc.text(` Container #: ${container.containerNumber}`);
} }
@ -127,16 +118,10 @@ export class PdfAdapter implements PdfPort {
if (data.specialInstructions) { if (data.specialInstructions) {
doc.moveDown(); doc.moveDown();
doc doc.fontSize(14).fillColor('#0066cc').text('Special Instructions');
.fontSize(14)
.fillColor('#0066cc')
.text('Special Instructions');
doc.moveTo(50, doc.y).lineTo(550, doc.y).stroke(); doc.moveTo(50, doc.y).lineTo(550, doc.y).stroke();
doc.moveDown(); doc.moveDown();
doc doc.fontSize(12).fillColor('#333333').text(data.specialInstructions);
.fontSize(12)
.fillColor('#333333')
.text(data.specialInstructions);
} }
doc.moveDown(2); doc.moveDown(2);
@ -149,10 +134,9 @@ export class PdfAdapter implements PdfPort {
doc doc
.fontSize(16) .fontSize(16)
.fillColor('#333333') .fillColor('#333333')
.text( .text(`${data.price.currency} ${data.price.amount.toLocaleString()}`, {
`${data.price.currency} ${data.price.amount.toLocaleString()}`, align: 'center',
{ align: 'center' } });
);
doc.moveDown(3); doc.moveDown(3);
@ -160,10 +144,7 @@ export class PdfAdapter implements PdfPort {
doc doc
.fontSize(10) .fontSize(10)
.fillColor('#666666') .fillColor('#666666')
.text( .text('This is a system-generated document. No signature required.', { align: 'center' });
'This is a system-generated document. No signature required.',
{ align: 'center' }
);
doc.text('© 2025 Xpeditis. All rights reserved.', { align: 'center' }); doc.text('© 2025 Xpeditis. All rights reserved.', { align: 'center' });
@ -193,10 +174,7 @@ export class PdfAdapter implements PdfPort {
}); });
// Header // Header
doc doc.fontSize(20).fillColor('#0066cc').text('RATE QUOTE COMPARISON', { align: 'center' });
.fontSize(20)
.fillColor('#0066cc')
.text('RATE QUOTE COMPARISON', { align: 'center' });
doc.moveDown(2); doc.moveDown(2);
@ -210,20 +188,18 @@ export class PdfAdapter implements PdfPort {
doc.text('ETA', 430, startY, { width: 80 }); doc.text('ETA', 430, startY, { width: 80 });
doc.text('Route', 520, startY, { width: 200 }); doc.text('Route', 520, startY, { width: 200 });
doc.moveTo(50, doc.y + 5).lineTo(750, doc.y + 5).stroke(); doc
.moveTo(50, doc.y + 5)
.lineTo(750, doc.y + 5)
.stroke();
doc.moveDown(); doc.moveDown();
// Table Rows // Table Rows
doc.fontSize(9).fillColor('#333333'); doc.fontSize(9).fillColor('#333333');
quotes.forEach((quote) => { quotes.forEach(quote => {
const rowY = doc.y; const rowY = doc.y;
doc.text(quote.carrier.name, 50, rowY, { width: 100 }); doc.text(quote.carrier.name, 50, rowY, { width: 100 });
doc.text( doc.text(`${quote.price.currency} ${quote.price.amount}`, 160, rowY, { width: 80 });
`${quote.price.currency} ${quote.price.amount}`,
160,
rowY,
{ width: 80 }
);
doc.text(quote.transitDays.toString(), 250, rowY, { width: 80 }); doc.text(quote.transitDays.toString(), 250, rowY, { width: 80 });
doc.text(new Date(quote.etd).toLocaleDateString(), 340, rowY, { doc.text(new Date(quote.etd).toLocaleDateString(), 340, rowY, {
width: 80, width: 80,
@ -240,10 +216,7 @@ export class PdfAdapter implements PdfPort {
doc.moveDown(2); doc.moveDown(2);
// Footer // Footer
doc doc.fontSize(10).fillColor('#666666').text('Generated by Xpeditis', { align: 'center' });
.fontSize(10)
.fillColor('#666666')
.text('Generated by Xpeditis', { align: 'center' });
doc.end(); doc.end();
} catch (error) { } catch (error) {

View File

@ -83,7 +83,7 @@ export class BookingOrmEntity {
@Column({ name: 'cargo_description', type: 'text' }) @Column({ name: 'cargo_description', type: 'text' })
cargoDescription: string; cargoDescription: string;
@OneToMany(() => ContainerOrmEntity, (container) => container.booking, { @OneToMany(() => ContainerOrmEntity, container => container.booking, {
cascade: true, cascade: true,
eager: true, eager: true,
}) })

View File

@ -4,14 +4,7 @@
* TypeORM entity for container persistence * TypeORM entity for container persistence
*/ */
import { import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
Entity,
Column,
PrimaryColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { BookingOrmEntity } from './booking.orm-entity'; import { BookingOrmEntity } from './booking.orm-entity';
@Entity('containers') @Entity('containers')
@ -24,7 +17,7 @@ export class ContainerOrmEntity {
@Column({ name: 'booking_id', type: 'uuid' }) @Column({ name: 'booking_id', type: 'uuid' })
bookingId: string; bookingId: string;
@ManyToOne(() => BookingOrmEntity, (booking) => booking.containers, { @ManyToOne(() => BookingOrmEntity, booking => booking.containers, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
@JoinColumn({ name: 'booking_id' }) @JoinColumn({ name: 'booking_id' })

View File

@ -2,13 +2,7 @@
* Notification ORM Entity * Notification ORM Entity
*/ */
import { import { Entity, PrimaryColumn, Column, CreateDateColumn, Index } from 'typeorm';
Entity,
PrimaryColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
@Entity('notifications') @Entity('notifications')
@Index(['user_id', 'read', 'created_at']) @Index(['user_id', 'read', 'created_at'])

View File

@ -2,14 +2,7 @@
* Webhook ORM Entity * Webhook ORM Entity
*/ */
import { import { Entity, PrimaryColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
Entity,
PrimaryColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
} from 'typeorm';
@Entity('webhooks') @Entity('webhooks')
@Index(['organization_id', 'status']) @Index(['organization_id', 'status'])

View File

@ -12,10 +12,7 @@ import {
} from '../../../../domain/entities/booking.entity'; } from '../../../../domain/entities/booking.entity';
import { BookingNumber } from '../../../../domain/value-objects/booking-number.vo'; import { BookingNumber } from '../../../../domain/value-objects/booking-number.vo';
import { BookingStatus } from '../../../../domain/value-objects/booking-status.vo'; import { BookingStatus } from '../../../../domain/value-objects/booking-status.vo';
import { import { BookingOrmEntity, PartyJson } from '../entities/booking.orm-entity';
BookingOrmEntity,
PartyJson,
} from '../entities/booking.orm-entity';
import { ContainerOrmEntity } from '../entities/container.orm-entity'; import { ContainerOrmEntity } from '../entities/container.orm-entity';
export class BookingOrmMapper { export class BookingOrmMapper {
@ -39,9 +36,7 @@ export class BookingOrmMapper {
orm.updatedAt = domain.updatedAt; orm.updatedAt = domain.updatedAt;
// Map containers // Map containers
orm.containers = domain.containers.map((container) => orm.containers = domain.containers.map(container => this.containerToOrm(container, domain.id));
this.containerToOrm(container, domain.id)
);
return orm; return orm;
} }
@ -60,9 +55,7 @@ export class BookingOrmMapper {
shipper: this.jsonToParty(orm.shipper), shipper: this.jsonToParty(orm.shipper),
consignee: this.jsonToParty(orm.consignee), consignee: this.jsonToParty(orm.consignee),
cargoDescription: orm.cargoDescription, cargoDescription: orm.cargoDescription,
containers: orm.containers containers: orm.containers ? orm.containers.map(c => this.ormToContainer(c)) : [],
? orm.containers.map((c) => this.ormToContainer(c))
: [],
specialInstructions: orm.specialInstructions || undefined, specialInstructions: orm.specialInstructions || undefined,
createdAt: orm.createdAt, createdAt: orm.createdAt,
updatedAt: orm.updatedAt, updatedAt: orm.updatedAt,
@ -79,7 +72,7 @@ export class BookingOrmMapper {
* Map array of ORM entities to domain entities * Map array of ORM entities to domain entities
*/ */
static toDomainMany(orms: BookingOrmEntity[]): Booking[] { static toDomainMany(orms: BookingOrmEntity[]): Booking[] {
return orms.map((orm) => this.toDomain(orm)); return orms.map(orm => this.toDomain(orm));
} }
/** /**

View File

@ -55,6 +55,6 @@ export class CarrierOrmMapper {
* Map array of ORM entities to domain entities * Map array of ORM entities to domain entities
*/ */
static toDomainMany(orms: CarrierOrmEntity[]): Carrier[] { static toDomainMany(orms: CarrierOrmEntity[]): Carrier[] {
return orms.map((orm) => this.toDomain(orm)); return orms.map(orm => this.toDomain(orm));
} }
} }

View File

@ -63,6 +63,6 @@ export class OrganizationOrmMapper {
* Map array of ORM entities to domain entities * Map array of ORM entities to domain entities
*/ */
static toDomainMany(orms: OrganizationOrmEntity[]): Organization[] { static toDomainMany(orms: OrganizationOrmEntity[]): Organization[] {
return orms.map((orm) => this.toDomain(orm)); return orms.map(orm => this.toDomain(orm));
} }
} }

View File

@ -59,6 +59,6 @@ export class PortOrmMapper {
* Map array of ORM entities to domain entities * Map array of ORM entities to domain entities
*/ */
static toDomainMany(orms: PortOrmEntity[]): Port[] { static toDomainMany(orms: PortOrmEntity[]): Port[] {
return orms.map((orm) => this.toDomain(orm)); return orms.map(orm => this.toDomain(orm));
} }
} }

View File

@ -93,6 +93,6 @@ export class RateQuoteOrmMapper {
* Map array of ORM entities to domain entities * Map array of ORM entities to domain entities
*/ */
static toDomainMany(orms: RateQuoteOrmEntity[]): RateQuote[] { static toDomainMany(orms: RateQuoteOrmEntity[]): RateQuote[] {
return orms.map((orm) => this.toDomain(orm)); return orms.map(orm => this.toDomain(orm));
} }
} }

View File

@ -61,6 +61,6 @@ export class UserOrmMapper {
* Map array of ORM entities to domain entities * Map array of ORM entities to domain entities
*/ */
static toDomainMany(orms: UserOrmEntity[]): User[] { static toDomainMany(orms: UserOrmEntity[]): User[] {
return orms.map((orm) => this.toDomain(orm)); return orms.map(orm => this.toDomain(orm));
} }
} }

View File

@ -86,7 +86,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
}, },
], ],
}), }),
true, true
); );
// Create indexes for efficient querying // Create indexes for efficient querying
@ -95,7 +95,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({ new TableIndex({
name: 'idx_audit_logs_organization_timestamp', name: 'idx_audit_logs_organization_timestamp',
columnNames: ['organization_id', 'timestamp'], columnNames: ['organization_id', 'timestamp'],
}), })
); );
await queryRunner.createIndex( await queryRunner.createIndex(
@ -103,7 +103,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({ new TableIndex({
name: 'idx_audit_logs_user_timestamp', name: 'idx_audit_logs_user_timestamp',
columnNames: ['user_id', 'timestamp'], columnNames: ['user_id', 'timestamp'],
}), })
); );
await queryRunner.createIndex( await queryRunner.createIndex(
@ -111,7 +111,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({ new TableIndex({
name: 'idx_audit_logs_resource', name: 'idx_audit_logs_resource',
columnNames: ['resource_type', 'resource_id'], columnNames: ['resource_type', 'resource_id'],
}), })
); );
await queryRunner.createIndex( await queryRunner.createIndex(
@ -119,7 +119,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({ new TableIndex({
name: 'idx_audit_logs_action', name: 'idx_audit_logs_action',
columnNames: ['action'], columnNames: ['action'],
}), })
); );
await queryRunner.createIndex( await queryRunner.createIndex(
@ -127,7 +127,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({ new TableIndex({
name: 'idx_audit_logs_timestamp', name: 'idx_audit_logs_timestamp',
columnNames: ['timestamp'], columnNames: ['timestamp'],
}), })
); );
} }

View File

@ -74,7 +74,7 @@ export class CreateNotificationsTable1700000002000 implements MigrationInterface
}, },
], ],
}), }),
true, true
); );
// Create indexes for efficient querying // Create indexes for efficient querying
@ -83,7 +83,7 @@ export class CreateNotificationsTable1700000002000 implements MigrationInterface
new TableIndex({ new TableIndex({
name: 'idx_notifications_user_read_created', name: 'idx_notifications_user_read_created',
columnNames: ['user_id', 'read', 'created_at'], columnNames: ['user_id', 'read', 'created_at'],
}), })
); );
await queryRunner.createIndex( await queryRunner.createIndex(
@ -91,7 +91,7 @@ export class CreateNotificationsTable1700000002000 implements MigrationInterface
new TableIndex({ new TableIndex({
name: 'idx_notifications_organization_created', name: 'idx_notifications_organization_created',
columnNames: ['organization_id', 'created_at'], columnNames: ['organization_id', 'created_at'],
}), })
); );
await queryRunner.createIndex( await queryRunner.createIndex(
@ -99,7 +99,7 @@ export class CreateNotificationsTable1700000002000 implements MigrationInterface
new TableIndex({ new TableIndex({
name: 'idx_notifications_user_created', name: 'idx_notifications_user_created',
columnNames: ['user_id', 'created_at'], columnNames: ['user_id', 'created_at'],
}), })
); );
} }

View File

@ -80,7 +80,7 @@ export class CreateWebhooksTable1700000003000 implements MigrationInterface {
}, },
], ],
}), }),
true, true
); );
// Create index for efficient querying // Create index for efficient querying
@ -89,7 +89,7 @@ export class CreateWebhooksTable1700000003000 implements MigrationInterface {
new TableIndex({ new TableIndex({
name: 'idx_webhooks_organization_status', name: 'idx_webhooks_organization_status',
columnNames: ['organization_id', 'status'], columnNames: ['organization_id', 'status'],
}), })
); );
} }

View File

@ -19,7 +19,11 @@ export class SeedCarriersAndOrganizations1730000000006 implements MigrationInter
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
// Delete seeded data // Delete seeded data
await queryRunner.query(`DELETE FROM "carriers" WHERE "code" IN ('MAERSK', 'MSC', 'CMA_CGM', 'HAPAG_LLOYD', 'ONE')`); await queryRunner.query(
await queryRunner.query(`DELETE FROM "organizations" WHERE "name" IN ('Test Freight Forwarder Inc.', 'Demo Shipping Company', 'Sample Shipper Ltd.')`); `DELETE FROM "carriers" WHERE "code" IN ('MAERSK', 'MSC', 'CMA_CGM', 'HAPAG_LLOYD', 'ONE')`
);
await queryRunner.query(
`DELETE FROM "organizations" WHERE "name" IN ('Test Freight Forwarder Inc.', 'Demo Shipping Company', 'Sample Shipper Ltd.')`
);
} }
} }

View File

@ -17,7 +17,9 @@ export class SeedTestUsers1730000000007 implements MigrationInterface {
`); `);
if (result.length === 0) { if (result.length === 0) {
throw new Error('No organization found to seed users. Please run organization seed migration first.'); throw new Error(
'No organization found to seed users. Please run organization seed migration first.'
);
} }
const organizationId = result[0].id; const organizationId = result[0].id;

View File

@ -16,7 +16,7 @@ import { AuditLogOrmEntity } from '../entities/audit-log.orm-entity';
export class TypeOrmAuditLogRepository implements AuditLogRepository { export class TypeOrmAuditLogRepository implements AuditLogRepository {
constructor( constructor(
@InjectRepository(AuditLogOrmEntity) @InjectRepository(AuditLogOrmEntity)
private readonly ormRepository: Repository<AuditLogOrmEntity>, private readonly ormRepository: Repository<AuditLogOrmEntity>
) {} ) {}
async save(auditLog: AuditLog): Promise<void> { async save(auditLog: AuditLog): Promise<void> {
@ -77,7 +77,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
} }
const ormEntities = await query.getMany(); const ormEntities = await query.getMany();
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async count(filters: AuditLogFilters): Promise<number> { async count(filters: AuditLogFilters): Promise<number> {
@ -131,7 +131,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
}, },
}); });
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async findRecentByOrganization(organizationId: string, limit: number): Promise<AuditLog[]> { async findRecentByOrganization(organizationId: string, limit: number): Promise<AuditLog[]> {
@ -145,7 +145,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
take: limit, take: limit,
}); });
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async findByUser(userId: string, limit: number): Promise<AuditLog[]> { async findByUser(userId: string, limit: number): Promise<AuditLog[]> {
@ -159,7 +159,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
take: limit, take: limit,
}); });
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
/** /**

View File

@ -26,7 +26,7 @@ export class TypeOrmCarrierRepository implements CarrierRepository {
} }
async saveMany(carriers: Carrier[]): Promise<Carrier[]> { async saveMany(carriers: Carrier[]): Promise<Carrier[]> {
const orms = carriers.map((carrier) => CarrierOrmMapper.toOrm(carrier)); const orms = carriers.map(carrier => CarrierOrmMapper.toOrm(carrier));
const saved = await this.repository.save(orms); const saved = await this.repository.save(orms);
return CarrierOrmMapper.toDomainMany(saved); return CarrierOrmMapper.toDomainMany(saved);
} }

View File

@ -29,7 +29,7 @@ export class TypeOrmCsvRateConfigRepository implements CsvRateConfigRepositoryPo
constructor( constructor(
@InjectRepository(CsvRateConfigOrmEntity) @InjectRepository(CsvRateConfigOrmEntity)
private readonly repository: Repository<CsvRateConfigOrmEntity>, private readonly repository: Repository<CsvRateConfigOrmEntity>
) {} ) {}
/** /**
@ -90,7 +90,7 @@ export class TypeOrmCsvRateConfigRepository implements CsvRateConfigRepositoryPo
*/ */
async update( async update(
id: string, id: string,
config: Partial<CsvRateConfigOrmEntity>, config: Partial<CsvRateConfigOrmEntity>
): Promise<CsvRateConfigOrmEntity> { ): Promise<CsvRateConfigOrmEntity> {
this.logger.log(`Updating CSV rate config: ${id}`); this.logger.log(`Updating CSV rate config: ${id}`);
@ -137,7 +137,7 @@ export class TypeOrmCsvRateConfigRepository implements CsvRateConfigRepositoryPo
async updateValidationInfo( async updateValidationInfo(
companyName: string, companyName: string,
rowCount: number, rowCount: number,
validationResult: { valid: boolean; errors: string[] }, validationResult: { valid: boolean; errors: string[] }
): Promise<void> { ): Promise<void> {
this.logger.log(`Updating validation info for company: ${companyName}`); this.logger.log(`Updating validation info for company: ${companyName}`);
@ -159,7 +159,7 @@ export class TypeOrmCsvRateConfigRepository implements CsvRateConfigRepositoryPo
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
} as any, } as any,
}, }
); );
} }

View File

@ -16,7 +16,7 @@ import { NotificationOrmEntity } from '../entities/notification.orm-entity';
export class TypeOrmNotificationRepository implements NotificationRepository { export class TypeOrmNotificationRepository implements NotificationRepository {
constructor( constructor(
@InjectRepository(NotificationOrmEntity) @InjectRepository(NotificationOrmEntity)
private readonly ormRepository: Repository<NotificationOrmEntity>, private readonly ormRepository: Repository<NotificationOrmEntity>
) {} ) {}
async save(notification: Notification): Promise<void> { async save(notification: Notification): Promise<void> {
@ -79,7 +79,7 @@ export class TypeOrmNotificationRepository implements NotificationRepository {
} }
const ormEntities = await query.getMany(); const ormEntities = await query.getMany();
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async count(filters: NotificationFilters): Promise<number> { async count(filters: NotificationFilters): Promise<number> {
@ -131,7 +131,7 @@ export class TypeOrmNotificationRepository implements NotificationRepository {
take: limit, take: limit,
}); });
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async countUnreadByUser(userId: string): Promise<number> { async countUnreadByUser(userId: string): Promise<number> {
@ -147,7 +147,7 @@ export class TypeOrmNotificationRepository implements NotificationRepository {
take: limit, take: limit,
}); });
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async markAsRead(id: string): Promise<void> { async markAsRead(id: string): Promise<void> {
@ -163,7 +163,7 @@ export class TypeOrmNotificationRepository implements NotificationRepository {
{ {
read: true, read: true,
read_at: new Date(), read_at: new Date(),
}, }
); );
} }

View File

@ -26,7 +26,7 @@ export class TypeOrmPortRepository implements PortRepository {
} }
async saveMany(ports: Port[]): Promise<Port[]> { async saveMany(ports: Port[]): Promise<Port[]> {
const orms = ports.map((port) => PortOrmMapper.toOrm(port)); const orms = ports.map(port => PortOrmMapper.toOrm(port));
const saved = await this.repository.save(orms); const saved = await this.repository.save(orms);
return PortOrmMapper.toDomainMany(saved); return PortOrmMapper.toDomainMany(saved);
} }
@ -39,7 +39,7 @@ export class TypeOrmPortRepository implements PortRepository {
} }
async findByCodes(codes: string[]): Promise<Port[]> { async findByCodes(codes: string[]): Promise<Port[]> {
const upperCodes = codes.map((c) => c.toUpperCase()); const upperCodes = codes.map(c => c.toUpperCase());
const orms = await this.repository const orms = await this.repository
.createQueryBuilder('port') .createQueryBuilder('port')
.where('port.code IN (:...codes)', { codes: upperCodes }) .where('port.code IN (:...codes)', { codes: upperCodes })
@ -54,14 +54,11 @@ export class TypeOrmPortRepository implements PortRepository {
// Fuzzy search using pg_trgm (trigram similarity) // Fuzzy search using pg_trgm (trigram similarity)
// First try exact match on code // First try exact match on code
qb.andWhere( qb.andWhere('(port.code ILIKE :code OR port.name ILIKE :name OR port.city ILIKE :city)', {
'(port.code ILIKE :code OR port.name ILIKE :name OR port.city ILIKE :city)', code: `${query}%`,
{ name: `%${query}%`,
code: `${query}%`, city: `%${query}%`,
name: `%${query}%`, });
city: `%${query}%`,
}
);
if (countryFilter) { if (countryFilter) {
qb.andWhere('port.country = :country', { country: countryFilter.toUpperCase() }); qb.andWhere('port.country = :country', { country: countryFilter.toUpperCase() });

View File

@ -26,7 +26,7 @@ export class TypeOrmRateQuoteRepository implements RateQuoteRepository {
} }
async saveMany(rateQuotes: RateQuote[]): Promise<RateQuote[]> { async saveMany(rateQuotes: RateQuote[]): Promise<RateQuote[]> {
const orms = rateQuotes.map((rq) => RateQuoteOrmMapper.toOrm(rq)); const orms = rateQuotes.map(rq => RateQuoteOrmMapper.toOrm(rq));
const saved = await this.repository.save(orms); const saved = await this.repository.save(orms);
return RateQuoteOrmMapper.toDomainMany(saved); return RateQuoteOrmMapper.toDomainMany(saved);
} }

View File

@ -5,10 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { import { WebhookRepository, WebhookFilters } from '../../../../domain/ports/out/webhook.repository';
WebhookRepository,
WebhookFilters,
} from '../../../../domain/ports/out/webhook.repository';
import { Webhook, WebhookEvent, WebhookStatus } from '../../../../domain/entities/webhook.entity'; import { Webhook, WebhookEvent, WebhookStatus } from '../../../../domain/entities/webhook.entity';
import { WebhookOrmEntity } from '../entities/webhook.orm-entity'; import { WebhookOrmEntity } from '../entities/webhook.orm-entity';
@ -16,7 +13,7 @@ import { WebhookOrmEntity } from '../entities/webhook.orm-entity';
export class TypeOrmWebhookRepository implements WebhookRepository { export class TypeOrmWebhookRepository implements WebhookRepository {
constructor( constructor(
@InjectRepository(WebhookOrmEntity) @InjectRepository(WebhookOrmEntity)
private readonly ormRepository: Repository<WebhookOrmEntity>, private readonly ormRepository: Repository<WebhookOrmEntity>
) {} ) {}
async save(webhook: Webhook): Promise<void> { async save(webhook: Webhook): Promise<void> {
@ -35,7 +32,7 @@ export class TypeOrmWebhookRepository implements WebhookRepository {
order: { created_at: 'DESC' }, order: { created_at: 'DESC' },
}); });
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async findActiveByEvent(event: WebhookEvent, organizationId: string): Promise<Webhook[]> { async findActiveByEvent(event: WebhookEvent, organizationId: string): Promise<Webhook[]> {
@ -46,7 +43,7 @@ export class TypeOrmWebhookRepository implements WebhookRepository {
.andWhere(':event = ANY(webhook.events)', { event }) .andWhere(':event = ANY(webhook.events)', { event })
.getMany(); .getMany();
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async findByFilters(filters: WebhookFilters): Promise<Webhook[]> { async findByFilters(filters: WebhookFilters): Promise<Webhook[]> {
@ -69,7 +66,7 @@ export class TypeOrmWebhookRepository implements WebhookRepository {
query.orderBy('webhook.created_at', 'DESC'); query.orderBy('webhook.created_at', 'DESC');
const ormEntities = await query.getMany(); const ormEntities = await query.getMany();
return ormEntities.map((e) => this.toDomain(e)); return ormEntities.map(e => this.toDomain(e));
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {

View File

@ -76,7 +76,7 @@ export const carrierSeeds: CarrierSeed[] = [
export function getCarriersInsertSQL(): string { export function getCarriersInsertSQL(): string {
const values = carrierSeeds const values = carrierSeeds
.map( .map(
(carrier) => carrier =>
`('${carrier.id}', '${carrier.name}', '${carrier.code}', '${carrier.scac}', ` + `('${carrier.id}', '${carrier.name}', '${carrier.code}', '${carrier.scac}', ` +
`'${carrier.logoUrl}', '${carrier.website}', NULL, ${carrier.isActive}, ${carrier.supportsApi}, NOW(), NOW())` `'${carrier.logoUrl}', '${carrier.website}', NULL, ${carrier.isActive}, ${carrier.supportsApi}, NOW(), NOW())`
) )

View File

@ -64,7 +64,7 @@ export const organizationSeeds: OrganizationSeed[] = [
export function getOrganizationsInsertSQL(): string { export function getOrganizationsInsertSQL(): string {
const values = organizationSeeds const values = organizationSeeds
.map( .map(
(org) => org =>
`('${org.id}', '${org.name}', '${org.type}', ` + `('${org.id}', '${org.name}', '${org.type}', ` +
`${org.scac ? `'${org.scac}'` : 'NULL'}, ` + `${org.scac ? `'${org.scac}'` : 'NULL'}, ` +
`'${org.addressStreet}', '${org.addressCity}', ` + `'${org.addressStreet}', '${org.addressCity}', ` +

View File

@ -102,12 +102,7 @@ export const corsConfig = {
origin: process.env.FRONTEND_URL || ['http://localhost:3000', 'http://localhost:3001'], origin: process.env.FRONTEND_URL || ['http://localhost:3000', 'http://localhost:3001'],
credentials: true, credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token'],
'Content-Type',
'Authorization',
'X-Requested-With',
'X-CSRF-Token',
],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'], exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
maxAge: 86400, // 24 hours maxAge: 86400, // 24 hours
}; };

View File

@ -22,11 +22,7 @@ import { rateLimitConfig } from './security.config';
}, },
]), ]),
], ],
providers: [ providers: [FileValidationService, BruteForceProtectionService, CustomThrottlerGuard],
FileValidationService,
BruteForceProtectionService,
CustomThrottlerGuard,
],
exports: [ exports: [
FileValidationService, FileValidationService,
BruteForceProtectionService, BruteForceProtectionService,

View File

@ -36,9 +36,7 @@ export class S3StorageAdapter implements StoragePort {
const region = this.configService.get<string>('AWS_REGION', 'us-east-1'); const region = this.configService.get<string>('AWS_REGION', 'us-east-1');
const endpoint = this.configService.get<string>('AWS_S3_ENDPOINT'); const endpoint = this.configService.get<string>('AWS_S3_ENDPOINT');
const accessKeyId = this.configService.get<string>('AWS_ACCESS_KEY_ID'); const accessKeyId = this.configService.get<string>('AWS_ACCESS_KEY_ID');
const secretAccessKey = this.configService.get<string>( const secretAccessKey = this.configService.get<string>('AWS_SECRET_ACCESS_KEY');
'AWS_SECRET_ACCESS_KEY'
);
this.s3Client = new S3Client({ this.s3Client = new S3Client({
region, region,
@ -73,9 +71,7 @@ export class S3StorageAdapter implements StoragePort {
const url = this.buildUrl(options.bucket, options.key); const url = this.buildUrl(options.bucket, options.key);
const size = const size =
typeof options.body === 'string' typeof options.body === 'string' ? Buffer.byteLength(options.body) : options.body.length;
? Buffer.byteLength(options.body)
: options.body.length;
this.logger.log(`Uploaded file to S3: ${options.key}`); this.logger.log(`Uploaded file to S3: ${options.key}`);
@ -109,10 +105,7 @@ export class S3StorageAdapter implements StoragePort {
this.logger.log(`Downloaded file from S3: ${options.key}`); this.logger.log(`Downloaded file from S3: ${options.key}`);
return Buffer.concat(chunks); return Buffer.concat(chunks);
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(`Failed to download file from S3: ${options.key}`, error);
`Failed to download file from S3: ${options.key}`,
error
);
throw error; throw error;
} }
} }
@ -132,10 +125,7 @@ export class S3StorageAdapter implements StoragePort {
} }
} }
async getSignedUrl( async getSignedUrl(options: DownloadOptions, expiresIn: number = 3600): Promise<string> {
options: DownloadOptions,
expiresIn: number = 3600
): Promise<string> {
try { try {
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: options.bucket, Bucket: options.bucket,
@ -143,15 +133,10 @@ export class S3StorageAdapter implements StoragePort {
}); });
const url = await getSignedUrl(this.s3Client, command, { expiresIn }); const url = await getSignedUrl(this.s3Client, command, { expiresIn });
this.logger.log( this.logger.log(`Generated signed URL for: ${options.key} (expires in ${expiresIn}s)`);
`Generated signed URL for: ${options.key} (expires in ${expiresIn}s)`
);
return url; return url;
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(`Failed to generate signed URL for: ${options.key}`, error);
`Failed to generate signed URL for: ${options.key}`,
error
);
throw error; throw error;
} }
} }

View File

@ -6,10 +6,7 @@ import helmet from 'helmet';
import * as compression from 'compression'; import * as compression from 'compression';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { Logger } from 'nestjs-pino'; import { Logger } from 'nestjs-pino';
import { import { helmetConfig, corsConfig } from './infrastructure/security/security.config';
helmetConfig,
corsConfig,
} from './infrastructure/security/security.config';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
@ -50,14 +47,14 @@ async function bootstrap() {
transformOptions: { transformOptions: {
enableImplicitConversion: true, enableImplicitConversion: true,
}, },
}), })
); );
// Swagger documentation // Swagger documentation
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Xpeditis API') .setTitle('Xpeditis API')
.setDescription( .setDescription(
'Maritime Freight Booking Platform - API for searching rates and managing bookings', 'Maritime Freight Booking Platform - API for searching rates and managing bookings'
) )
.setVersion('1.0') .setVersion('1.0')
.addBearerAuth() .addBearerAuth()

View File

@ -19,7 +19,7 @@ describe('AppController (e2e)', () => {
return request(app.getHttpServer()) return request(app.getHttpServer())
.get('/api/v1/health') .get('/api/v1/health')
.expect(200) .expect(200)
.expect((res) => { .expect(res => {
expect(res.body).toHaveProperty('status', 'ok'); expect(res.body).toHaveProperty('status', 'ok');
expect(res.body).toHaveProperty('timestamp'); expect(res.body).toHaveProperty('timestamp');
}); });

View File

@ -309,7 +309,7 @@ describe('TypeOrmBookingRepository (Integration)', () => {
const bookings = await repository.findByOrganization(testOrganization.id); const bookings = await repository.findByOrganization(testOrganization.id);
expect(bookings).toHaveLength(3); expect(bookings).toHaveLength(3);
expect(bookings.every((b) => b.organizationId === testOrganization.id)).toBe(true); expect(bookings.every(b => b.organizationId === testOrganization.id)).toBe(true);
}); });
it('should return empty array for organization with no bookings', async () => { it('should return empty array for organization with no bookings', async () => {
@ -338,8 +338,8 @@ describe('TypeOrmBookingRepository (Integration)', () => {
expect(draftBookings).toHaveLength(2); expect(draftBookings).toHaveLength(2);
expect(confirmedBookings).toHaveLength(1); expect(confirmedBookings).toHaveLength(1);
expect(draftBookings.every((b) => b.status.value === 'draft')).toBe(true); expect(draftBookings.every(b => b.status.value === 'draft')).toBe(true);
expect(confirmedBookings.every((b) => b.status.value === 'confirmed')).toBe(true); expect(confirmedBookings.every(b => b.status.value === 'confirmed')).toBe(true);
}); });
}); });

View File

@ -230,7 +230,7 @@ describe('MaerskConnector (Integration)', () => {
expect(quotes[0].route).toBeDefined(); expect(quotes[0].route).toBeDefined();
expect(Array.isArray(quotes[0].route)).toBe(true); expect(Array.isArray(quotes[0].route)).toBe(true);
// Vessel name should be in route segments // Vessel name should be in route segments
const hasVesselInfo = quotes[0].route.some((seg) => seg.vesselName); const hasVesselInfo = quotes[0].route.some(seg => seg.vesselName);
expect(hasVesselInfo).toBe(true); expect(hasVesselInfo).toBe(true);
}); });