feat(documents): auto-deposit signed PDFs into entity folders
handleDocumentCompleted resolves the workflow owner via the Owner-wins chain (document.clientId → companyId → yachtId, then interest.clientId → yachtId), ensures the matching entity subfolder, and sets files.folder_id + the matching entity FK on the signed file row. Falls back to root (folder_id=null) when no owner is resolvable. ensureEntityFolder failures are logged at warn level — the signed PDF always lands; the backfill script heals missing folders. The interest fallback omits the company branch because interests table has no companyId column. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,9 @@ import { getPortEoiSigners } from '@/lib/services/documenso-payload';
|
||||
import {
|
||||
listTree,
|
||||
collectDescendantIds,
|
||||
ensureEntityFolder,
|
||||
type FolderNode,
|
||||
type EntityType,
|
||||
} from '@/lib/services/document-folders.service';
|
||||
import type {
|
||||
CreateDocumentInput,
|
||||
@@ -1062,6 +1064,46 @@ export async function handleRecipientSigned(eventData: {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Owner-wins resolution ────────────────────────────────────────────────────
|
||||
|
||||
interface ResolvedOwner {
|
||||
entityType: EntityType;
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner-wins owner resolution chain — see spec §"Routing on workflow
|
||||
* completion" §3a. Returns the first non-null candidate in priority
|
||||
* order: direct client/company/yacht FK on the document, then via the
|
||||
* linked interest's client / yacht FK. The interests table has no
|
||||
* 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<ResolvedOwner | null> {
|
||||
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) {
|
||||
const interest = await db.query.interests.findFirst({
|
||||
where: eq(interests.id, doc.interestId),
|
||||
columns: { clientId: true, yachtId: true },
|
||||
});
|
||||
if (interest?.clientId) {
|
||||
return { entityType: 'client', entityId: interest.clientId };
|
||||
}
|
||||
if (interest?.yachtId) {
|
||||
return { entityType: 'yacht', entityId: interest.yachtId };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function handleDocumentCompleted(eventData: { documentId: string; portId?: string }) {
|
||||
const doc = await resolveWebhookDocument(eventData.documentId, eventData.portId);
|
||||
if (!doc) return;
|
||||
@@ -1085,11 +1127,34 @@ export async function handleDocumentCompleted(eventData: { documentId: string; p
|
||||
sizeBytes: signedPdfBuffer.length,
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
let entityFolderId: string | null = null;
|
||||
if (owner) {
|
||||
try {
|
||||
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.
|
||||
// Logged at warn level: missing entity folder is recoverable via
|
||||
// the backfill script.
|
||||
logger.warn(
|
||||
{ err, documentId: doc.id, owner },
|
||||
'ensureEntityFolder failed during document completion',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [fileRecord] = await db
|
||||
.insert(files)
|
||||
.values({
|
||||
portId: doc.portId,
|
||||
clientId: doc.clientId ?? null,
|
||||
clientId: owner?.entityType === 'client' ? owner.entityId : (doc.clientId ?? null),
|
||||
companyId: owner?.entityType === 'company' ? owner.entityId : (doc.companyId ?? null),
|
||||
yachtId: owner?.entityType === 'yacht' ? owner.entityId : (doc.yachtId ?? null),
|
||||
folderId: entityFolderId,
|
||||
filename: `signed-${doc.id}.pdf`,
|
||||
originalName: `signed-${doc.id}.pdf`,
|
||||
mimeType: 'application/pdf',
|
||||
|
||||
Reference in New Issue
Block a user