feat(interests): upload externally-signed EOI (paper / non-Documenso)
Sales reps need to file EOIs that were signed outside Documenso — on paper, in person at a boat show, or via an alternate e-sign vendor. Until now the EOI flow assumed Documenso was the only path. - external-eoi.service.uploadExternallySignedEoi creates BOTH the document row AND the signed-file record in one shot. Document is marked isManualUpload=true with status=completed and signedFileId set. Distinct from the existing uploadSignedManually which augments a document row that came from the Documenso pathway. - POST /api/v1/interests/[id]/external-eoi accepts multipart with the PDF + optional title + signedAt date + comma-separated signer names + free-text notes. Gated on documents.upload_signed permission. - Interest stage auto-advances to eoi_signed (only when the interest is currently at or before eoi_sent — past that, just file the doc). - The signing date, signer names, and any notes are captured into document_events.eventData + the audit_log metadata so the audit trail records who said the document was signed and when. - ExternalEoiUploadDialog renders a small modal: file picker, title override, signed-date (defaults to today), comma-separated signer names, notes. Wired into interest-detail-header behind an Upload icon button (gated on documents.upload_signed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
src/app/api/v1/interests/[id]/external-eoi/route.ts
Normal file
61
src/app/api/v1/interests/[id]/external-eoi/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { uploadExternallySignedEoi } from '@/lib/services/external-eoi.service';
|
||||
import { errorResponse, NotFoundError, ValidationError } from '@/lib/errors';
|
||||
|
||||
export const POST = withAuth(
|
||||
withPermission('documents', 'upload_signed', async (req, ctx, params) => {
|
||||
try {
|
||||
const interestId = params.id;
|
||||
if (!interestId) throw new NotFoundError('Interest');
|
||||
|
||||
const form = await req.formData();
|
||||
const file = form.get('file');
|
||||
if (!file || !(file instanceof File)) {
|
||||
throw new ValidationError('Missing file');
|
||||
}
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const title = (form.get('title') as string | null) ?? undefined;
|
||||
const notes = (form.get('notes') as string | null) ?? undefined;
|
||||
const signerNamesRaw = (form.get('signerNames') as string | null) ?? '';
|
||||
const signerNames = signerNamesRaw
|
||||
? signerNamesRaw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
const signedAtRaw = (form.get('signedAt') as string | null) ?? null;
|
||||
const signedAt = signedAtRaw ? new Date(signedAtRaw) : undefined;
|
||||
if (signedAt && Number.isNaN(signedAt.getTime())) {
|
||||
throw new ValidationError('Invalid signedAt');
|
||||
}
|
||||
|
||||
const result = await uploadExternallySignedEoi({
|
||||
interestId,
|
||||
portId: ctx.portId,
|
||||
fileData: {
|
||||
buffer,
|
||||
originalName: file.name,
|
||||
mimeType: file.type || 'application/pdf',
|
||||
size: file.size,
|
||||
},
|
||||
title,
|
||||
signedAt,
|
||||
signerNames,
|
||||
notes,
|
||||
meta: {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ data: result });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user