add company stamp upload functionality for contract signing

- Introduced StampUpload component for uploading company stamp images.
- Integrated stamp upload in contract signing modal, supporting PNG and JPG formats.
- Implemented validation for file type and size (max 5 MB).
- Added visual feedback for drag-and-drop functionality.
- Updated contract-related pages to handle duplicate contract alerts and pricing notices.
- Enhanced contract expiry management with a nightly sweep service.
- Added unit tests for new features and updated existing tests for contract handling.
This commit is contained in:
Marshal
2026-07-25 17:14:58 +00:00
parent 54b5882355
commit fde5e6de4b
68 changed files with 1858 additions and 289 deletions

View File

@@ -22,6 +22,7 @@ describe('BookingWindowService — window state machine', () => {
expireLeftoverDayPool: jest.Mock;
expireLeftoverExportDay: jest.Mock;
fillFromWaitingList: jest.Mock;
countUnacceptedForRouteDay: jest.Mock;
};
let trainSchedulesRepository: { findById: jest.Mock; findAll: jest.Mock };
let trainSchedulingService: { finalizeSchedule: jest.Mock; getWindowConfig: jest.Mock };
@@ -79,6 +80,7 @@ describe('BookingWindowService — window state machine', () => {
expireLeftoverExportDay: jest.fn().mockResolvedValue(undefined),
// No waiting booking fits by default, so conclude proceeds to reopen/DONE.
fillFromWaitingList: jest.fn().mockResolvedValue(0),
countUnacceptedForRouteDay: jest.fn().mockResolvedValue(0),
};
trainSchedulesRepository = {
findById: jest.fn().mockResolvedValue(null),
@@ -267,4 +269,87 @@ describe('BookingWindowService — window state machine', () => {
expect(s.windowPhase).toBe('OPEN');
expect(batch.setWindow).not.toHaveBeenCalled();
});
// ---- header alarm ---------------------------------------------------------
describe('getDocReviewAlert', () => {
const reviewing = (over: Partial<TrainSchedule>): TrainSchedule =>
baseSchedule({
windowPhase: 'DOC_REVIEW',
docReviewEndsAt: new Date('2026-07-01T01:30:00.000Z'),
...over,
});
it('returns null when nothing is under document review', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([
baseSchedule({ windowPhase: 'OPEN' }),
]);
expect(await service.getDocReviewAlert()).toBeNull();
});
it('returns null when every request on the route-day is decided', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([reviewing({})]);
batch.countUnacceptedForRouteDay.mockResolvedValue(0);
expect(await service.getDocReviewAlert()).toBeNull();
});
it('reports the deadline, its own pending count and the phase length', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([reviewing({})]);
batch.countUnacceptedForRouteDay.mockResolvedValue(3);
const alert = await service.getDocReviewAlert();
expect(alert).toMatchObject({
scheduleId,
originYardId: 'yard-o',
destinationYardId: 'yard-d',
tradeDirection: 'IMPORT',
pendingCount: 3,
docReviewMinutes: 30,
docReviewEndsAt: '2026-07-01T01:30:00.000Z',
});
});
it('skips the nearest deadline when it has nothing pending', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([
reviewing({
id: 'sched-later',
destinationStationId: 'yard-far',
docReviewEndsAt: new Date('2026-07-01T02:00:00.000Z'),
}),
reviewing({ id: 'sched-soon' }),
]);
// Nearest (sched-soon, yard-d) is clear; the later route-day still isn't.
batch.countUnacceptedForRouteDay.mockImplementation(
async (g: { destinationYardId: string }) =>
g.destinationYardId === 'yard-far' ? 2 : 0,
);
const alert = await service.getDocReviewAlert();
expect(alert?.scheduleId).toBe('sched-later');
expect(alert?.pendingCount).toBe(2);
});
it('counts a route-day once when sibling trains share the review phase', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([
reviewing({ id: 'sched-a' }),
reviewing({ id: 'sched-b' }),
]);
batch.countUnacceptedForRouteDay.mockResolvedValue(4);
const alert = await service.getDocReviewAlert();
expect(alert?.pendingCount).toBe(4);
expect(batch.countUnacceptedForRouteDay).toHaveBeenCalledTimes(1);
});
it('ignores a phase staff already completed early', async () => {
trainSchedulesRepository.findAll.mockResolvedValue([
reviewing({ docReviewCompletedAt: new Date('2026-07-01T01:10:00.000Z') }),
]);
batch.countUnacceptedForRouteDay.mockResolvedValue(5);
expect(await service.getDocReviewAlert()).toBeNull();
});
});
});