fix(uat-batch-2): external-EOI five-bug bundle (a/b/c/d) + presign filename override
Tackles the linked B4 #5 findings on the external-EOI flow. Item (e) [Edit metadata affordance per row] is deferred to a later wave so it can share infra with the broader signing-flow rework. - (a) lying toast: uploadExternallySignedEoi now returns { stageChanged, newStage }. Client toasts conditionally so a Reservation+ deal that uploads paper-signing evidence no longer claims the stage advanced. - (b) View downloads instead of previewing: SignedPdfActions takes an onView callback; InterestEoiTab lifts a single FilePreviewDialog and passes the callback down. Click-View opens the in-app preview rather than the presigned URL (which the storage backend served as attachment). - (c) UUID filename on download: getDownloadUrl now passes the canonical filename through presignDownloadUrl; S3 backend adds a response-content-disposition override (filename + UTF-8 filename*) to the presign. Filesystem backend already passed it through. - (d) Discarded dateEoiSigned: external-eoi service splits document- metadata writes (always — dateEoiSigned, eoiStatus='signed') from stage advance (gated on past-EOI). Also fires evaluateRule('eoi_signed') so berth-rules stay in sync when an EOI is filed manually. - Default title for external-EOI dialog now derives "External EOI — <Client> — <berth range> — <date>" via the existing formatBerthRange helper; rep can override. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,34 +135,43 @@ export async function uploadExternallySignedEoi(input: ExternalEoiInput) {
|
||||
},
|
||||
});
|
||||
|
||||
// Advance the interest stage to eoi_signed (no-op if already past it).
|
||||
// We bypass canTransitionStage explicitly because the operator just
|
||||
// brought concrete proof that the EOI is signed — that's higher
|
||||
// confidence than a normal forward-jump.
|
||||
if (
|
||||
// Two concerns to keep separate:
|
||||
// 1. Document metadata — always write `dateEoiSigned` + `eoiStatus`
|
||||
// from the upload. Even if the rep already advanced the stage
|
||||
// manually, the paper signing event needs a recorded date so
|
||||
// downstream surfaces (SkipAheadBanner, milestone strip, EOI
|
||||
// merge fields) reflect reality. Honour an existing
|
||||
// dateEoiSigned (don't overwrite if already set — covers the
|
||||
// case where the rep is uploading evidence for an event whose
|
||||
// date was already backfilled).
|
||||
// 2. Stage advance — only when the deal hasn't reached eoi_signed
|
||||
// yet. Bypasses canTransitionStage because the operator just
|
||||
// brought concrete proof.
|
||||
const shouldAdvanceStage =
|
||||
interest.pipelineStage === 'open' ||
|
||||
interest.pipelineStage === 'details_sent' ||
|
||||
interest.pipelineStage === 'in_communication' ||
|
||||
interest.pipelineStage === 'eoi_sent'
|
||||
) {
|
||||
await tx
|
||||
.update(interests)
|
||||
.set({
|
||||
pipelineStage: 'eoi_signed',
|
||||
eoiStatus: 'signed',
|
||||
dateEoiSigned: input.signedAt ?? new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(interests.id, interestId));
|
||||
} else {
|
||||
// Past eoi_signed — just record the document, don't touch stage.
|
||||
await tx.update(interests).set({ updatedAt: new Date() }).where(eq(interests.id, interestId));
|
||||
}
|
||||
interest.pipelineStage === 'eoi_sent';
|
||||
|
||||
return { documentId: doc.id, fileId: fileRecord.id };
|
||||
await tx
|
||||
.update(interests)
|
||||
.set({
|
||||
dateEoiSigned: interest.dateEoiSigned ?? input.signedAt ?? new Date(),
|
||||
eoiStatus: 'signed',
|
||||
...(shouldAdvanceStage ? { pipelineStage: 'eoi_signed' as const } : {}),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(interests.id, interestId));
|
||||
|
||||
return {
|
||||
documentId: doc.id,
|
||||
fileId: fileRecord.id,
|
||||
stageChanged: shouldAdvanceStage,
|
||||
newStage: shouldAdvanceStage ? ('eoi_signed' as const) : interest.pipelineStage,
|
||||
};
|
||||
});
|
||||
|
||||
const { documentId: docId, fileId: fId } = result;
|
||||
const { documentId: docId, fileId: fId, stageChanged, newStage } = result;
|
||||
|
||||
void createAuditLog({
|
||||
portId,
|
||||
@@ -184,5 +193,19 @@ export async function uploadExternallySignedEoi(input: ExternalEoiInput) {
|
||||
|
||||
emitToRoom(`port:${portId}`, 'document:completed', { documentId: docId });
|
||||
|
||||
return { documentId: docId, fileId: fId };
|
||||
// Berth rules engine: a manually-uploaded external EOI is still an
|
||||
// EOI-signed event for the rules that watch this trigger (e.g.
|
||||
// auto-mark the primary berth Under Offer). Fire via dynamic import
|
||||
// to dodge the circular dep between berth-rules-engine and the
|
||||
// interest services.
|
||||
try {
|
||||
const { evaluateRule } = await import('@/lib/services/berth-rules-engine');
|
||||
await evaluateRule('eoi_signed', interestId, portId, meta);
|
||||
} catch {
|
||||
// Swallow — rules engine failures should never block the upload
|
||||
// that the rep has already completed end-to-end. The orphan-reaper
|
||||
// path doesn't apply; a missed rule evaluation is a soft failure.
|
||||
}
|
||||
|
||||
return { documentId: docId, fileId: fId, stageChanged, newStage };
|
||||
}
|
||||
|
||||
@@ -149,7 +149,11 @@ export async function uploadFile(
|
||||
|
||||
export async function getDownloadUrl(id: string, portId: string) {
|
||||
const file = await getFileById(id, portId);
|
||||
const url = await presignDownloadUrl(file.storagePath);
|
||||
// Pass the canonical filename through to the presign so MinIO/S3
|
||||
// returns Content-Disposition with the original name. Without the
|
||||
// override the file lands with the bare storage-key UUID (no
|
||||
// extension) in every browser.
|
||||
const url = await presignDownloadUrl(file.storagePath, 900, file.filename);
|
||||
return { url, filename: file.filename };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user