fix
This commit is contained in:
parent
22b17ef8c3
commit
68e321a08f
@ -11,7 +11,11 @@
|
||||
"Bash(git commit:*)",
|
||||
"Bash(k6:*)",
|
||||
"Bash(npx playwright:*)",
|
||||
"Bash(npx newman:*)"
|
||||
"Bash(npx newman:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(netstat -ano)",
|
||||
"Bash(findstr \":5432\")",
|
||||
"Bash(findstr \"LISTENING\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@ -69,7 +69,7 @@ temp
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose*.yml
|
||||
docker-compose.yaml
|
||||
|
||||
# CI/CD
|
||||
.gitlab-ci.yml
|
||||
|
||||
19
apps/backend/docker-compose.yaml
Normal file
19
apps/backend/docker-compose.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
container_name: xpeditis-postgres
|
||||
environment:
|
||||
POSTGRES_USER: xpeditis
|
||||
POSTGRES_PASSWORD: xpeditis_dev_password
|
||||
POSTGRES_DB: xpeditis_dev
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: xpeditis-redis
|
||||
command: redis-server --requirepass xpeditis_redis_password
|
||||
environment:
|
||||
REDIS_PASSWORD: xpeditis_redis_password
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { AuthController } from '../controllers/auth.controller';
|
||||
@ -9,18 +10,8 @@ import { AuthController } from '../controllers/auth.controller';
|
||||
// Import domain and infrastructure dependencies
|
||||
import { USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
||||
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
||||
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||
|
||||
/**
|
||||
* Authentication Module
|
||||
*
|
||||
* Wires together the authentication system:
|
||||
* - JWT configuration with access/refresh tokens
|
||||
* - Passport JWT strategy
|
||||
* - Auth service and controller
|
||||
* - User repository for database access
|
||||
*
|
||||
* This module should be imported in AppModule.
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
// Passport configuration
|
||||
@ -37,6 +28,9 @@ import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
// 👇 Add this to register TypeORM repository for UserOrmEntity
|
||||
TypeOrmModule.forFeature([UserOrmEntity]),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable, UnauthorizedException, ConflictException, Logger } 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';
|
||||
import { UserRepository } from '../../domain/ports/out/user.repository';
|
||||
import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
||||
import { User, UserRole } from '../../domain/entities/user.entity';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@ -19,7 +19,8 @@ export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly userRepository: UserRepository, // ✅ Correct injection
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
@ -36,14 +37,12 @@ export class AuthService {
|
||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||
this.logger.log(`Registering new user: ${email}`);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await this.userRepository.findByEmail(email);
|
||||
|
||||
if (existingUser) {
|
||||
throw new ConflictException('User with this email already exists');
|
||||
}
|
||||
|
||||
// Hash password with Argon2
|
||||
const passwordHash = await argon2.hash(password, {
|
||||
type: argon2.argon2id,
|
||||
memoryCost: 65536, // 64 MB
|
||||
@ -51,7 +50,6 @@ export class AuthService {
|
||||
parallelism: 4,
|
||||
});
|
||||
|
||||
// Create user entity
|
||||
const user = User.create({
|
||||
id: uuidv4(),
|
||||
organizationId,
|
||||
@ -59,13 +57,11 @@ export class AuthService {
|
||||
passwordHash,
|
||||
firstName,
|
||||
lastName,
|
||||
role: UserRole.USER, // Default role
|
||||
role: UserRole.USER,
|
||||
});
|
||||
|
||||
// Save to database
|
||||
const savedUser = await this.userRepository.save(user);
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(savedUser);
|
||||
|
||||
this.logger.log(`User registered successfully: ${email}`);
|
||||
@ -92,7 +88,6 @@ export class AuthService {
|
||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||
this.logger.log(`Login attempt for: ${email}`);
|
||||
|
||||
// Find user by email
|
||||
const user = await this.userRepository.findByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
@ -103,14 +98,12 @@ export class AuthService {
|
||||
throw new UnauthorizedException('User account is inactive');
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await argon2.verify(user.passwordHash, password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.generateTokens(user);
|
||||
|
||||
this.logger.log(`User logged in successfully: ${email}`);
|
||||
@ -133,7 +126,6 @@ export class AuthService {
|
||||
*/
|
||||
async refreshAccessToken(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
||||
try {
|
||||
// Verify refresh token
|
||||
const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
|
||||
secret: this.configService.get('JWT_SECRET'),
|
||||
});
|
||||
@ -142,14 +134,12 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Invalid token type');
|
||||
}
|
||||
|
||||
// Get user
|
||||
const user = await this.userRepository.findById(payload.sub);
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException('User not found or inactive');
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
const tokens = await this.generateTokens(user);
|
||||
|
||||
this.logger.log(`Access token refreshed for user: ${user.email}`);
|
||||
|
||||
@ -101,17 +101,13 @@ export class UsersController {
|
||||
})
|
||||
async createUser(
|
||||
@Body() dto: CreateUserDto,
|
||||
@CurrentUser() user: UserPayload,
|
||||
@CurrentUser() user: UserPayload
|
||||
): Promise<UserResponseDto> {
|
||||
this.logger.log(
|
||||
`[User: ${user.email}] Creating user: ${dto.email} (${dto.role})`,
|
||||
);
|
||||
this.logger.log(`[User: ${user.email}] Creating user: ${dto.email} (${dto.role})`);
|
||||
|
||||
// Authorization: Managers can only create users in their own organization
|
||||
if (user.role === 'manager' && dto.organizationId !== user.organizationId) {
|
||||
throw new ForbiddenException(
|
||||
'You can only create users in your own organization',
|
||||
);
|
||||
throw new ForbiddenException('You can only create users in your own organization');
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
@ -121,8 +117,7 @@ export class UsersController {
|
||||
}
|
||||
|
||||
// Generate temporary password if not provided
|
||||
const tempPassword =
|
||||
dto.password || this.generateTemporaryPassword();
|
||||
const tempPassword = dto.password || this.generateTemporaryPassword();
|
||||
|
||||
// Hash password with Argon2id
|
||||
const passwordHash = await argon2.hash(tempPassword, {
|
||||
@ -153,7 +148,7 @@ export class UsersController {
|
||||
|
||||
// TODO: Send invitation email with temporary password
|
||||
this.logger.warn(
|
||||
`TODO: Send invitation email to ${dto.email} with temp password: ${tempPassword}`,
|
||||
`TODO: Send invitation email to ${dto.email} with temp password: ${tempPassword}`
|
||||
);
|
||||
|
||||
return UserMapper.toDto(savedUser);
|
||||
@ -165,8 +160,7 @@ export class UsersController {
|
||||
@Get(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Get user by ID',
|
||||
description:
|
||||
'Retrieve user details. Users can view users in their org, admins can view any.',
|
||||
description: 'Retrieve user details. Users can view users in their org, admins can view any.',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
@ -183,7 +177,7 @@ export class UsersController {
|
||||
})
|
||||
async getUser(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() currentUser: UserPayload,
|
||||
@CurrentUser() currentUser: UserPayload
|
||||
): Promise<UserResponseDto> {
|
||||
this.logger.log(`[User: ${currentUser.email}] Fetching user: ${id}`);
|
||||
|
||||
@ -193,10 +187,7 @@ export class UsersController {
|
||||
}
|
||||
|
||||
// Authorization: Can only view users in same organization (unless admin)
|
||||
if (
|
||||
currentUser.role !== 'admin' &&
|
||||
user.organizationId !== currentUser.organizationId
|
||||
) {
|
||||
if (currentUser.role !== 'admin' && user.organizationId !== currentUser.organizationId) {
|
||||
throw new ForbiddenException('You can only view users in your organization');
|
||||
}
|
||||
|
||||
@ -211,8 +202,7 @@ export class UsersController {
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
@ApiOperation({
|
||||
summary: 'Update user',
|
||||
description:
|
||||
'Update user details (name, role, status). Admin/manager only.',
|
||||
description: 'Update user details (name, role, status). Admin/manager only.',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
@ -233,7 +223,7 @@ export class UsersController {
|
||||
async updateUser(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateUserDto,
|
||||
@CurrentUser() currentUser: UserPayload,
|
||||
@CurrentUser() currentUser: UserPayload
|
||||
): Promise<UserResponseDto> {
|
||||
this.logger.log(`[User: ${currentUser.email}] Updating user: ${id}`);
|
||||
|
||||
@ -243,13 +233,8 @@ export class UsersController {
|
||||
}
|
||||
|
||||
// Authorization: Managers can only update users in their own organization
|
||||
if (
|
||||
currentUser.role === 'manager' &&
|
||||
user.organizationId !== currentUser.organizationId
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
'You can only update users in your own organization',
|
||||
);
|
||||
if (currentUser.role === 'manager' && user.organizationId !== currentUser.organizationId) {
|
||||
throw new ForbiddenException('You can only update users in your own organization');
|
||||
}
|
||||
|
||||
// Update fields
|
||||
@ -308,7 +293,7 @@ export class UsersController {
|
||||
})
|
||||
async deleteUser(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() currentUser: UserPayload,
|
||||
@CurrentUser() currentUser: UserPayload
|
||||
): Promise<void> {
|
||||
this.logger.log(`[Admin: ${currentUser.email}] Deactivating user: ${id}`);
|
||||
|
||||
@ -360,21 +345,17 @@ export class UsersController {
|
||||
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
||||
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
|
||||
@Query('role') role: string | undefined,
|
||||
@CurrentUser() currentUser: UserPayload,
|
||||
@CurrentUser() currentUser: UserPayload
|
||||
): Promise<UserListResponseDto> {
|
||||
this.logger.log(
|
||||
`[User: ${currentUser.email}] Listing users: page=${page}, pageSize=${pageSize}, role=${role}`,
|
||||
`[User: ${currentUser.email}] Listing users: page=${page}, pageSize=${pageSize}, role=${role}`
|
||||
);
|
||||
|
||||
// Fetch users by organization
|
||||
const users = await this.userRepository.findByOrganization(
|
||||
currentUser.organizationId,
|
||||
);
|
||||
const users = await this.userRepository.findByOrganization(currentUser.organizationId);
|
||||
|
||||
// Filter by role if provided
|
||||
const filteredUsers = role
|
||||
? users.filter(u => u.role === role)
|
||||
: users;
|
||||
const filteredUsers = role ? users.filter(u => u.role === role) : users;
|
||||
|
||||
// Paginate
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
@ -418,7 +399,7 @@ export class UsersController {
|
||||
})
|
||||
async updatePassword(
|
||||
@Body() dto: UpdatePasswordDto,
|
||||
@CurrentUser() currentUser: UserPayload,
|
||||
@CurrentUser() currentUser: UserPayload
|
||||
): Promise<{ message: string }> {
|
||||
this.logger.log(`[User: ${currentUser.email}] Updating password`);
|
||||
|
||||
@ -428,10 +409,7 @@ export class UsersController {
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isPasswordValid = await argon2.verify(
|
||||
user.passwordHash,
|
||||
dto.currentPassword,
|
||||
);
|
||||
const isPasswordValid = await argon2.verify(user.passwordHash, dto.currentPassword);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new ForbiddenException('Current password is incorrect');
|
||||
@ -459,8 +437,7 @@ export class UsersController {
|
||||
*/
|
||||
private generateTemporaryPassword(): string {
|
||||
const length = 16;
|
||||
const charset =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
|
||||
const randomBytes = crypto.randomBytes(length);
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { OrganizationsController } from '../controllers/organizations.controller';
|
||||
|
||||
// Import domain ports
|
||||
import { ORGANIZATION_REPOSITORY } from '../../domain/ports/out/organization.repository';
|
||||
import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-organization.repository';
|
||||
import { OrganizationOrmEntity } from '../../infrastructure/persistence/typeorm/entities/organization.orm-entity';
|
||||
|
||||
/**
|
||||
* Organizations Module
|
||||
*
|
||||
* Handles organization management functionality:
|
||||
* - Create organizations (admin only)
|
||||
* - View organization details
|
||||
* - Update organization (admin/manager)
|
||||
* - List organizations
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([OrganizationOrmEntity]), // 👈 This line registers the repository provider
|
||||
],
|
||||
controllers: [OrganizationsController],
|
||||
providers: [
|
||||
{
|
||||
@ -22,6 +18,8 @@ import { TypeOrmOrganizationRepository } from '../../infrastructure/persistence/
|
||||
useClass: TypeOrmOrganizationRepository,
|
||||
},
|
||||
],
|
||||
exports: [],
|
||||
exports: [
|
||||
ORGANIZATION_REPOSITORY, // optional, if other modules need it
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { RatesController } from '../controllers/rates.controller';
|
||||
import { CacheModule } from '../../infrastructure/cache/cache.module';
|
||||
import { CarrierModule } from '../../infrastructure/carriers/carrier.module';
|
||||
@ -6,18 +7,14 @@ import { CarrierModule } from '../../infrastructure/carriers/carrier.module';
|
||||
// Import domain ports
|
||||
import { RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.repository';
|
||||
import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-rate-quote.repository';
|
||||
import { RateQuoteOrmEntity } from '../../infrastructure/persistence/typeorm/entities/rate-quote.orm-entity';
|
||||
|
||||
/**
|
||||
* Rates Module
|
||||
*
|
||||
* Handles rate search functionality:
|
||||
* - Rate search API endpoint
|
||||
* - Integration with carrier APIs
|
||||
* - Redis caching for rate quotes
|
||||
* - Rate quote persistence
|
||||
*/
|
||||
@Module({
|
||||
imports: [CacheModule, CarrierModule],
|
||||
imports: [
|
||||
CacheModule,
|
||||
CarrierModule,
|
||||
TypeOrmModule.forFeature([RateQuoteOrmEntity]), // 👈 Add this
|
||||
],
|
||||
controllers: [RatesController],
|
||||
providers: [
|
||||
{
|
||||
@ -25,6 +22,8 @@ import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typ
|
||||
useClass: TypeOrmRateQuoteRepository,
|
||||
},
|
||||
],
|
||||
exports: [],
|
||||
exports: [
|
||||
RATE_QUOTE_REPOSITORY, // optional, if used in other modules
|
||||
],
|
||||
})
|
||||
export class RatesModule {}
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UsersController } from '../controllers/users.controller';
|
||||
|
||||
// Import domain ports
|
||||
import { USER_REPOSITORY } from '../../domain/ports/out/user.repository';
|
||||
import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-user.repository';
|
||||
import { UserOrmEntity } from '../../infrastructure/persistence/typeorm/entities/user.orm-entity';
|
||||
|
||||
/**
|
||||
* Users Module
|
||||
*
|
||||
* Handles user management functionality:
|
||||
* - Create/invite users (admin/manager)
|
||||
* - View user details
|
||||
* - Update user (admin/manager)
|
||||
* - Deactivate user (admin)
|
||||
* - List users in organization
|
||||
* - Update own password
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([UserOrmEntity]), // 👈 Add this line
|
||||
],
|
||||
controllers: [UsersController],
|
||||
providers: [
|
||||
{
|
||||
@ -24,6 +18,8 @@ import { TypeOrmUserRepository } from '../../infrastructure/persistence/typeorm/
|
||||
useClass: TypeOrmUserRepository,
|
||||
},
|
||||
],
|
||||
exports: [],
|
||||
exports: [
|
||||
USER_REPOSITORY, // optional, export if other modules need it
|
||||
],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
/**
|
||||
* BookingService (Domain Service)
|
||||
*
|
||||
* Business logic for booking management
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Booking, BookingContainer } from '../entities/booking.entity';
|
||||
import { BookingNumber } from '../value-objects/booking-number.vo';
|
||||
import { BookingStatus } from '../value-objects/booking-status.vo';
|
||||
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||
import { Booking } from '../entities/booking.entity';
|
||||
import { BookingRepository } from '../ports/out/booking.repository';
|
||||
import { RateQuoteRepository } from '../ports/out/rate-quote.repository';
|
||||
import { BOOKING_REPOSITORY } from '../ports/out/booking.repository';
|
||||
import { RATE_QUOTE_REPOSITORY } from '../ports/out/rate-quote.repository';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface CreateBookingInput {
|
||||
@ -24,7 +18,10 @@ export interface CreateBookingInput {
|
||||
@Injectable()
|
||||
export class BookingService {
|
||||
constructor(
|
||||
@Inject(BOOKING_REPOSITORY)
|
||||
private readonly bookingRepository: BookingRepository,
|
||||
|
||||
@Inject(RATE_QUOTE_REPOSITORY)
|
||||
private readonly rateQuoteRepository: RateQuoteRepository
|
||||
) {}
|
||||
|
||||
@ -35,7 +32,7 @@ export class BookingService {
|
||||
// Validate rate quote exists
|
||||
const rateQuote = await this.rateQuoteRepository.findById(input.rateQuoteId);
|
||||
if (!rateQuote) {
|
||||
throw new Error(`Rate quote ${input.rateQuoteId} not found`);
|
||||
throw new NotFoundException(`Rate quote ${input.rateQuoteId} not found`);
|
||||
}
|
||||
|
||||
// TODO: Get userId and organizationId from context
|
||||
@ -51,7 +48,7 @@ export class BookingService {
|
||||
shipper: input.shipper,
|
||||
consignee: input.consignee,
|
||||
cargoDescription: input.cargoDescription,
|
||||
containers: input.containers.map((c) => ({
|
||||
containers: input.containers.map(c => ({
|
||||
id: uuidv4(),
|
||||
type: c.type,
|
||||
containerNumber: c.containerNumber,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import { Logger } from '@nestjs/common';
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import CircuitBreaker from 'opossum';
|
||||
import * as CircuitBreaker from 'opossum'; // ✅ Correction ici
|
||||
import {
|
||||
CarrierConnectorPort,
|
||||
CarrierRateSearchInput,
|
||||
@ -45,28 +45,28 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
},
|
||||
});
|
||||
|
||||
// Add request interceptor for logging
|
||||
// Request interceptor
|
||||
this.httpClient.interceptors.request.use(
|
||||
(request: any) => {
|
||||
request => {
|
||||
this.logger.debug(
|
||||
`Request: ${request.method?.toUpperCase()} ${request.url}`,
|
||||
request.data ? JSON.stringify(request.data).substring(0, 200) : ''
|
||||
);
|
||||
return request;
|
||||
},
|
||||
(error: any) => {
|
||||
error => {
|
||||
this.logger.error(`Request error: ${error?.message || 'Unknown error'}`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Add response interceptor for logging
|
||||
// Response interceptor
|
||||
this.httpClient.interceptors.response.use(
|
||||
(response: any) => {
|
||||
response => {
|
||||
this.logger.debug(`Response: ${response.status} ${response.statusText}`);
|
||||
return response;
|
||||
},
|
||||
(error: any) => {
|
||||
error => {
|
||||
if (error?.code === 'ECONNABORTED') {
|
||||
this.logger.warn(`Request timeout after ${config.timeout}ms`);
|
||||
throw new CarrierTimeoutException(config.name, config.timeout);
|
||||
@ -76,7 +76,7 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
}
|
||||
);
|
||||
|
||||
// Create circuit breaker
|
||||
// Circuit breaker
|
||||
this.circuitBreaker = new CircuitBreaker(this.makeRequest.bind(this), {
|
||||
timeout: config.timeout,
|
||||
errorThresholdPercentage: config.circuitBreakerThreshold,
|
||||
@ -84,18 +84,15 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
name: `${config.name}-circuit-breaker`,
|
||||
});
|
||||
|
||||
// Circuit breaker event handlers
|
||||
this.circuitBreaker.on('open', () => {
|
||||
this.logger.warn('Circuit breaker opened - carrier unavailable');
|
||||
});
|
||||
|
||||
this.circuitBreaker.on('halfOpen', () => {
|
||||
this.logger.log('Circuit breaker half-open - testing carrier availability');
|
||||
});
|
||||
|
||||
this.circuitBreaker.on('close', () => {
|
||||
this.logger.log('Circuit breaker closed - carrier available');
|
||||
});
|
||||
this.circuitBreaker.on('open', () =>
|
||||
this.logger.warn('Circuit breaker opened - carrier unavailable')
|
||||
);
|
||||
this.circuitBreaker.on('halfOpen', () =>
|
||||
this.logger.log('Circuit breaker half-open - testing carrier availability')
|
||||
);
|
||||
this.circuitBreaker.on('close', () =>
|
||||
this.logger.log('Circuit breaker closed - carrier available')
|
||||
);
|
||||
}
|
||||
|
||||
getCarrierName(): string {
|
||||
@ -106,9 +103,6 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
return this.config.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request with retry logic
|
||||
*/
|
||||
protected async makeRequest<T>(
|
||||
config: AxiosRequestConfig,
|
||||
retries = this.config.maxRetries
|
||||
@ -126,41 +120,27 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if error is retryable
|
||||
*/
|
||||
protected isRetryableError(error: any): boolean {
|
||||
// Retry on network errors, timeouts, and 5xx server errors
|
||||
if (error.code === 'ECONNABORTED') return false; // Don't retry timeouts
|
||||
if (error.code === 'ENOTFOUND') return false; // Don't retry DNS errors
|
||||
if (error.code === 'ECONNABORTED') return false;
|
||||
if (error.code === 'ENOTFOUND') return false;
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
return status >= 500 && status < 600;
|
||||
}
|
||||
return true; // Retry network errors
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate retry delay with exponential backoff
|
||||
*/
|
||||
protected calculateRetryDelay(attempt: number): number {
|
||||
const baseDelay = 1000; // 1 second
|
||||
const maxDelay = 5000; // 5 seconds
|
||||
const baseDelay = 1000;
|
||||
const maxDelay = 5000;
|
||||
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
||||
// Add jitter to prevent thundering herd
|
||||
return delay + Math.random() * 1000;
|
||||
return delay + Math.random() * 1000; // jitter
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep utility
|
||||
*/
|
||||
protected sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make request with circuit breaker protection
|
||||
*/
|
||||
protected async requestWithCircuitBreaker<T>(
|
||||
config: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<T>> {
|
||||
@ -174,16 +154,9 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check implementation
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await this.requestWithCircuitBreaker({
|
||||
method: 'GET',
|
||||
url: '/health',
|
||||
timeout: 5000,
|
||||
});
|
||||
await this.requestWithCircuitBreaker({ method: 'GET', url: '/health', timeout: 5000 });
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Health check failed: ${error?.message || 'Unknown error'}`);
|
||||
@ -191,9 +164,6 @@ export abstract class BaseCarrierConnector implements CarrierConnectorPort {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract methods to be implemented by specific carriers
|
||||
*/
|
||||
abstract searchRates(input: CarrierRateSearchInput): Promise<RateQuote[]>;
|
||||
abstract checkAvailability(input: CarrierAvailabilityInput): Promise<number>;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user