/** * Task 7 - handleDocumentCompleted auto-deposit. * * Verifies that when a document is completed: * - The signed PDF is deposited into the owner's entity subfolder * (files.folderId set + the matching entity FK set). * - Owner is resolved via the Owner-wins chain: * direct clientId / companyId / yachtId on the document, then * interest.clientId / interest.yachtId when only interestId is set. * - No owner → folderId=null, entity FKs null. * * Fixture convention: makePort + makeClient from helpers/factories; * TEST_USER_ID resolved once via beforeAll from a seeded user, matching * document-folders-crud.test.ts and document-folders-system-folders.test.ts. */ import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; import { and, eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { documents, files, documentFolders } from '@/lib/db/schema/documents'; import { interests } from '@/lib/db/schema/interests'; import { user } from '@/lib/db/schema/users'; import { handleDocumentCompleted } from '@/lib/services/documents.service'; import { ensureSystemRoots } from '@/lib/services/document-folders.service'; import { makeClient, makeCompany, makePort, makeYacht } from '../helpers/factories'; // Stub Documenso download - do NOT hit the network. vi.mock('@/lib/services/documenso-client', async (importOriginal) => { const real = await importOriginal(); return { ...real, downloadSignedPdf: vi.fn(async () => Buffer.from('%PDF-1.4 stub\n')), }; }); // Stub storage backend - write to an in-memory map so no MinIO required. const stubPuts = new Map(); vi.mock('@/lib/storage', async (importOriginal) => { const real = await importOriginal(); return { ...real, getStorageBackend: vi.fn(async () => ({ put: async (path: string, data: Buffer) => { stubPuts.set(path, data); }, get: async (path: string) => stubPuts.get(path) ?? Buffer.alloc(0), head: async (path: string) => { const buf = stubPuts.get(path); return buf ? { sizeBytes: buf.length, contentType: 'application/pdf' } : null; }, delete: async (path: string) => { stubPuts.delete(path); }, presignedGet: async () => 'http://stub-url', presignedPut: async () => ({ url: 'http://stub-url', fields: {} }), })), }; }); let TEST_USER_ID = ''; beforeAll(async () => { const [u] = await db.select({ id: user.id }).from(user).limit(1); if (!u) throw new Error('No user available; run pnpm db:seed first'); TEST_USER_ID = u.id; }); describe('handleDocumentCompleted · auto-deposit', () => { let portId: string; let clientId: string; beforeEach(async () => { stubPuts.clear(); const port = await makePort(); portId = port.id; // Ensure system roots exist (required for ensureEntityFolder to work). await db.delete(documentFolders).where(eq(documentFolders.portId, portId)); await ensureSystemRoots(portId, TEST_USER_ID); const client = await makeClient({ portId }); clientId = client.id; }); it('client-direct: signed PDF lands in the client subfolder', async () => { const documensoId = `docu-auto-deposit-client-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId, clientId, documentType: 'eoi', title: 'Auto-deposit test EOI (client)', status: 'partially_signed', documensoId, createdBy: 'seed', }) .returning(); await handleDocumentCompleted({ documentId: documensoId, portId }); // Document should be marked completed. const updatedDoc = await db.query.documents.findFirst({ where: eq(documents.id, doc!.id), }); expect(updatedDoc?.status).toBe('completed'); expect(updatedDoc?.signedFileId).not.toBeNull(); // Signed file should have folderId set (the client entity subfolder). const fileRow = await db.query.files.findFirst({ where: eq(files.id, updatedDoc!.signedFileId!), }); expect(fileRow?.folderId).not.toBeNull(); expect(fileRow?.clientId).toBe(clientId); // Verify the folder is the entity folder for this client. const folder = await db.query.documentFolders.findFirst({ where: and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)), }); expect(folder).toBeDefined(); expect(fileRow?.folderId).toBe(folder!.id); }); it('no owner: signed PDF lands at root with folder_id=null', async () => { const documensoId = `docu-auto-deposit-noowner-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId, // No clientId, companyId, yachtId, or interestId. documentType: 'other', title: 'Auto-deposit test (no owner)', status: 'partially_signed', documensoId, createdBy: 'seed', }) .returning(); await handleDocumentCompleted({ documentId: documensoId, portId }); const updatedDoc = await db.query.documents.findFirst({ where: eq(documents.id, doc!.id), }); expect(updatedDoc?.status).toBe('completed'); expect(updatedDoc?.signedFileId).not.toBeNull(); const fileRow = await db.query.files.findFirst({ where: eq(files.id, updatedDoc!.signedFileId!), }); expect(fileRow?.folderId).toBeNull(); expect(fileRow?.clientId).toBeNull(); expect(fileRow?.companyId).toBeNull(); expect(fileRow?.yachtId).toBeNull(); }); it('via interest.clientId: resolves owner through linked interest', async () => { // Create an interest with clientId pointing to our client. const [interest] = await db .insert(interests) .values({ portId, clientId, pipelineStage: 'eoi_sent', }) .returning(); const documensoId = `docu-auto-deposit-interest-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId, interestId: interest!.id, // All direct owner FKs null - owner must be resolved via interest. documentType: 'eoi', title: 'Auto-deposit test EOI (via interest)', status: 'partially_signed', documensoId, createdBy: 'seed', }) .returning(); await handleDocumentCompleted({ documentId: documensoId, portId }); const updatedDoc = await db.query.documents.findFirst({ where: eq(documents.id, doc!.id), }); expect(updatedDoc?.status).toBe('completed'); expect(updatedDoc?.signedFileId).not.toBeNull(); const fileRow = await db.query.files.findFirst({ where: eq(files.id, updatedDoc!.signedFileId!), }); // Should have been deposited with the interest's clientId. expect(fileRow?.clientId).toBe(clientId); expect(fileRow?.folderId).not.toBeNull(); // And the folder should be the client entity folder. const folder = await db.query.documentFolders.findFirst({ where: and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)), }); expect(folder).toBeDefined(); expect(fileRow?.folderId).toBe(folder!.id); }); it('company-direct: signed PDF lands in the company subfolder', async () => { const company = await makeCompany({ portId }); const documensoId = `docu-auto-deposit-company-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId, companyId: company.id, documentType: 'other', title: 'Auto-deposit test (company direct)', status: 'partially_signed', documensoId, createdBy: 'seed', }) .returning(); await handleDocumentCompleted({ documentId: documensoId, portId }); const updatedDoc = await db.query.documents.findFirst({ where: eq(documents.id, doc!.id), }); expect(updatedDoc?.status).toBe('completed'); expect(updatedDoc?.signedFileId).not.toBeNull(); const fileRow = await db.query.files.findFirst({ where: eq(files.id, updatedDoc!.signedFileId!), }); expect(fileRow?.companyId).toBe(company.id); expect(fileRow?.folderId).not.toBeNull(); const folder = await db.query.documentFolders.findFirst({ where: and( eq(documentFolders.entityType, 'company'), eq(documentFolders.entityId, company.id), ), }); expect(folder).toBeDefined(); expect(fileRow?.folderId).toBe(folder!.id); }); it('yacht-direct: signed PDF lands in the yacht subfolder', async () => { // Yachts require a real owner client per schema constraint. const ownerClient = await makeClient({ portId }); const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: ownerClient.id }); const documensoId = `docu-auto-deposit-yacht-${Date.now()}`; const [doc] = await db .insert(documents) .values({ portId, yachtId: yacht.id, documentType: 'other', title: 'Auto-deposit test (yacht direct)', status: 'partially_signed', documensoId, createdBy: 'seed', }) .returning(); await handleDocumentCompleted({ documentId: documensoId, portId }); const updatedDoc = await db.query.documents.findFirst({ where: eq(documents.id, doc!.id), }); expect(updatedDoc?.status).toBe('completed'); expect(updatedDoc?.signedFileId).not.toBeNull(); const fileRow = await db.query.files.findFirst({ where: eq(files.id, updatedDoc!.signedFileId!), }); expect(fileRow?.yachtId).toBe(yacht.id); expect(fileRow?.folderId).not.toBeNull(); const folder = await db.query.documentFolders.findFirst({ where: and(eq(documentFolders.entityType, 'yacht'), eq(documentFolders.entityId, yacht.id)), }); expect(folder).toBeDefined(); expect(fileRow?.folderId).toBe(folder!.id); }); });