import { FilesService } from './files.service'; /** * Replacing a stored document must never destroy the previous one: the customer * uploaded it, and a staff correction has to stay auditable against it. The old * row is soft-deleted (so every normal read still returns exactly the current * version) and stamped with who replaced it and why. */ describe('FilesService — document versions', () => { const file = { originalname: 'bill-of-lading.pdf', size: 1234, mimetype: 'application/pdf', buffer: Buffer.from('x'), } as Express.Multer.File; let filesRepository: { deleteByCode: jest.Mock; create: jest.Mock; findVersionHistory: jest.Mock; }; let service: FilesService; beforeEach(() => { filesRepository = { deleteByCode: jest.fn().mockResolvedValue(undefined), create: jest.fn(async (row) => ({ id: 'file-new', ...row })), findVersionHistory: jest.fn().mockResolvedValue([]), }; service = new FilesService( filesRepository as never, { uploadFile: jest.fn().mockResolvedValue('https://minio/bucket/new.pdf'), getObjectNameFromUrl: (u: string) => u, getSignedUrl: jest.fn(), } as never, ); }); it('stamps the retired version with who replaced it and why', async () => { await service.upsertByCode( { resourceId: 'ctr-1', resource: 'contracts', code: 'bill_of_lading', file }, { userId: 'gl-user-1', reason: 'Customer sent page 2 only' }, ); expect(filesRepository.deleteByCode).toHaveBeenCalledWith( 'ctr-1', 'contracts', 'bill_of_lading', { userId: 'gl-user-1', reason: 'Customer sent page 2 only' }, ); }); it('still replaces silently when no replacer is given (system overwrites)', async () => { await service.upsertByCode({ resourceId: 'ctr-1', resource: 'contracts', code: 'contract_pdf', file, }); expect(filesRepository.deleteByCode).toHaveBeenCalledWith( 'ctr-1', 'contracts', 'contract_pdf', undefined, ); }); it('marks the live row current and the soft-deleted ones superseded', async () => { filesRepository.findVersionHistory.mockResolvedValue([ { id: 'v2', name: 'corrected.pdf', url: 'u2', size: 2, mimeType: 'application/pdf', createdAt: new Date('2026-07-20T10:00:00Z'), deletedAt: null, replacedByUserId: null, replaceReason: null, }, { id: 'v1', name: 'original.pdf', url: 'u1', size: 1, mimeType: 'application/pdf', createdAt: new Date('2026-07-18T10:00:00Z'), deletedAt: new Date('2026-07-20T10:00:00Z'), replacedByUserId: 'gl-user-1', replaceReason: 'Wrong page order', }, ]); const versions = await service.versionHistory( 'ctr-1', 'contracts', 'bill_of_lading', ); expect(versions[0]).toMatchObject({ id: 'v2', isCurrent: true, replacedAt: null }); expect(versions[1]).toMatchObject({ id: 'v1', isCurrent: false, replacedByUserId: 'gl-user-1', replaceReason: 'Wrong page order', }); // The customer's original is still readable — that is the whole point. expect(versions[1].url).toBe('u1'); }); });