diff --git a/src/lib/services/documents.service.ts b/src/lib/services/documents.service.ts index 7471035c..ab917760 100644 --- a/src/lib/services/documents.service.ts +++ b/src/lib/services/documents.service.ts @@ -1079,27 +1079,30 @@ interface ResolvedOwner { * companyId (per schema), so the company branch is omitted from the * interest fallback. Returns null when no owner is resolvable. */ -async function resolveDocumentOwner(doc: { - clientId: string | null; - companyId: string | null; - yachtId: string | null; - interestId: string | null; -}): Promise { +async function resolveDocumentOwner( + portId: string, + doc: { + clientId: string | null; + companyId: string | null; + yachtId: string | null; + interestId: string | null; + }, +): Promise { if (doc.clientId) return { entityType: 'client', entityId: doc.clientId }; if (doc.companyId) return { entityType: 'company', entityId: doc.companyId }; if (doc.yachtId) return { entityType: 'yacht', entityId: doc.yachtId }; if (doc.interestId) { + // interests.clientId is NOT NULL — if the interest row exists, the + // client owner is always resolvable through it. The yacht-only path + // would require relaxing the schema constraint first. const interest = await db.query.interests.findFirst({ - where: eq(interests.id, doc.interestId), - columns: { clientId: true, yachtId: true }, + where: and(eq(interests.id, doc.interestId), eq(interests.portId, portId)), + columns: { clientId: true }, }); if (interest?.clientId) { return { entityType: 'client', entityId: interest.clientId }; } - if (interest?.yachtId) { - return { entityType: 'yacht', entityId: interest.yachtId }; - } } return null; } @@ -1129,12 +1132,17 @@ export async function handleDocumentCompleted(eventData: { documentId: string; p // Resolve owner via the Owner-wins chain. The signed PDF lands in // this owner's auto-created entity subfolder (or at root if no owner). - const owner = await resolveDocumentOwner(doc); + const owner = await resolveDocumentOwner(doc.portId, doc); let entityFolderId: string | null = null; if (owner) { try { - const folder = await ensureEntityFolder(doc.portId, owner.entityType, owner.entityId, 'system'); + const folder = await ensureEntityFolder( + doc.portId, + owner.entityType, + owner.entityId, + 'system', + ); entityFolderId = folder.id; } catch (err) { // Folder creation is best-effort — signed file still lands at root. diff --git a/tests/integration/documents-completion-auto-deposit.test.ts b/tests/integration/documents-completion-auto-deposit.test.ts index 2c1c23c1..d61ce302 100644 --- a/tests/integration/documents-completion-auto-deposit.test.ts +++ b/tests/integration/documents-completion-auto-deposit.test.ts @@ -23,7 +23,7 @@ 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, makePort } from '../helpers/factories'; +import { makeClient, makeCompany, makePort, makeYacht } from '../helpers/factories'; // Stub Documenso download — do NOT hit the network. vi.mock('@/lib/services/documenso-client', async (importOriginal) => { @@ -116,10 +116,7 @@ describe('handleDocumentCompleted · auto-deposit', () => { // 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), - ), + where: and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)), }); expect(folder).toBeDefined(); expect(fileRow?.folderId).toBe(folder!.id); @@ -199,13 +196,91 @@ describe('handleDocumentCompleted · auto-deposit', () => { 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, 'client'), - eq(documentFolders.entityId, clientId), + 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); + }); });