Files
pn-new-crm/tests/integration/documents-completion-auto-deposit.test.ts
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

287 lines
9.7 KiB
TypeScript

/**
* 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<typeof import('@/lib/services/documenso-client')>();
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<string, Buffer>();
vi.mock('@/lib/storage', async (importOriginal) => {
const real = await importOriginal<typeof import('@/lib/storage')>();
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);
});
});