Some checks failed
Dev CI / Unit Tests (${{ matrix.app }}) (backend) (push) Blocked by required conditions
Dev CI / Unit Tests (${{ matrix.app }}) (frontend) (push) Blocked by required conditions
Dev CI / Notify Failure (push) Blocked by required conditions
Dev CI / Quality (${{ matrix.app }}) (backend) (push) Has been cancelled
Dev CI / Quality (${{ matrix.app }}) (frontend) (push) Has been cancelled
Aligns dev with the complete application codebase (cicd branch). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
488 lines
13 KiB
TypeScript
488 lines
13 KiB
TypeScript
import {
|
|
CsvBooking,
|
|
CsvBookingStatus,
|
|
DocumentType,
|
|
CsvBookingDocument,
|
|
} from './csv-booking.entity';
|
|
import { PortCode } from '../value-objects/port-code.vo';
|
|
|
|
describe('CsvBooking Entity', () => {
|
|
// Test data factory
|
|
const createValidBooking = (
|
|
_overrides?: Partial<ConstructorParameters<typeof CsvBooking>[0]>
|
|
): CsvBooking => {
|
|
const documents: CsvBookingDocument[] = [
|
|
{
|
|
id: 'doc-1',
|
|
type: DocumentType.BILL_OF_LADING,
|
|
fileName: 'bill-of-lading.pdf',
|
|
filePath: '/uploads/bill-of-lading.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 1024,
|
|
uploadedAt: new Date(),
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
type: DocumentType.PACKING_LIST,
|
|
fileName: 'packing-list.pdf',
|
|
filePath: '/uploads/packing-list.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 2048,
|
|
uploadedAt: new Date(),
|
|
},
|
|
{
|
|
id: 'doc-3',
|
|
type: DocumentType.COMMERCIAL_INVOICE,
|
|
fileName: 'invoice.pdf',
|
|
filePath: '/uploads/invoice.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 3072,
|
|
uploadedAt: new Date(),
|
|
},
|
|
];
|
|
|
|
return new CsvBooking(
|
|
'booking-123',
|
|
'user-456',
|
|
'org-789',
|
|
'SSC Consolidation',
|
|
'bookings@sscconsolidation.com',
|
|
PortCode.create('NLRTM'),
|
|
PortCode.create('USNYC'),
|
|
10.5,
|
|
1500,
|
|
3,
|
|
1200.0,
|
|
1100.0,
|
|
'USD',
|
|
15,
|
|
'LCL',
|
|
CsvBookingStatus.PENDING,
|
|
documents,
|
|
'token-abc123',
|
|
new Date(),
|
|
undefined,
|
|
'Test booking',
|
|
undefined
|
|
);
|
|
};
|
|
|
|
describe('Constructor and Validation', () => {
|
|
it('should create a valid booking', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.id).toBe('booking-123');
|
|
expect(booking.userId).toBe('user-456');
|
|
expect(booking.organizationId).toBe('org-789');
|
|
expect(booking.carrierName).toBe('SSC Consolidation');
|
|
expect(booking.carrierEmail).toBe('bookings@sscconsolidation.com');
|
|
expect(booking.status).toBe(CsvBookingStatus.PENDING);
|
|
expect(booking.documents).toHaveLength(3);
|
|
});
|
|
|
|
it('should throw error if ID is empty', () => {
|
|
expect(() => {
|
|
const docs: CsvBookingDocument[] = [
|
|
{
|
|
id: 'doc-1',
|
|
type: DocumentType.BILL_OF_LADING,
|
|
fileName: 'test.pdf',
|
|
filePath: '/test.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 1024,
|
|
uploadedAt: new Date(),
|
|
},
|
|
];
|
|
new CsvBooking(
|
|
'',
|
|
'user-456',
|
|
'org-789',
|
|
'SSC Consolidation',
|
|
'bookings@sscconsolidation.com',
|
|
PortCode.create('NLRTM'),
|
|
PortCode.create('USNYC'),
|
|
10.5,
|
|
1500,
|
|
3,
|
|
1200.0,
|
|
1100.0,
|
|
'USD',
|
|
15,
|
|
'LCL',
|
|
CsvBookingStatus.PENDING,
|
|
docs,
|
|
'token-abc123',
|
|
new Date()
|
|
);
|
|
}).toThrow('Booking ID is required');
|
|
});
|
|
|
|
it('should throw error if volume is negative', () => {
|
|
expect(() => {
|
|
const docs: CsvBookingDocument[] = [
|
|
{
|
|
id: 'doc-1',
|
|
type: DocumentType.BILL_OF_LADING,
|
|
fileName: 'test.pdf',
|
|
filePath: '/test.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 1024,
|
|
uploadedAt: new Date(),
|
|
},
|
|
];
|
|
new CsvBooking(
|
|
'booking-123',
|
|
'user-456',
|
|
'org-789',
|
|
'SSC Consolidation',
|
|
'bookings@sscconsolidation.com',
|
|
PortCode.create('NLRTM'),
|
|
PortCode.create('USNYC'),
|
|
-10.5, // Negative volume
|
|
1500,
|
|
3,
|
|
1200.0,
|
|
1100.0,
|
|
'USD',
|
|
15,
|
|
'LCL',
|
|
CsvBookingStatus.PENDING,
|
|
docs,
|
|
'token-abc123',
|
|
new Date()
|
|
);
|
|
}).toThrow('Volume must be positive');
|
|
});
|
|
|
|
it('should throw error if email format is invalid', () => {
|
|
expect(() => {
|
|
const docs: CsvBookingDocument[] = [
|
|
{
|
|
id: 'doc-1',
|
|
type: DocumentType.BILL_OF_LADING,
|
|
fileName: 'test.pdf',
|
|
filePath: '/test.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 1024,
|
|
uploadedAt: new Date(),
|
|
},
|
|
];
|
|
new CsvBooking(
|
|
'booking-123',
|
|
'user-456',
|
|
'org-789',
|
|
'SSC Consolidation',
|
|
'invalid-email', // Invalid email
|
|
PortCode.create('NLRTM'),
|
|
PortCode.create('USNYC'),
|
|
10.5,
|
|
1500,
|
|
3,
|
|
1200.0,
|
|
1100.0,
|
|
'USD',
|
|
15,
|
|
'LCL',
|
|
CsvBookingStatus.PENDING,
|
|
docs,
|
|
'token-abc123',
|
|
new Date()
|
|
);
|
|
}).toThrow('Invalid carrier email format');
|
|
});
|
|
|
|
it('should throw error if no documents provided', () => {
|
|
expect(() => {
|
|
new CsvBooking(
|
|
'booking-123',
|
|
'user-456',
|
|
'org-789',
|
|
'SSC Consolidation',
|
|
'bookings@sscconsolidation.com',
|
|
PortCode.create('NLRTM'),
|
|
PortCode.create('USNYC'),
|
|
10.5,
|
|
1500,
|
|
3,
|
|
1200.0,
|
|
1100.0,
|
|
'USD',
|
|
15,
|
|
'LCL',
|
|
CsvBookingStatus.PENDING,
|
|
[], // Empty documents
|
|
'token-abc123',
|
|
new Date()
|
|
);
|
|
}).toThrow('At least one document is required for booking');
|
|
});
|
|
});
|
|
|
|
describe('Accept Method', () => {
|
|
it('should accept a pending booking', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.status).toBe(CsvBookingStatus.PENDING);
|
|
|
|
booking.accept();
|
|
|
|
expect(booking.status).toBe(CsvBookingStatus.ACCEPTED);
|
|
expect(booking.respondedAt).toBeDefined();
|
|
});
|
|
|
|
it('should throw error when accepting non-pending booking', () => {
|
|
const booking = createValidBooking();
|
|
booking.accept(); // First acceptance
|
|
|
|
expect(() => {
|
|
booking.accept(); // Try to accept again
|
|
}).toThrow('Cannot accept booking with status ACCEPTED');
|
|
});
|
|
|
|
it('should throw error when accepting expired booking', () => {
|
|
const booking = createValidBooking();
|
|
// Set requestedAt to 8 days ago (expired)
|
|
(booking as any).requestedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
|
|
|
|
expect(booking.isExpired()).toBe(true);
|
|
|
|
expect(() => {
|
|
booking.accept();
|
|
}).toThrow('Cannot accept expired booking');
|
|
});
|
|
});
|
|
|
|
describe('Reject Method', () => {
|
|
it('should reject a pending booking', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.status).toBe(CsvBookingStatus.PENDING);
|
|
|
|
booking.reject('Capacity full');
|
|
|
|
expect(booking.status).toBe(CsvBookingStatus.REJECTED);
|
|
expect(booking.respondedAt).toBeDefined();
|
|
expect(booking.rejectionReason).toBe('Capacity full');
|
|
});
|
|
|
|
it('should reject without reason', () => {
|
|
const booking = createValidBooking();
|
|
|
|
booking.reject();
|
|
|
|
expect(booking.status).toBe(CsvBookingStatus.REJECTED);
|
|
expect(booking.rejectionReason).toBeUndefined();
|
|
});
|
|
|
|
it('should throw error when rejecting non-pending booking', () => {
|
|
const booking = createValidBooking();
|
|
booking.reject(); // First rejection
|
|
|
|
expect(() => {
|
|
booking.reject(); // Try to reject again
|
|
}).toThrow('Cannot reject booking with status REJECTED');
|
|
});
|
|
});
|
|
|
|
describe('Cancel Method', () => {
|
|
it('should cancel a pending booking', () => {
|
|
const booking = createValidBooking();
|
|
|
|
booking.cancel();
|
|
|
|
expect(booking.status).toBe(CsvBookingStatus.CANCELLED);
|
|
expect(booking.respondedAt).toBeDefined();
|
|
});
|
|
|
|
it('should throw error when cancelling accepted booking', () => {
|
|
const booking = createValidBooking();
|
|
booking.accept();
|
|
|
|
expect(() => {
|
|
booking.cancel();
|
|
}).toThrow('Cannot cancel accepted booking');
|
|
});
|
|
|
|
it('should throw error when cancelling rejected booking', () => {
|
|
const booking = createValidBooking();
|
|
booking.reject();
|
|
|
|
expect(() => {
|
|
booking.cancel();
|
|
}).toThrow('Cannot cancel rejected booking');
|
|
});
|
|
});
|
|
|
|
describe('Expiration Logic', () => {
|
|
it('should not be expired for recent bookings', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.isExpired()).toBe(false);
|
|
});
|
|
|
|
it('should be expired after 7 days', () => {
|
|
const booking = createValidBooking();
|
|
// Set requestedAt to 8 days ago
|
|
(booking as any).requestedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
|
|
|
|
expect(booking.isExpired()).toBe(true);
|
|
});
|
|
|
|
it('should not be expired if already accepted', () => {
|
|
const booking = createValidBooking();
|
|
booking.accept();
|
|
|
|
// Set requestedAt to 8 days ago
|
|
(booking as any).requestedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
|
|
|
|
expect(booking.isExpired()).toBe(false);
|
|
});
|
|
|
|
it('should calculate days until expiration correctly', () => {
|
|
const booking = createValidBooking();
|
|
|
|
const days = booking.getDaysUntilExpiration();
|
|
|
|
expect(days).toBeGreaterThan(6);
|
|
expect(days).toBeLessThanOrEqual(7);
|
|
});
|
|
|
|
it('should return 0 days for accepted bookings', () => {
|
|
const booking = createValidBooking();
|
|
booking.accept();
|
|
|
|
expect(booking.getDaysUntilExpiration()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Status Check Methods', () => {
|
|
it('should correctly identify pending booking', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.isPending()).toBe(true);
|
|
expect(booking.isAccepted()).toBe(false);
|
|
expect(booking.isRejected()).toBe(false);
|
|
expect(booking.isCancelled()).toBe(false);
|
|
});
|
|
|
|
it('should correctly identify accepted booking', () => {
|
|
const booking = createValidBooking();
|
|
booking.accept();
|
|
|
|
expect(booking.isPending()).toBe(false);
|
|
expect(booking.isAccepted()).toBe(true);
|
|
expect(booking.isRejected()).toBe(false);
|
|
expect(booking.isCancelled()).toBe(false);
|
|
});
|
|
|
|
it('should correctly identify rejected booking', () => {
|
|
const booking = createValidBooking();
|
|
booking.reject();
|
|
|
|
expect(booking.isPending()).toBe(false);
|
|
expect(booking.isAccepted()).toBe(false);
|
|
expect(booking.isRejected()).toBe(true);
|
|
expect(booking.isCancelled()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Document Methods', () => {
|
|
it('should check if document type exists', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.hasDocumentType(DocumentType.BILL_OF_LADING)).toBe(true);
|
|
expect(booking.hasDocumentType(DocumentType.CERTIFICATE_OF_ORIGIN)).toBe(false);
|
|
});
|
|
|
|
it('should get documents by type', () => {
|
|
const booking = createValidBooking();
|
|
|
|
const billOfLading = booking.getDocumentsByType(DocumentType.BILL_OF_LADING);
|
|
expect(billOfLading).toHaveLength(1);
|
|
expect(billOfLading[0].fileName).toBe('bill-of-lading.pdf');
|
|
});
|
|
|
|
it('should check if all required documents are present', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.hasAllRequiredDocuments()).toBe(true);
|
|
});
|
|
|
|
it('should return false if required documents are missing', () => {
|
|
const docs: CsvBookingDocument[] = [
|
|
{
|
|
id: 'doc-1',
|
|
type: DocumentType.BILL_OF_LADING,
|
|
fileName: 'bill-of-lading.pdf',
|
|
filePath: '/uploads/bill-of-lading.pdf',
|
|
mimeType: 'application/pdf',
|
|
size: 1024,
|
|
uploadedAt: new Date(),
|
|
},
|
|
];
|
|
|
|
const booking = new CsvBooking(
|
|
'booking-123',
|
|
'user-456',
|
|
'org-789',
|
|
'SSC Consolidation',
|
|
'bookings@sscconsolidation.com',
|
|
PortCode.create('NLRTM'),
|
|
PortCode.create('USNYC'),
|
|
10.5,
|
|
1500,
|
|
3,
|
|
1200.0,
|
|
1100.0,
|
|
'USD',
|
|
15,
|
|
'LCL',
|
|
CsvBookingStatus.PENDING,
|
|
docs,
|
|
'token-abc123',
|
|
new Date()
|
|
);
|
|
|
|
expect(booking.hasAllRequiredDocuments()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Helper Methods', () => {
|
|
it('should return route description', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.getRouteDescription()).toBe('NLRTM → USNYC');
|
|
});
|
|
|
|
it('should return booking summary', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.getSummary()).toContain('CSV Booking booking-123');
|
|
expect(booking.getSummary()).toContain('SSC Consolidation');
|
|
expect(booking.getSummary()).toContain('NLRTM → USNYC');
|
|
expect(booking.getSummary()).toContain('PENDING');
|
|
});
|
|
|
|
it('should return price in specified currency', () => {
|
|
const booking = createValidBooking();
|
|
|
|
expect(booking.getPriceInCurrency('USD')).toBe(1200.0);
|
|
expect(booking.getPriceInCurrency('EUR')).toBe(1100.0);
|
|
});
|
|
|
|
it('should calculate response time in hours', () => {
|
|
const booking = createValidBooking();
|
|
|
|
// No response yet
|
|
expect(booking.getResponseTimeHours()).toBeNull();
|
|
|
|
// Accept booking
|
|
booking.accept();
|
|
|
|
const responseTime = booking.getResponseTimeHours();
|
|
expect(responseTime).toBeGreaterThanOrEqual(0);
|
|
expect(responseTime).toBeLessThan(1); // Should be less than 1 hour for this test
|
|
});
|
|
});
|
|
});
|