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({
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
PORT: Joi.number().default(4000),
DATABASE_HOST: Joi.string().required(),
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 { ConfigService } from '@nestjs/config';
import * as argon2 from 'argon2';
@ -22,7 +28,7 @@ export class AuthService {
@Inject(USER_REPOSITORY)
private readonly userRepository: UserRepository, // ✅ Correct injection
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly configService: ConfigService
) {}
/**
@ -33,7 +39,7 @@ export class AuthService {
password: string,
firstName: string,
lastName: string,
organizationId?: string,
organizationId?: string
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
this.logger.log(`Registering new user: ${email}`);
@ -87,7 +93,7 @@ export class AuthService {
*/
async login(
email: string,
password: string,
password: string
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
this.logger.log(`Login attempt for: ${email}`);
@ -127,7 +133,9 @@ export class AuthService {
/**
* 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 {
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
secret: this.configService.get('JWT_SECRET'),

View File

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

View File

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

View File

@ -66,8 +66,18 @@ export class AuditController {
@ApiOperation({ summary: 'Get audit logs with filters' })
@ApiResponse({ status: 200, description: 'Audit logs retrieved successfully' })
@ApiQuery({ name: 'userId', required: false, description: 'Filter by user ID' })
@ApiQuery({ 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: '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: 'resourceId', required: false, description: 'Filter by resource ID' })
@ApiQuery({ name: 'startDate', required: false, description: 'Filter by start date (ISO 8601)' })
@ -84,7 +94,7 @@ export class AuditController {
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@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 }> {
page = page || 1;
limit = limit || 50;
@ -104,7 +114,7 @@ export class AuditController {
const { logs, total } = await this.auditService.getAuditLogs(filters);
return {
logs: logs.map((log) => this.mapToDto(log)),
logs: logs.map(log => this.mapToDto(log)),
total,
page,
pageSize: limit,
@ -121,7 +131,7 @@ export class AuditController {
@ApiResponse({ status: 404, description: 'Audit log not found' })
async getAuditLogById(
@Param('id') id: string,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<AuditLogResponseDto> {
const log = await this.auditService.getAuditLogs({
organizationId: user.organizationId,
@ -145,14 +155,14 @@ export class AuditController {
async getResourceAuditTrail(
@Param('type') resourceType: string,
@Param('id') resourceId: string,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<AuditLogResponseDto[]> {
const logs = await this.auditService.getResourceAuditTrail(resourceType, resourceId);
// 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)' })
async getOrganizationActivity(
@CurrentUser() user: UserPayload,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number
): Promise<AuditLogResponseDto[]> {
limit = limit || 50;
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(
@CurrentUser() user: UserPayload,
@Param('userId') userId: string,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number,
@Query('limit', new DefaultValuePipe(50), ParseIntPipe) limit?: number
): Promise<AuditLogResponseDto[]> {
limit = limit || 50;
const logs = await this.auditService.getUserActivity(userId, limit);
// 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 {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
UseGuards,
Get,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { AuthService } from '../auth/auth.service';
import {
LoginDto,
RegisterDto,
AuthResponseDto,
RefreshTokenDto,
} from '../dto/auth-login.dto';
import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto } from '../dto/auth-login.dto';
import { Public } from '../decorators/public.decorator';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
@ -52,8 +34,7 @@ export class AuthController {
@HttpCode(HttpStatus.CREATED)
@ApiOperation({
summary: 'Register new user',
description:
'Create a new user account with email and password. Returns JWT tokens.',
description: 'Create a new user account with email and password. Returns JWT tokens.',
})
@ApiResponse({
status: 201,
@ -74,7 +55,7 @@ export class AuthController {
dto.password,
dto.firstName,
dto.lastName,
dto.organizationId,
dto.organizationId
);
return {
@ -147,11 +128,8 @@ export class AuthController {
status: 401,
description: 'Invalid or expired refresh token',
})
async refresh(
@Body() dto: RefreshTokenDto,
): Promise<{ accessToken: string }> {
const result =
await this.authService.refreshAccessToken(dto.refreshToken);
async refresh(@Body() dto: RefreshTokenDto): Promise<{ accessToken: string }> {
const result = await this.authService.refreshAccessToken(dto.refreshToken);
return { accessToken: result.accessToken };
}
@ -170,8 +148,7 @@ export class AuthController {
@ApiBearerAuth()
@ApiOperation({
summary: 'Logout',
description:
'Logout the current user. Currently handled client-side by removing tokens.',
description: 'Logout the current user. Currently handled client-side by removing tokens.',
})
@ApiResponse({
status: 200,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,13 +16,12 @@ import {
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { WebhookService, CreateWebhookInput, UpdateWebhookInput } from '../services/webhook.service';
WebhookService,
CreateWebhookInput,
UpdateWebhookInput,
} from '../services/webhook.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator';
@ -74,7 +73,7 @@ export class WebhooksController {
@ApiResponse({ status: 201, description: 'Webhook created successfully' })
async createWebhook(
@Body() dto: CreateWebhookDto,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<WebhookResponseDto> {
const input: CreateWebhookInput = {
organizationId: user.organizationId,
@ -96,10 +95,8 @@ export class WebhooksController {
@ApiOperation({ summary: 'Get all webhooks for organization' })
@ApiResponse({ status: 200, description: 'Webhooks retrieved successfully' })
async getWebhooks(@CurrentUser() user: UserPayload): Promise<WebhookResponseDto[]> {
const webhooks = await this.webhookService.getWebhooksByOrganization(
user.organizationId,
);
return webhooks.map((w) => this.mapToDto(w));
const webhooks = await this.webhookService.getWebhooksByOrganization(user.organizationId);
return webhooks.map(w => this.mapToDto(w));
}
/**
@ -112,7 +109,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' })
async getWebhookById(
@Param('id') id: string,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<WebhookResponseDto> {
const webhook = await this.webhookService.getWebhookById(id);
@ -139,7 +136,7 @@ export class WebhooksController {
async updateWebhook(
@Param('id') id: string,
@Body() dto: UpdateWebhookDto,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<WebhookResponseDto> {
const webhook = await this.webhookService.getWebhookById(id);
@ -166,7 +163,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' })
async activateWebhook(
@Param('id') id: string,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<{ success: boolean }> {
const webhook = await this.webhookService.getWebhookById(id);
@ -193,7 +190,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' })
async deactivateWebhook(
@Param('id') id: string,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<{ success: boolean }> {
const webhook = await this.webhookService.getWebhookById(id);
@ -220,7 +217,7 @@ export class WebhooksController {
@ApiResponse({ status: 404, description: 'Webhook not found' })
async deleteWebhook(
@Param('id') id: string,
@CurrentUser() user: UserPayload,
@CurrentUser() user: UserPayload
): Promise<{ success: boolean }> {
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
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 { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
@ -45,7 +54,9 @@ export class PartyDto {
@ApiProperty({ example: '+31612345678' })
@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;
}
@ -57,14 +68,19 @@ export class ContainerDto {
@ApiPropertyOptional({ example: 'ABCU1234567', description: 'Container number (11 characters)' })
@IsOptional()
@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;
@ApiPropertyOptional({ example: 22000, description: 'Verified Gross Mass in kg' })
@IsOptional()
vgm?: number;
@ApiPropertyOptional({ example: -18, description: 'Temperature in Celsius (for reefer containers)' })
@ApiPropertyOptional({
example: -18,
description: 'Temperature in Celsius (for reefer containers)',
})
@IsOptional()
temperature?: number;
@ -77,7 +93,7 @@ export class ContainerDto {
export class CreateBookingRequestDto {
@ApiProperty({
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' })
rateQuoteId: string;
@ -94,7 +110,7 @@ export class CreateBookingRequestDto {
@ApiProperty({
example: 'Electronics and consumer goods',
description: 'Cargo description'
description: 'Cargo description',
})
@IsString()
@MinLength(10, { message: 'Cargo description must be at least 10 characters' })
@ -102,7 +118,7 @@ export class CreateBookingRequestDto {
@ApiProperty({
type: [ContainerDto],
description: 'Container details (can be empty for initial booking)'
description: 'Container details (can be empty for initial booking)',
})
@IsArray()
@ValidateNested({ each: true })
@ -111,7 +127,7 @@ export class CreateBookingRequestDto {
@ApiPropertyOptional({
example: 'Please handle with care. Delivery before 5 PM.',
description: 'Special instructions for the carrier'
description: 'Special instructions for the carrier',
})
@IsOptional()
@IsString()

View File

@ -1,12 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsNotEmpty,
IsString,
IsNumber,
Min,
IsOptional,
ValidateNested,
} from 'class-validator';
import { IsNotEmpty, IsString, IsNumber, Min, IsOptional, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { RateSearchFiltersDto } from './rate-search-filters.dto';
@ -152,7 +145,7 @@ export class CsvRateResultDto {
@ApiProperty({
description: 'Calculated price in USD',
example: 1850.50,
example: 1850.5,
})
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';
export class RateSearchRequestDto {
@ -17,7 +26,9 @@ export class RateSearchRequestDto {
pattern: '^[A-Z]{5}$',
})
@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;
@ApiProperty({
@ -92,6 +103,8 @@ export class RateSearchRequestDto {
})
@IsOptional()
@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;
}

View File

@ -67,7 +67,8 @@ export class CreateUserDto {
@ApiPropertyOptional({
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,
})
@IsString()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,7 @@
import { Injectable } from '@nestjs/common';
import { CsvRate } from '@domain/entities/csv-rate.entity';
import { Volume } from '@domain/value-objects/volume.vo';
import {
CsvRateResultDto,
CsvRateSearchResponseDto,
} from '../dto/csv-rate-search.dto';
import { CsvRateResultDto, CsvRateSearchResponseDto } from '../dto/csv-rate-search.dto';
import {
CsvRateSearchInput,
CsvRateSearchOutput,
@ -77,7 +74,7 @@ export class CsvRateMapper {
*/
mapSearchOutputToResponseDto(output: CsvRateSearchOutput): CsvRateSearchResponseDto {
return {
results: output.results.map((result) => this.mapSearchResultToDto(result)),
results: output.results.map(result => this.mapSearchResultToDto(result)),
totalResults: output.totalResults,
searchedFiles: output.searchedFiles,
searchedAt: output.searchedAt,
@ -107,6 +104,6 @@ export class CsvRateMapper {
* Map multiple config entities to DTOs
*/
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
*/
private static mapDocumentToDto(
document: OrganizationDocument,
): OrganizationDocumentDto {
private static mapDocumentToDto(document: OrganizationDocument): OrganizationDocumentDto {
return {
id: document.id,
type: document.type,

View File

@ -29,7 +29,7 @@ export class RateQuoteMapper {
},
pricing: {
baseFreight: entity.pricing.baseFreight,
surcharges: entity.pricing.surcharges.map((s) => ({
surcharges: entity.pricing.surcharges.map(s => ({
type: s.type,
description: s.description,
amount: s.amount,
@ -43,7 +43,7 @@ export class RateQuoteMapper {
etd: entity.etd.toISOString(),
eta: entity.eta.toISOString(),
transitDays: entity.transitDays,
route: entity.route.map((segment) => ({
route: entity.route.map(segment => ({
portCode: segment.portCode,
portName: segment.portName,
arrival: segment.arrival?.toISOString(),
@ -64,6 +64,6 @@ export class RateQuoteMapper {
* Map array of RateQuote entities to DTOs
*/
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,
useFactory: (
cache: any,
rateQuoteRepo: any,
portRepo: any,
carrierRepo: any,
) => {
useFactory: (cache: any, rateQuoteRepo: any, portRepo: any, carrierRepo: any) => {
// For now, create service with empty connectors array
// TODO: Inject actual carrier connectors
return new RateSearchService(
[],
cache,
rateQuoteRepo,
portRepo,
carrierRepo,
);
return new RateSearchService([], cache, rateQuoteRepo, portRepo, carrierRepo);
},
inject: [
CACHE_PORT,
RATE_QUOTE_REPOSITORY,
PORT_REPOSITORY,
CARRIER_REPOSITORY,
],
inject: [CACHE_PORT, RATE_QUOTE_REPOSITORY, PORT_REPOSITORY, CARRIER_REPOSITORY],
},
],
exports: [
RATE_QUOTE_REPOSITORY,
RateSearchService,
],
exports: [RATE_QUOTE_REPOSITORY, RateSearchService],
})
export class RatesModule {}

View File

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

View File

@ -4,7 +4,10 @@
import { Test, TestingModule } from '@nestjs/testing';
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';
describe('AuditService', () => {

View File

@ -7,11 +7,7 @@
import { Injectable, Logger, Inject } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import {
AuditLog,
AuditAction,
AuditStatus,
} from '../../domain/entities/audit-log.entity';
import { AuditLog, AuditAction, AuditStatus } from '../../domain/entities/audit-log.entity';
import {
AuditLogRepository,
AUDIT_LOG_REPOSITORY,
@ -39,7 +35,7 @@ export class AuditService {
constructor(
@Inject(AUDIT_LOG_REPOSITORY)
private readonly auditLogRepository: AuditLogRepository,
private readonly auditLogRepository: AuditLogRepository
) {}
/**
@ -54,14 +50,12 @@ export class AuditService {
await this.auditLogRepository.save(auditLog);
this.logger.log(
`Audit log created: ${input.action} by ${input.userEmail} (${input.status})`,
);
this.logger.log(`Audit log created: ${input.action} by ${input.userEmail} (${input.status})`);
} catch (error: any) {
// Never throw on audit logging failure - log the error and continue
this.logger.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>;
ipAddress?: string;
userAgent?: string;
},
}
): Promise<void> {
await this.log({
action,
@ -108,7 +102,7 @@ export class AuditService {
metadata?: Record<string, any>;
ipAddress?: string;
userAgent?: string;
},
}
): Promise<void> {
await this.log({
action,
@ -139,20 +133,14 @@ export class AuditService {
/**
* Get audit trail for a specific resource
*/
async getResourceAuditTrail(
resourceType: string,
resourceId: string,
): Promise<AuditLog[]> {
async getResourceAuditTrail(resourceType: string, resourceId: string): Promise<AuditLog[]> {
return this.auditLogRepository.findByResource(resourceType, resourceId);
}
/**
* Get recent activity for an organization
*/
async getOrganizationActivity(
organizationId: string,
limit: number = 50,
): Promise<AuditLog[]> {
async getOrganizationActivity(organizationId: string, limit: number = 50): Promise<AuditLog[]> {
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 { EmailPort, EMAIL_PORT } from '../../domain/ports/out/email.port';
import { PdfPort, PDF_PORT, BookingPdfData } from '../../domain/ports/out/pdf.port';
import {
StoragePort,
STORAGE_PORT,
} from '../../domain/ports/out/storage.port';
import { StoragePort, STORAGE_PORT } from '../../domain/ports/out/storage.port';
import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository';
import { RateQuoteRepository, RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.repository';
import {
RateQuoteRepository,
RATE_QUOTE_REPOSITORY,
} from '../../domain/ports/out/rate-quote.repository';
@Injectable()
export class BookingAutomationService {
@ -24,16 +24,14 @@ export class BookingAutomationService {
@Inject(PDF_PORT) private readonly pdfPort: PdfPort,
@Inject(STORAGE_PORT) private readonly storagePort: StoragePort,
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
@Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository,
@Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository
) {}
/**
* Execute all post-booking automation tasks
*/
async executePostBookingTasks(booking: Booking): Promise<void> {
this.logger.log(
`Starting post-booking automation for booking: ${booking.bookingNumber.value}`
);
this.logger.log(`Starting post-booking automation for booking: ${booking.bookingNumber.value}`);
try {
// Get user and rate quote details
@ -42,9 +40,7 @@ export class BookingAutomationService {
throw new Error(`User not found: ${booking.userId}`);
}
const rateQuote = await this.rateQuoteRepository.findById(
booking.rateQuoteId
);
const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
if (!rateQuote) {
throw new Error(`Rate quote not found: ${booking.rateQuoteId}`);
}
@ -79,7 +75,7 @@ export class BookingAutomationService {
email: booking.consignee.contactEmail,
phone: booking.consignee.contactPhone,
},
containers: booking.containers.map((c) => ({
containers: booking.containers.map(c => ({
type: c.type,
quantity: 1,
containerNumber: c.containerNumber,
@ -173,10 +169,7 @@ export class BookingAutomationService {
`Sent ${updateType} notification for booking: ${booking.bookingNumber.value}`
);
} catch (error) {
this.logger.error(
`Failed to send booking update notification`,
error
);
this.logger.error(`Failed to send booking update notification`, error);
}
}
}

View File

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

View File

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

View File

@ -32,14 +32,14 @@ export class FileValidationService {
// Validate file size
if (file.size > fileUploadConfig.maxFileSize) {
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
if (!fileUploadConfig.allowedMimeTypes.includes(file.mimetype)) {
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();
if (!fileUploadConfig.allowedExtensions.includes(ext)) {
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();
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
// For now, just log
this.logger.log(
`Virus scan requested for file: ${file.originalname} (not implemented)`,
);
this.logger.log(`Virus scan requested for file: ${file.originalname} (not implemented)`);
return true;
}
@ -190,9 +188,7 @@ export class FileValidationService {
/**
* Validate multiple files
*/
async validateFiles(
files: Express.Multer.File[],
): Promise<FileValidationResult> {
async validateFiles(files: Express.Multer.File[]): Promise<FileValidationResult> {
const allErrors: string[] = [];
for (const file of files) {

View File

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

View File

@ -31,7 +31,7 @@ export class GDPRService {
constructor(
@InjectRepository(UserOrmEntity)
private readonly userRepository: Repository<UserOrmEntity>,
private readonly userRepository: Repository<UserOrmEntity>
) {}
/**
@ -63,7 +63,8 @@ export class GDPRService {
exportDate: new Date().toISOString(),
userId,
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}`);
@ -76,7 +77,9 @@ export class GDPRService {
* Note: This is a simplified version. In production, implement full anonymization logic.
*/
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
const user = await this.userRepository.findOne({ where: { id: userId } });
@ -117,7 +120,9 @@ export class GDPRService {
// In production, store in separate consent table
// 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 { NotificationService } from './notification.service';
import { NOTIFICATION_REPOSITORY, NotificationRepository } from '../../domain/ports/out/notification.repository';
import { Notification, NotificationType, NotificationPriority } from '../../domain/entities/notification.entity';
import {
NOTIFICATION_REPOSITORY,
NotificationRepository,
} from '../../domain/ports/out/notification.repository';
import {
Notification,
NotificationType,
NotificationPriority,
} from '../../domain/entities/notification.entity';
describe('NotificationService', () => {
let service: NotificationService;

View File

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

View File

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

View File

@ -9,11 +9,7 @@ import { HttpService } from '@nestjs/axios';
import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto';
import { firstValueFrom } from 'rxjs';
import {
Webhook,
WebhookEvent,
WebhookStatus,
} from '../../domain/entities/webhook.entity';
import { Webhook, WebhookEvent, WebhookStatus } from '../../domain/entities/webhook.entity';
import {
WebhookRepository,
WEBHOOK_REPOSITORY,
@ -51,7 +47,7 @@ export class WebhookService {
constructor(
@Inject(WEBHOOK_REPOSITORY)
private readonly webhookRepository: WebhookRepository,
private readonly httpService: HttpService,
private readonly httpService: HttpService
) {}
/**
@ -72,9 +68,7 @@ export class WebhookService {
await this.webhookRepository.save(webhook);
this.logger.log(
`Webhook created: ${webhook.id} for organization ${input.organizationId}`,
);
this.logger.log(`Webhook created: ${webhook.id} for organization ${input.organizationId}`);
return webhook;
}
@ -158,11 +152,7 @@ export class WebhookService {
/**
* Trigger webhooks for an event
*/
async triggerWebhooks(
event: WebhookEvent,
organizationId: string,
data: any,
): Promise<void> {
async triggerWebhooks(event: WebhookEvent, organizationId: string, data: any): Promise<void> {
try {
const webhooks = await this.webhookRepository.findActiveByEvent(event, organizationId);
@ -179,17 +169,13 @@ export class WebhookService {
};
// Trigger all webhooks in parallel
await Promise.allSettled(
webhooks.map((webhook) => this.triggerWebhook(webhook, payload)),
);
await Promise.allSettled(webhooks.map(webhook => this.triggerWebhook(webhook, payload)));
this.logger.log(
`Triggered ${webhooks.length} webhooks for event: ${event}`,
);
this.logger.log(`Triggered ${webhooks.length} webhooks for event: ${event}`);
} catch (error: any) {
this.logger.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
*/
private async triggerWebhook(
webhook: Webhook,
payload: WebhookPayload,
): Promise<void> {
private async triggerWebhook(webhook: Webhook, payload: WebhookPayload): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) {
@ -226,7 +209,7 @@ export class WebhookService {
this.httpService.post(webhook.url, payload, {
headers,
timeout: 10000, // 10 seconds
}),
})
);
if (response && response.status >= 200 && response.status < 300) {
@ -234,17 +217,17 @@ export class WebhookService {
const updatedWebhook = webhook.recordTrigger();
await this.webhookRepository.save(updatedWebhook);
this.logger.log(
`Webhook triggered successfully: ${webhook.id} (attempt ${attempt + 1})`,
);
this.logger.log(`Webhook triggered successfully: ${webhook.id} (attempt ${attempt + 1})`);
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) {
lastError = error;
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);
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 {
const expectedSignature = this.generateSignature(payload, secret);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
/**
* Delay helper for retries
*/
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 {
if (!this.status.canTransitionTo(newStatus)) {
throw new Error(
`Cannot transition from ${this.status.value} to ${newStatus.value}`
);
throw new Error(`Cannot transition from ${this.status.value} to ${newStatus.value}`);
}
return new Booking({
@ -209,7 +207,7 @@ export class Booking {
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) {
throw new Error(`Container ${containerId} not found`);
}
@ -237,7 +235,7 @@ export class Booking {
return new Booking({
...this.props,
containers: this.props.containers.filter((c) => c.id !== containerId),
containers: this.props.containers.filter(c => c.id !== containerId),
updatedAt: new Date(),
});
}

View File

@ -53,7 +53,9 @@ export class Carrier {
// Validate carrier 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

View File

@ -233,7 +233,10 @@ export class Container {
// Twenty-foot Equivalent Unit
if (this.props.size === ContainerSize.TWENTY) {
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 0;

View File

@ -55,7 +55,7 @@ export class CsvRate {
public readonly currency: string, // Primary currency (USD or EUR)
public readonly surcharges: SurchargeCollection,
public readonly transitDays: number,
public readonly validity: DateRange,
public readonly validity: DateRange
) {
this.validate();
}
@ -111,7 +111,7 @@ export class CsvRate {
// Freight class rule: max(volume price, weight price)
const freightPrice = volume.calculateFreightPrice(
this.pricing.pricePerCBM,
this.pricing.pricePerKG,
this.pricing.pricePerKG
);
// 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
// and recalculate proportionally
const basePriceInPrimaryCurrency =
this.currency === 'USD'
? this.pricing.basePriceUSD
: this.pricing.basePriceEUR;
this.currency === 'USD' ? this.pricing.basePriceUSD : this.pricing.basePriceEUR;
const basePriceInTargetCurrency =
targetCurrency === 'USD'
? this.pricing.basePriceUSD
: this.pricing.basePriceEUR;
targetCurrency === 'USD' ? this.pricing.basePriceUSD : this.pricing.basePriceEUR;
// Calculate conversion ratio
const ratio =
basePriceInTargetCurrency.getAmount() /
basePriceInPrimaryCurrency.getAmount();
const ratio = basePriceInTargetCurrency.getAmount() / basePriceInPrimaryCurrency.getAmount();
// Apply ratio to calculated price
const convertedAmount = price.getAmount() * ratio;
@ -179,7 +173,7 @@ export class CsvRate {
this.volumeRange.minCBM,
this.volumeRange.maxCBM,
this.weightRange.minKG,
this.weightRange.maxKG,
this.weightRange.maxKG
);
}

View File

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

View File

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

View File

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

View File

@ -41,7 +41,10 @@ export class Webhook {
private constructor(private readonly props: WebhookProps) {}
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 {
return new Webhook({
...props,

View File

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

View File

@ -10,7 +10,12 @@
*/
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 { PortNotFoundException } from '../exceptions/port-not-found.exception';
@ -53,8 +58,8 @@ export class PortSearchService implements GetPortsPort {
const ports = await this.portRepository.findByCodes(portCodes);
// Check if all ports were found
const foundCodes = ports.map((p) => p.code);
const missingCodes = portCodes.filter((code) => !foundCodes.includes(code));
const foundCodes = ports.map(p => p.code);
const missingCodes = portCodes.filter(code => !foundCodes.includes(code));
if (missingCodes.length > 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
const carrierResults = await Promise.allSettled(
connectorsToQuery.map((connector) => this.queryCarrier(connector, input))
connectorsToQuery.map(connector => this.queryCarrier(connector, input))
);
// Process results
@ -140,7 +140,7 @@ export class RateSearchService implements SearchRatesPort {
return this.carrierConnectors;
}
return this.carrierConnectors.filter((connector) =>
return this.carrierConnectors.filter(connector =>
carrierPreferences.includes(connector.getCarrierCode())
);
}

View File

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

View File

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

View File

@ -20,7 +20,7 @@ export class Surcharge {
constructor(
public readonly type: SurchargeType,
public readonly amount: Money,
public readonly description?: string,
public readonly description?: string
) {
this.validate();
}
@ -46,10 +46,7 @@ export class Surcharge {
}
equals(other: Surcharge): boolean {
return (
this.type === other.type &&
this.amount.isEqualTo(other.amount)
);
return this.type === other.type && this.amount.isEqualTo(other.amount);
}
toString(): string {
@ -70,15 +67,16 @@ export class SurchargeCollection {
* In production, currency conversion would be needed
*/
getTotalAmount(currency: string): Money {
const relevantSurcharges = this.surcharges
.filter((s) => s.amount.getCurrency() === currency);
const relevantSurcharges = this.surcharges.filter(s => s.amount.getCurrency() === currency);
if (relevantSurcharges.length === 0) {
return Money.zero(currency);
}
return relevantSurcharges
.reduce((total, surcharge) => total.add(surcharge.amount), Money.zero(currency));
return relevantSurcharges.reduce(
(total, surcharge) => total.add(surcharge.amount),
Money.zero(currency)
);
}
/**
@ -92,7 +90,7 @@ export class SurchargeCollection {
* Get surcharges by type
*/
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()) {
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 {
constructor(
public readonly cbm: number,
public readonly weightKG: number,
public readonly weightKG: number
) {
this.validate();
}

View File

@ -31,7 +31,7 @@ export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestr
port,
password,
db,
retryStrategy: (times) => {
retryStrategy: times => {
const delay = Math.min(times * 50, 2000);
return delay;
},
@ -42,7 +42,7 @@ export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestr
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}`);
});
@ -112,7 +112,9 @@ export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestr
const result = await this.client.exists(key);
return result === 1;
} 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;
}
}

View File

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

View File

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

View File

@ -30,11 +30,31 @@ export class CMACGMRequestMapper {
return cgmResponse.quotations.map((quotation: any) => {
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: '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);
{
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: '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 totalSurcharges = surcharges.reduce((sum, s) => sum + s.amount, 0);
@ -53,7 +73,10 @@ export class CMACGMRequestMapper {
});
// 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) => {
route.push({
portCode: port.code || port,
@ -69,7 +92,12 @@ export class CMACGMRequestMapper {
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({
id: uuidv4(),

View File

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

View File

@ -9,7 +9,7 @@ import { ConfigService } from '@nestjs/config';
import {
CarrierConnectorPort,
CarrierRateSearchInput,
CarrierAvailabilityInput
CarrierAvailabilityInput,
} from '../../../domain/ports/out/carrier-connector.port';
import { RateQuote } from '../../../domain/entities/rate-quote.entity';
import { BaseCarrierConnector, CarrierConfig } from '../base-carrier.connector';
@ -25,7 +25,7 @@ export class HapagLloydConnectorAdapter
constructor(
private readonly configService: ConfigService,
private readonly requestMapper: HapagLloydRequestMapper,
private readonly requestMapper: HapagLloydRequestMapper
) {
const config: CarrierConfig = {
name: 'Hapag-Lloyd',
@ -91,7 +91,9 @@ export class HapagLloydConnectorAdapter
return (response.data as any).available_capacity || 0;
} 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;
}
}

View File

@ -91,7 +91,12 @@ export class HapagLloydRequestMapper {
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({
id: uuidv4(),

View File

@ -17,7 +17,7 @@ export class MaerskResponseMapper {
originCode: string,
destinationCode: string
): 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,
destinationCode: string
): RateQuote {
const surcharges = result.pricing.charges.map((charge) => ({
const surcharges = result.pricing.charges.map(charge => ({
type: charge.chargeCode,
description: charge.chargeName,
amount: charge.amount,
currency: charge.currency,
}));
const route = result.schedule.routeSchedule.map((segment) =>
this.mapRouteSegment(segment)
);
const route = result.schedule.routeSchedule.map(segment => this.mapRouteSegment(segment));
return RateQuote.create({
id: uuidv4(),

View File

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

View File

@ -58,7 +58,7 @@ export class MSCRequestMapper {
amount: quote.surcharges?.pss || 0,
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 baseFreight = quote.ocean_freight || 0;

View File

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

View File

@ -78,7 +78,8 @@ export class ONERequestMapper {
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({
id: uuidv4(),
@ -130,7 +131,7 @@ export class ONERequestMapper {
private formatChargeName(key: string): string {
return key
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,10 +12,7 @@ import {
} from '../../../../domain/entities/booking.entity';
import { BookingNumber } from '../../../../domain/value-objects/booking-number.vo';
import { BookingStatus } from '../../../../domain/value-objects/booking-status.vo';
import {
BookingOrmEntity,
PartyJson,
} from '../entities/booking.orm-entity';
import { BookingOrmEntity, PartyJson } from '../entities/booking.orm-entity';
import { ContainerOrmEntity } from '../entities/container.orm-entity';
export class BookingOrmMapper {
@ -39,9 +36,7 @@ export class BookingOrmMapper {
orm.updatedAt = domain.updatedAt;
// Map containers
orm.containers = domain.containers.map((container) =>
this.containerToOrm(container, domain.id)
);
orm.containers = domain.containers.map(container => this.containerToOrm(container, domain.id));
return orm;
}
@ -60,9 +55,7 @@ export class BookingOrmMapper {
shipper: this.jsonToParty(orm.shipper),
consignee: this.jsonToParty(orm.consignee),
cargoDescription: orm.cargoDescription,
containers: orm.containers
? orm.containers.map((c) => this.ormToContainer(c))
: [],
containers: orm.containers ? orm.containers.map(c => this.ormToContainer(c)) : [],
specialInstructions: orm.specialInstructions || undefined,
createdAt: orm.createdAt,
updatedAt: orm.updatedAt,
@ -79,7 +72,7 @@ export class BookingOrmMapper {
* Map array of ORM entities to domain entities
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
@ -95,7 +95,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({
name: 'idx_audit_logs_organization_timestamp',
columnNames: ['organization_id', 'timestamp'],
}),
})
);
await queryRunner.createIndex(
@ -103,7 +103,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({
name: 'idx_audit_logs_user_timestamp',
columnNames: ['user_id', 'timestamp'],
}),
})
);
await queryRunner.createIndex(
@ -111,7 +111,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({
name: 'idx_audit_logs_resource',
columnNames: ['resource_type', 'resource_id'],
}),
})
);
await queryRunner.createIndex(
@ -119,7 +119,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({
name: 'idx_audit_logs_action',
columnNames: ['action'],
}),
})
);
await queryRunner.createIndex(
@ -127,7 +127,7 @@ export class CreateAuditLogsTable1700000001000 implements MigrationInterface {
new TableIndex({
name: 'idx_audit_logs_timestamp',
columnNames: ['timestamp'],
}),
})
);
}

View File

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

View File

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

View File

@ -19,7 +19,11 @@ export class SeedCarriersAndOrganizations1730000000006 implements MigrationInter
public async down(queryRunner: QueryRunner): Promise<void> {
// Delete seeded data
await queryRunner.query(`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.')`);
await queryRunner.query(
`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) {
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;

View File

@ -16,7 +16,7 @@ import { AuditLogOrmEntity } from '../entities/audit-log.orm-entity';
export class TypeOrmAuditLogRepository implements AuditLogRepository {
constructor(
@InjectRepository(AuditLogOrmEntity)
private readonly ormRepository: Repository<AuditLogOrmEntity>,
private readonly ormRepository: Repository<AuditLogOrmEntity>
) {}
async save(auditLog: AuditLog): Promise<void> {
@ -77,7 +77,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
}
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> {
@ -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[]> {
@ -145,7 +145,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
take: limit,
});
return ormEntities.map((e) => this.toDomain(e));
return ormEntities.map(e => this.toDomain(e));
}
async findByUser(userId: string, limit: number): Promise<AuditLog[]> {
@ -159,7 +159,7 @@ export class TypeOrmAuditLogRepository implements AuditLogRepository {
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[]> {
const orms = carriers.map((carrier) => CarrierOrmMapper.toOrm(carrier));
const orms = carriers.map(carrier => CarrierOrmMapper.toOrm(carrier));
const saved = await this.repository.save(orms);
return CarrierOrmMapper.toDomainMany(saved);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -76,7 +76,7 @@ export const carrierSeeds: CarrierSeed[] = [
export function getCarriersInsertSQL(): string {
const values = carrierSeeds
.map(
(carrier) =>
carrier =>
`('${carrier.id}', '${carrier.name}', '${carrier.code}', '${carrier.scac}', ` +
`'${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 {
const values = organizationSeeds
.map(
(org) =>
org =>
`('${org.id}', '${org.name}', '${org.type}', ` +
`${org.scac ? `'${org.scac}'` : 'NULL'}, ` +
`'${org.addressStreet}', '${org.addressCity}', ` +

View File

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

View File

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

View File

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

View File

@ -6,10 +6,7 @@ import helmet from 'helmet';
import * as compression from 'compression';
import { AppModule } from './app.module';
import { Logger } from 'nestjs-pino';
import {
helmetConfig,
corsConfig,
} from './infrastructure/security/security.config';
import { helmetConfig, corsConfig } from './infrastructure/security/security.config';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
@ -50,14 +47,14 @@ async function bootstrap() {
transformOptions: {
enableImplicitConversion: true,
},
}),
})
);
// Swagger documentation
const config = new DocumentBuilder()
.setTitle('Xpeditis API')
.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')
.addBearerAuth()

View File

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

View File

@ -309,7 +309,7 @@ describe('TypeOrmBookingRepository (Integration)', () => {
const bookings = await repository.findByOrganization(testOrganization.id);
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 () => {
@ -338,8 +338,8 @@ describe('TypeOrmBookingRepository (Integration)', () => {
expect(draftBookings).toHaveLength(2);
expect(confirmedBookings).toHaveLength(1);
expect(draftBookings.every((b) => b.status.value === 'draft')).toBe(true);
expect(confirmedBookings.every((b) => b.status.value === 'confirmed')).toBe(true);
expect(draftBookings.every(b => b.status.value === 'draft')).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(Array.isArray(quotes[0].route)).toBe(true);
// 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);
});