import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { documents, documentTemplates } from '@/lib/db/schema/documents'; import { clientAddresses, clientContacts } from '@/lib/db/schema/clients'; import { interests as interestsTable } from '@/lib/db/schema/interests'; import { ValidationError } from '@/lib/errors'; import { makeBerth, makeClient, makePort, makeYacht } from '../helpers/factories'; // ─── Mocks ──────────────────────────────────────────────────────────────────── vi.mock('@/lib/services/documenso-client', () => ({ createDocument: vi.fn(), sendDocument: vi.fn(), generateDocumentFromTemplate: vi.fn(), })); vi.mock('@/lib/minio', async () => { const actual = await vi.importActual('@/lib/minio'); return { ...actual, minioClient: { putObject: vi.fn().mockResolvedValue(undefined), getObject: vi.fn().mockImplementation(async () => { async function* gen() { yield Buffer.from('fake-pdf'); } return gen(); }), }, }; }); vi.mock('@/lib/socket/server', () => ({ emitToRoom: vi.fn(), })); vi.mock('@/lib/pdf/generate', () => ({ generatePdf: vi.fn().mockResolvedValue(new Uint8Array(Buffer.from('fake-pdf'))), })); vi.mock('@/lib/audit', () => ({ createAuditLog: vi.fn().mockResolvedValue(undefined), })); // ─── Helpers ────────────────────────────────────────────────────────────────── async function insertInApptemplate(portId: string) { const [row] = await db .insert(documentTemplates) .values({ portId, name: 'Test EOI', templateType: 'eoi', bodyHtml: '

Hello {{client.fullName}} for berth {{berth.mooringNumber}}

', createdBy: 'test', }) .returning(); return row!; } async function insertInterest(args: { portId: string; clientId: string; yachtId: string; berthId: string; }) { const [row] = await db .insert(interestsTable) .values({ portId: args.portId, clientId: args.clientId, yachtId: args.yachtId, berthId: args.berthId, pipelineStage: 'open', }) .returning(); return row!; } // ─── Setup ──────────────────────────────────────────────────────────────────── let setup: { portId: string; clientId: string; yachtId: string; berthId: string; interestId: string; inAppTemplateId: string; }; let generateAndSign: typeof import('@/lib/services/document-templates').generateAndSign; beforeAll(async () => { ({ generateAndSign } = await import('@/lib/services/document-templates')); const port = await makePort(); const client = await makeClient({ portId: port.id, overrides: { fullName: 'Dual Path Client', nationality: 'US' }, }); await db.insert(clientContacts).values({ clientId: client.id, channel: 'email', value: 'client@example.com', isPrimary: true, }); await db.insert(clientAddresses).values({ clientId: client.id, portId: port.id, streetAddress: '1 Wharf Rd', city: 'Harbor', country: 'US', isPrimary: true, }); const yacht = await makeYacht({ portId: port.id, ownerType: 'client', ownerId: client.id, name: 'Dual Path Yacht', overrides: { lengthFt: '45', widthFt: '14', draftFt: '6' }, }); const berth = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'DP-1' }, }); const interest = await insertInterest({ portId: port.id, clientId: client.id, yachtId: yacht.id, berthId: berth.id, }); const template = await insertInApptemplate(port.id); setup = { portId: port.id, clientId: client.id, yachtId: yacht.id, berthId: berth.id, interestId: interest.id, inAppTemplateId: template.id, }; }); beforeEach(() => { vi.clearAllMocks(); }); const meta = { userId: 'test-user', portId: '', ipAddress: '127.0.0.1', userAgent: 'vitest', }; // ─── Pathway: inapp ─────────────────────────────────────────────────────────── describe('generateAndSign — inapp pathway', () => { it('generates PDF via pdfme, uploads to MinIO, and sends to Documenso', async () => { const client = await import('@/lib/services/documenso-client'); vi.mocked(client.createDocument).mockResolvedValue({ id: 'documenso-inapp-123', status: 'PENDING', recipients: [], }); vi.mocked(client.sendDocument).mockResolvedValue({ id: 'documenso-inapp-123', status: 'PENDING', recipients: [], }); const result = await generateAndSign( setup.inAppTemplateId, setup.portId, { clientId: setup.clientId, interestId: setup.interestId }, [{ name: 'Client', email: 'client@example.com', role: 'signer', signingOrder: 1 }], 'inapp', { ...meta, portId: setup.portId }, ); expect(result.file).not.toBeNull(); expect(client.createDocument).toHaveBeenCalledOnce(); expect(client.sendDocument).toHaveBeenCalledWith('documenso-inapp-123'); const [docRow] = await db .select() .from(documents) .where(eq(documents.id, (result.document as { id: string }).id)); expect(docRow?.documensoId).toBe('documenso-inapp-123'); expect(docRow?.status).toBe('sent'); expect(docRow?.fileId).toBeTruthy(); }); it('throws ValidationError when signers array is empty', async () => { await expect( generateAndSign( setup.inAppTemplateId, setup.portId, { clientId: setup.clientId, interestId: setup.interestId }, [], 'inapp', { ...meta, portId: setup.portId }, ), ).rejects.toThrow(ValidationError); }); it('throws ValidationError when templateId is null', async () => { await expect( generateAndSign( null, setup.portId, { clientId: setup.clientId, interestId: setup.interestId }, [{ name: 'X', email: 'x@x.com', role: 'signer', signingOrder: 1 }], 'inapp', { ...meta, portId: setup.portId }, ), ).rejects.toThrow(ValidationError); }); }); // ─── Pathway: documenso-template ────────────────────────────────────────────── describe('generateAndSign — documenso-template pathway', () => { it('calls Documenso template-generate endpoint and records a documents row', async () => { const client = await import('@/lib/services/documenso-client'); vi.mocked(client.generateDocumentFromTemplate).mockResolvedValue({ id: 'documenso-template-456', status: 'PENDING', recipients: [], }); const result = await generateAndSign( null, setup.portId, { clientId: setup.clientId, interestId: setup.interestId }, [], 'documenso-template', { ...meta, portId: setup.portId }, ); expect(result.file).toBeNull(); expect(client.generateDocumentFromTemplate).toHaveBeenCalledOnce(); const [templateArg, payloadArg] = vi.mocked(client.generateDocumentFromTemplate).mock.calls[0]!; expect(typeof templateArg).toBe('number'); expect(payloadArg).toMatchObject({ externalId: `loi-${setup.interestId}`, formValues: { Name: 'Dual Path Client', 'Yacht Name': 'Dual Path Yacht', 'Berth Number': 'DP-1', }, }); const [docRow] = await db .select() .from(documents) .where(eq(documents.id, (result.document as { id: string }).id)); expect(docRow?.documensoId).toBe('documenso-template-456'); expect(docRow?.status).toBe('sent'); expect(docRow?.documentType).toBe('eoi'); expect(docRow?.interestId).toBe(setup.interestId); expect(docRow?.fileId).toBeNull(); }); it('throws ValidationError when interestId is missing', async () => { await expect( generateAndSign(null, setup.portId, { clientId: setup.clientId }, [], 'documenso-template', { ...meta, portId: setup.portId, }), ).rejects.toThrow(ValidationError); }); it('does NOT call createDocument / sendDocument / minio for this pathway', async () => { const client = await import('@/lib/services/documenso-client'); vi.mocked(client.generateDocumentFromTemplate).mockResolvedValue({ id: 'documenso-template-789', status: 'PENDING', recipients: [], }); await generateAndSign( null, setup.portId, { clientId: setup.clientId, interestId: setup.interestId }, [], 'documenso-template', { ...meta, portId: setup.portId }, ); expect(client.createDocument).not.toHaveBeenCalled(); expect(client.sendDocument).not.toHaveBeenCalled(); }); });