fix test
This commit is contained in:
parent
177606bbbe
commit
cfef7005b3
@ -3,8 +3,7 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import { UserRepository } from '../../domain/ports/out/user.repository';
|
import { UserRepository } from '../../domain/ports/out/user.repository';
|
||||||
import { User } from '../../domain/entities/user.entity';
|
import { User, UserRole } from '../../domain/entities/user.entity';
|
||||||
import { Email } from '../../domain/value-objects/email.vo';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
@ -38,8 +37,7 @@ export class AuthService {
|
|||||||
this.logger.log(`Registering new user: ${email}`);
|
this.logger.log(`Registering new user: ${email}`);
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const emailVo = Email.create(email);
|
const existingUser = await this.userRepository.findByEmail(email);
|
||||||
const existingUser = await this.userRepository.findByEmail(emailVo);
|
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ConflictException('User with this email already exists');
|
throw new ConflictException('User with this email already exists');
|
||||||
@ -57,14 +55,11 @@ export class AuthService {
|
|||||||
const user = User.create({
|
const user = User.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
organizationId,
|
organizationId,
|
||||||
email: emailVo,
|
email,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
role: 'user', // Default role
|
role: UserRole.USER, // Default role
|
||||||
isActive: true,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@ -79,7 +74,7 @@ export class AuthService {
|
|||||||
...tokens,
|
...tokens,
|
||||||
user: {
|
user: {
|
||||||
id: savedUser.id,
|
id: savedUser.id,
|
||||||
email: savedUser.email.value,
|
email: savedUser.email,
|
||||||
firstName: savedUser.firstName,
|
firstName: savedUser.firstName,
|
||||||
lastName: savedUser.lastName,
|
lastName: savedUser.lastName,
|
||||||
role: savedUser.role,
|
role: savedUser.role,
|
||||||
@ -98,8 +93,7 @@ export class AuthService {
|
|||||||
this.logger.log(`Login attempt for: ${email}`);
|
this.logger.log(`Login attempt for: ${email}`);
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email
|
||||||
const emailVo = Email.create(email);
|
const user = await this.userRepository.findByEmail(email);
|
||||||
const user = await this.userRepository.findByEmail(emailVo);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException('Invalid credentials');
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
@ -125,7 +119,7 @@ export class AuthService {
|
|||||||
...tokens,
|
...tokens,
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email.value,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
@ -158,7 +152,7 @@ export class AuthService {
|
|||||||
// Generate new tokens
|
// Generate new tokens
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
|
|
||||||
this.logger.log(`Access token refreshed for user: ${user.email.value}`);
|
this.logger.log(`Access token refreshed for user: ${user.email}`);
|
||||||
|
|
||||||
return tokens;
|
return tokens;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -186,7 +180,7 @@ export class AuthService {
|
|||||||
private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
|
private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
|
||||||
const accessPayload: JwtPayload = {
|
const accessPayload: JwtPayload = {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
email: user.email.value,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
organizationId: user.organizationId,
|
organizationId: user.organizationId,
|
||||||
type: 'access',
|
type: 'access',
|
||||||
@ -194,7 +188,7 @@ export class AuthService {
|
|||||||
|
|
||||||
const refreshPayload: JwtPayload = {
|
const refreshPayload: JwtPayload = {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
email: user.email.value,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
organizationId: user.organizationId,
|
organizationId: user.organizationId,
|
||||||
type: 'refresh',
|
type: 'refresh',
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
// This object will be attached to request.user
|
// This object will be attached to request.user
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email.value,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
organizationId: user.organizationId,
|
organizationId: user.organizationId,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { BookingsController } from '../controllers/bookings.controller';
|
import { BookingsController } from '../controllers/bookings.controller';
|
||||||
|
|
||||||
// Import domain ports
|
// Import domain ports
|
||||||
@ -7,6 +8,11 @@ import { RATE_QUOTE_REPOSITORY } from '../../domain/ports/out/rate-quote.reposit
|
|||||||
import { TypeOrmBookingRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-booking.repository';
|
import { TypeOrmBookingRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-booking.repository';
|
||||||
import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-rate-quote.repository';
|
import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typeorm/repositories/typeorm-rate-quote.repository';
|
||||||
|
|
||||||
|
// Import ORM entities
|
||||||
|
import { BookingOrmEntity } from '../../infrastructure/persistence/typeorm/entities/booking.orm-entity';
|
||||||
|
import { ContainerOrmEntity } from '../../infrastructure/persistence/typeorm/entities/container.orm-entity';
|
||||||
|
import { RateQuoteOrmEntity } from '../../infrastructure/persistence/typeorm/entities/rate-quote.orm-entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bookings Module
|
* Bookings Module
|
||||||
*
|
*
|
||||||
@ -17,6 +23,9 @@ import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typ
|
|||||||
* - Update booking status
|
* - Update booking status
|
||||||
*/
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([BookingOrmEntity, ContainerOrmEntity, RateQuoteOrmEntity]),
|
||||||
|
],
|
||||||
controllers: [BookingsController],
|
controllers: [BookingsController],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -28,6 +37,6 @@ import { TypeOrmRateQuoteRepository } from '../../infrastructure/persistence/typ
|
|||||||
useClass: TypeOrmRateQuoteRepository,
|
useClass: TypeOrmRateQuoteRepository,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [BOOKING_REPOSITORY],
|
||||||
})
|
})
|
||||||
export class BookingsModule {}
|
export class BookingsModule {}
|
||||||
|
|||||||
@ -150,10 +150,10 @@ export class AuthController {
|
|||||||
async refresh(
|
async refresh(
|
||||||
@Body() dto: RefreshTokenDto,
|
@Body() dto: RefreshTokenDto,
|
||||||
): Promise<{ accessToken: string }> {
|
): Promise<{ accessToken: string }> {
|
||||||
const accessToken =
|
const result =
|
||||||
await this.authService.refreshAccessToken(dto.refreshToken);
|
await this.authService.refreshAccessToken(dto.refreshToken);
|
||||||
|
|
||||||
return { accessToken };
|
return { accessToken: result.accessToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -39,8 +39,7 @@ import {
|
|||||||
} from '../dto/user.dto';
|
} from '../dto/user.dto';
|
||||||
import { UserMapper } from '../mappers/user.mapper';
|
import { UserMapper } from '../mappers/user.mapper';
|
||||||
import { UserRepository } from '../../domain/ports/out/user.repository';
|
import { UserRepository } from '../../domain/ports/out/user.repository';
|
||||||
import { User } from '../../domain/entities/user.entity';
|
import { User, UserRole as DomainUserRole } from '../../domain/entities/user.entity';
|
||||||
import { Email } from '../../domain/value-objects/email.vo';
|
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
import { RolesGuard } from '../guards/roles.guard';
|
import { RolesGuard } from '../guards/roles.guard';
|
||||||
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
||||||
@ -116,8 +115,7 @@ export class UsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const emailVo = Email.create(dto.email);
|
const existingUser = await this.userRepository.findByEmail(dto.email);
|
||||||
const existingUser = await this.userRepository.findByEmail(emailVo);
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new ConflictException('User with this email already exists');
|
throw new ConflictException('User with this email already exists');
|
||||||
}
|
}
|
||||||
@ -134,18 +132,18 @@ export class UsersController {
|
|||||||
parallelism: 4,
|
parallelism: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Map DTO role to Domain role
|
||||||
|
const domainRole = dto.role as unknown as DomainUserRole;
|
||||||
|
|
||||||
// Create user entity
|
// Create user entity
|
||||||
const newUser = User.create({
|
const newUser = User.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
organizationId: dto.organizationId,
|
organizationId: dto.organizationId,
|
||||||
email: emailVo,
|
email: dto.email,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
firstName: dto.firstName,
|
firstName: dto.firstName,
|
||||||
lastName: dto.lastName,
|
lastName: dto.lastName,
|
||||||
role: dto.role,
|
role: domainRole,
|
||||||
isActive: true,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@ -264,7 +262,8 @@ export class UsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dto.role) {
|
if (dto.role) {
|
||||||
user.updateRole(dto.role);
|
const domainRole = dto.role as unknown as DomainUserRole;
|
||||||
|
user.updateRole(domainRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.isActive !== undefined) {
|
if (dto.isActive !== undefined) {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export class UserMapper {
|
|||||||
static toDto(user: User): UserResponseDto {
|
static toDto(user: User): UserResponseDto {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email.value,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
role: user.role as any,
|
role: user.role as any,
|
||||||
|
|||||||
@ -11,10 +11,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
ADMIN = 'ADMIN', // Full system access
|
ADMIN = 'admin', // Full system access
|
||||||
MANAGER = 'MANAGER', // Manage bookings and users within organization
|
MANAGER = 'manager', // Manage bookings and users within organization
|
||||||
USER = 'USER', // Create and view bookings
|
USER = 'user', // Create and view bookings
|
||||||
VIEWER = 'VIEWER', // Read-only access
|
VIEWER = 'viewer', // Read-only access
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProps {
|
export interface UserProps {
|
||||||
@ -182,6 +182,22 @@ export class User {
|
|||||||
this.props.updatedAt = new Date();
|
this.props.updatedAt = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFirstName(firstName: string): void {
|
||||||
|
if (!firstName || firstName.trim().length === 0) {
|
||||||
|
throw new Error('First name cannot be empty.');
|
||||||
|
}
|
||||||
|
this.props.firstName = firstName.trim();
|
||||||
|
this.props.updatedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLastName(lastName: string): void {
|
||||||
|
if (!lastName || lastName.trim().length === 0) {
|
||||||
|
throw new Error('Last name cannot be empty.');
|
||||||
|
}
|
||||||
|
this.props.lastName = lastName.trim();
|
||||||
|
this.props.updatedAt = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
updateProfile(firstName: string, lastName: string, phoneNumber?: string): void {
|
updateProfile(firstName: string, lastName: string, phoneNumber?: string): void {
|
||||||
if (!firstName || firstName.trim().length === 0) {
|
if (!firstName || firstName.trim().length === 0) {
|
||||||
throw new Error('First name cannot be empty.');
|
throw new Error('First name cannot be empty.');
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Booking ORM Entity (Infrastructure Layer)
|
||||||
|
*
|
||||||
|
* TypeORM entity for booking persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Column,
|
||||||
|
PrimaryColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { UserOrmEntity } from './user.orm-entity';
|
||||||
|
import { OrganizationOrmEntity } from './organization.orm-entity';
|
||||||
|
import { ContainerOrmEntity } from './container.orm-entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address stored as JSON
|
||||||
|
*/
|
||||||
|
export interface AddressJson {
|
||||||
|
street: string;
|
||||||
|
city: string;
|
||||||
|
postalCode: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Party (shipper/consignee) stored as JSON
|
||||||
|
*/
|
||||||
|
export interface PartyJson {
|
||||||
|
name: string;
|
||||||
|
address: AddressJson;
|
||||||
|
contactName: string;
|
||||||
|
contactEmail: string;
|
||||||
|
contactPhone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('bookings')
|
||||||
|
@Index('idx_bookings_booking_number', ['bookingNumber'], { unique: true })
|
||||||
|
@Index('idx_bookings_user', ['userId'])
|
||||||
|
@Index('idx_bookings_organization', ['organizationId'])
|
||||||
|
@Index('idx_bookings_rate_quote', ['rateQuoteId'])
|
||||||
|
@Index('idx_bookings_status', ['status'])
|
||||||
|
@Index('idx_bookings_created_at', ['createdAt'])
|
||||||
|
export class BookingOrmEntity {
|
||||||
|
@PrimaryColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'booking_number', type: 'varchar', length: 20, unique: true })
|
||||||
|
bookingNumber: string;
|
||||||
|
|
||||||
|
@Column({ name: 'user_id', type: 'uuid' })
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => UserOrmEntity, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: UserOrmEntity;
|
||||||
|
|
||||||
|
@Column({ name: 'organization_id', type: 'uuid' })
|
||||||
|
organizationId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => OrganizationOrmEntity, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'organization_id' })
|
||||||
|
organization: OrganizationOrmEntity;
|
||||||
|
|
||||||
|
@Column({ name: 'rate_quote_id', type: 'uuid' })
|
||||||
|
rateQuoteId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50 })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
shipper: PartyJson;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb' })
|
||||||
|
consignee: PartyJson;
|
||||||
|
|
||||||
|
@Column({ name: 'cargo_description', type: 'text' })
|
||||||
|
cargoDescription: string;
|
||||||
|
|
||||||
|
@OneToMany(() => ContainerOrmEntity, (container) => container.booking, {
|
||||||
|
cascade: true,
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
containers: ContainerOrmEntity[];
|
||||||
|
|
||||||
|
@Column({ name: 'special_instructions', type: 'text', nullable: true })
|
||||||
|
specialInstructions: string | null;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Container ORM Entity (Infrastructure Layer)
|
||||||
|
*
|
||||||
|
* TypeORM entity for container persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Column,
|
||||||
|
PrimaryColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { BookingOrmEntity } from './booking.orm-entity';
|
||||||
|
|
||||||
|
@Entity('containers')
|
||||||
|
@Index('idx_containers_booking', ['bookingId'])
|
||||||
|
@Index('idx_containers_container_number', ['containerNumber'])
|
||||||
|
export class ContainerOrmEntity {
|
||||||
|
@PrimaryColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'booking_id', type: 'uuid' })
|
||||||
|
bookingId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => BookingOrmEntity, (booking) => booking.containers, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'booking_id' })
|
||||||
|
booking: BookingOrmEntity;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50 })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@Column({ name: 'container_number', type: 'varchar', length: 20, nullable: true })
|
||||||
|
containerNumber: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
vgm: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 5, scale: 2, nullable: true })
|
||||||
|
temperature: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'seal_number', type: 'varchar', length: 50, nullable: true })
|
||||||
|
sealNumber: string | null;
|
||||||
|
}
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Booking ORM Mapper
|
||||||
|
*
|
||||||
|
* Maps between Booking domain entity and BookingOrmEntity
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Booking,
|
||||||
|
BookingProps,
|
||||||
|
Party,
|
||||||
|
BookingContainer,
|
||||||
|
} 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 { ContainerOrmEntity } from '../entities/container.orm-entity';
|
||||||
|
|
||||||
|
export class BookingOrmMapper {
|
||||||
|
/**
|
||||||
|
* Map domain entity to ORM entity
|
||||||
|
*/
|
||||||
|
static toOrm(domain: Booking): BookingOrmEntity {
|
||||||
|
const orm = new BookingOrmEntity();
|
||||||
|
|
||||||
|
orm.id = domain.id;
|
||||||
|
orm.bookingNumber = domain.bookingNumber.value;
|
||||||
|
orm.userId = domain.userId;
|
||||||
|
orm.organizationId = domain.organizationId;
|
||||||
|
orm.rateQuoteId = domain.rateQuoteId;
|
||||||
|
orm.status = domain.status.value;
|
||||||
|
orm.shipper = this.partyToJson(domain.shipper);
|
||||||
|
orm.consignee = this.partyToJson(domain.consignee);
|
||||||
|
orm.cargoDescription = domain.cargoDescription;
|
||||||
|
orm.specialInstructions = domain.specialInstructions || null;
|
||||||
|
orm.createdAt = domain.createdAt;
|
||||||
|
orm.updatedAt = domain.updatedAt;
|
||||||
|
|
||||||
|
// Map containers
|
||||||
|
orm.containers = domain.containers.map((container) =>
|
||||||
|
this.containerToOrm(container, domain.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return orm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map ORM entity to domain entity
|
||||||
|
*/
|
||||||
|
static toDomain(orm: BookingOrmEntity): Booking {
|
||||||
|
const props: BookingProps = {
|
||||||
|
id: orm.id,
|
||||||
|
bookingNumber: BookingNumber.fromString(orm.bookingNumber),
|
||||||
|
userId: orm.userId,
|
||||||
|
organizationId: orm.organizationId,
|
||||||
|
rateQuoteId: orm.rateQuoteId,
|
||||||
|
status: BookingStatus.create(orm.status as any),
|
||||||
|
shipper: this.jsonToParty(orm.shipper),
|
||||||
|
consignee: this.jsonToParty(orm.consignee),
|
||||||
|
cargoDescription: orm.cargoDescription,
|
||||||
|
containers: orm.containers
|
||||||
|
? orm.containers.map((c) => this.ormToContainer(c))
|
||||||
|
: [],
|
||||||
|
specialInstructions: orm.specialInstructions || undefined,
|
||||||
|
createdAt: orm.createdAt,
|
||||||
|
updatedAt: orm.updatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Booking.create({
|
||||||
|
...props,
|
||||||
|
bookingNumber: props.bookingNumber,
|
||||||
|
status: props.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map array of ORM entities to domain entities
|
||||||
|
*/
|
||||||
|
static toDomainMany(orms: BookingOrmEntity[]): Booking[] {
|
||||||
|
return orms.map((orm) => this.toDomain(orm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert domain Party to JSON
|
||||||
|
*/
|
||||||
|
private static partyToJson(party: Party): PartyJson {
|
||||||
|
return {
|
||||||
|
name: party.name,
|
||||||
|
address: {
|
||||||
|
street: party.address.street,
|
||||||
|
city: party.address.city,
|
||||||
|
postalCode: party.address.postalCode,
|
||||||
|
country: party.address.country,
|
||||||
|
},
|
||||||
|
contactName: party.contactName,
|
||||||
|
contactEmail: party.contactEmail,
|
||||||
|
contactPhone: party.contactPhone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert JSON to domain Party
|
||||||
|
*/
|
||||||
|
private static jsonToParty(json: PartyJson): Party {
|
||||||
|
return {
|
||||||
|
name: json.name,
|
||||||
|
address: {
|
||||||
|
street: json.address.street,
|
||||||
|
city: json.address.city,
|
||||||
|
postalCode: json.address.postalCode,
|
||||||
|
country: json.address.country,
|
||||||
|
},
|
||||||
|
contactName: json.contactName,
|
||||||
|
contactEmail: json.contactEmail,
|
||||||
|
contactPhone: json.contactPhone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert domain BookingContainer to ORM entity
|
||||||
|
*/
|
||||||
|
private static containerToOrm(
|
||||||
|
container: BookingContainer,
|
||||||
|
bookingId: string
|
||||||
|
): ContainerOrmEntity {
|
||||||
|
const orm = new ContainerOrmEntity();
|
||||||
|
orm.id = container.id;
|
||||||
|
orm.bookingId = bookingId;
|
||||||
|
orm.type = container.type;
|
||||||
|
orm.containerNumber = container.containerNumber || null;
|
||||||
|
orm.vgm = container.vgm || null;
|
||||||
|
orm.temperature = container.temperature || null;
|
||||||
|
orm.sealNumber = container.sealNumber || null;
|
||||||
|
return orm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ORM entity to domain BookingContainer
|
||||||
|
*/
|
||||||
|
private static ormToContainer(orm: ContainerOrmEntity): BookingContainer {
|
||||||
|
return {
|
||||||
|
id: orm.id,
|
||||||
|
type: orm.type,
|
||||||
|
containerNumber: orm.containerNumber || undefined,
|
||||||
|
vgm: orm.vgm || undefined,
|
||||||
|
temperature: orm.temperature || undefined,
|
||||||
|
sealNumber: orm.sealNumber || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* TypeORM Booking Repository
|
||||||
|
*
|
||||||
|
* Implements BookingRepository interface using TypeORM
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Booking } from '../../../../domain/entities/booking.entity';
|
||||||
|
import { BookingNumber } from '../../../../domain/value-objects/booking-number.vo';
|
||||||
|
import { BookingStatus } from '../../../../domain/value-objects/booking-status.vo';
|
||||||
|
import { BookingRepository } from '../../../../domain/ports/out/booking.repository';
|
||||||
|
import { BookingOrmEntity } from '../entities/booking.orm-entity';
|
||||||
|
import { ContainerOrmEntity } from '../entities/container.orm-entity';
|
||||||
|
import { BookingOrmMapper } from '../mappers/booking-orm.mapper';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TypeOrmBookingRepository implements BookingRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(BookingOrmEntity)
|
||||||
|
private readonly bookingRepository: Repository<BookingOrmEntity>,
|
||||||
|
@InjectRepository(ContainerOrmEntity)
|
||||||
|
private readonly containerRepository: Repository<ContainerOrmEntity>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async save(booking: Booking): Promise<Booking> {
|
||||||
|
const orm = BookingOrmMapper.toOrm(booking);
|
||||||
|
const saved = await this.bookingRepository.save(orm);
|
||||||
|
return BookingOrmMapper.toDomain(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Booking | null> {
|
||||||
|
const orm = await this.bookingRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['containers'],
|
||||||
|
});
|
||||||
|
return orm ? BookingOrmMapper.toDomain(orm) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByBookingNumber(bookingNumber: BookingNumber): Promise<Booking | null> {
|
||||||
|
const orm = await this.bookingRepository.findOne({
|
||||||
|
where: { bookingNumber: bookingNumber.value },
|
||||||
|
relations: ['containers'],
|
||||||
|
});
|
||||||
|
return orm ? BookingOrmMapper.toDomain(orm) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUser(userId: string): Promise<Booking[]> {
|
||||||
|
const orms = await this.bookingRepository.find({
|
||||||
|
where: { userId },
|
||||||
|
relations: ['containers'],
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
return BookingOrmMapper.toDomainMany(orms);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByOrganization(organizationId: string): Promise<Booking[]> {
|
||||||
|
const orms = await this.bookingRepository.find({
|
||||||
|
where: { organizationId },
|
||||||
|
relations: ['containers'],
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
return BookingOrmMapper.toDomainMany(orms);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByStatus(status: BookingStatus): Promise<Booking[]> {
|
||||||
|
const orms = await this.bookingRepository.find({
|
||||||
|
where: { status: status.value },
|
||||||
|
relations: ['containers'],
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
return BookingOrmMapper.toDomainMany(orms);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.bookingRepository.delete({ id });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,13 +35,20 @@ export class TypeOrmOrganizationRepository implements OrganizationRepository {
|
|||||||
return orm ? OrganizationOrmMapper.toDomain(orm) : null;
|
return orm ? OrganizationOrmMapper.toDomain(orm) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByScac(scac: string): Promise<Organization | null> {
|
async findBySCAC(scac: string): Promise<Organization | null> {
|
||||||
const orm = await this.repository.findOne({
|
const orm = await this.repository.findOne({
|
||||||
where: { scac: scac.toUpperCase() },
|
where: { scac: scac.toUpperCase() },
|
||||||
});
|
});
|
||||||
return orm ? OrganizationOrmMapper.toDomain(orm) : null;
|
return orm ? OrganizationOrmMapper.toDomain(orm) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Organization[]> {
|
||||||
|
const orms = await this.repository.find({
|
||||||
|
order: { name: 'ASC' },
|
||||||
|
});
|
||||||
|
return OrganizationOrmMapper.toDomainMany(orms);
|
||||||
|
}
|
||||||
|
|
||||||
async findAllActive(): Promise<Organization[]> {
|
async findAllActive(): Promise<Organization[]> {
|
||||||
const orms = await this.repository.find({
|
const orms = await this.repository.find({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user