fix(signing): route paper-signed reservation/contract uploads to the right doc type
The Reservation and Contract tabs reused ExternalEoiUploadDialog, but the service hard-coded the EOI document type, status columns, stage target, and berth rule. A signed contract uploaded from the Contract tab filed as an `eoi`, flipped `eoi_status`, and advanced the stage to `eoi` - wrong doc kind, wrong sub-state, wrong stage. - external-eoi.service: UPLOAD_CONFIG keyed off docType (eoi | reservation | contract) parameterises documentType, file category, storage prefix, doc-status column, signed-date column, target stage, advance-from set, and berth rule. eoi_status is written only for docType=eoi. - route: parse docType from the form (default eoi). - dialog: docType prop; generalised copy; EOI-only UI (active-EOI replace banner, public-map flip, cancelActiveDocumentId) gated to docType=eoi. - reservation/contract tabs: pass docType; drop the coming-soon comments. - test: docType routing cases (reservation -> reservation_agreement + reservation cols; contract -> contract + contract cols; eoi_status stays null on both; contract idempotent at/past contract stage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vite
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { interests } from '@/lib/db/schema/interests';
|
||||
import { documents } from '@/lib/db/schema/documents';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
let uploadExternallySignedEoi: typeof import('@/lib/services/external-eoi.service').uploadExternallySignedEoi;
|
||||
@@ -97,6 +98,7 @@ describe('uploadExternallySignedEoi — stage-advance gate', () => {
|
||||
interestId: string,
|
||||
portId: string,
|
||||
meta: ReturnType<typeof makeAuditMeta>,
|
||||
docType?: 'eoi' | 'reservation' | 'contract',
|
||||
) {
|
||||
return uploadExternallySignedEoi({
|
||||
interestId,
|
||||
@@ -109,6 +111,7 @@ describe('uploadExternallySignedEoi — stage-advance gate', () => {
|
||||
},
|
||||
signedAt: new Date('2026-05-24T10:00:00Z'),
|
||||
signatories: [{ name: 'Client', email: 'client@example.com', role: 'client' }],
|
||||
...(docType ? { docType } : {}),
|
||||
meta,
|
||||
});
|
||||
}
|
||||
@@ -148,4 +151,68 @@ describe('uploadExternallySignedEoi — stage-advance gate', () => {
|
||||
expect(row?.dateEoiSigned).toBeTruthy();
|
||||
},
|
||||
);
|
||||
|
||||
// ── docType routing ────────────────────────────────────────────────────────
|
||||
// Regression coverage for the paper-upload misroute: the reservation and
|
||||
// contract tabs reused this dialog, but the service hard-coded the EOI
|
||||
// documentType / status columns / stage target. A signed contract uploaded
|
||||
// from the Contract tab filed as an `eoi` and flipped `eoi_status` — wrong
|
||||
// doc kind, wrong sub-state, wrong stage. The service now keys all of that
|
||||
// off `docType`.
|
||||
describe('docType routing (reservation / contract)', () => {
|
||||
it('reservation: files as reservation_agreement, advances eoi → reservation, leaves eoi_status null', async () => {
|
||||
const { port, meta, interest } = await makeInterest('eoi');
|
||||
|
||||
const result = await uploadFakeEoi(interest.id, port.id, meta, 'reservation');
|
||||
|
||||
expect(result.stageChanged).toBe(true);
|
||||
expect(result.newStage).toBe('reservation');
|
||||
|
||||
const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
|
||||
expect(row?.pipelineStage).toBe('reservation');
|
||||
expect(row?.reservationDocStatus).toBe('signed');
|
||||
expect(row?.dateReservationSigned).toBeTruthy();
|
||||
// The EOI lifecycle column must NOT be touched by a reservation upload.
|
||||
expect(row?.eoiStatus).toBeNull();
|
||||
expect(row?.eoiDocStatus).toBeNull();
|
||||
|
||||
const doc = await db.query.documents.findFirst({
|
||||
where: eq(documents.id, result.documentId),
|
||||
});
|
||||
expect(doc?.documentType).toBe('reservation_agreement');
|
||||
});
|
||||
|
||||
it('contract: files as contract, advances reservation → contract, sets contract columns only', async () => {
|
||||
const { port, meta, interest } = await makeInterest('reservation');
|
||||
|
||||
const result = await uploadFakeEoi(interest.id, port.id, meta, 'contract');
|
||||
|
||||
expect(result.stageChanged).toBe(true);
|
||||
expect(result.newStage).toBe('contract');
|
||||
|
||||
const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
|
||||
expect(row?.pipelineStage).toBe('contract');
|
||||
expect(row?.contractDocStatus).toBe('signed');
|
||||
expect(row?.dateContractSigned).toBeTruthy();
|
||||
expect(row?.eoiStatus).toBeNull();
|
||||
|
||||
const doc = await db.query.documents.findFirst({
|
||||
where: eq(documents.id, result.documentId),
|
||||
});
|
||||
expect(doc?.documentType).toBe('contract');
|
||||
});
|
||||
|
||||
it('contract: at-or-past contract stage stays put (idempotent), still files the doc', async () => {
|
||||
const { port, meta, interest } = await makeInterest('contract');
|
||||
|
||||
const result = await uploadFakeEoi(interest.id, port.id, meta, 'contract');
|
||||
|
||||
expect(result.stageChanged).toBe(false);
|
||||
expect(result.newStage).toBe('contract');
|
||||
|
||||
const row = await db.query.interests.findFirst({ where: eq(interests.id, interest.id) });
|
||||
expect(row?.pipelineStage).toBe('contract');
|
||||
expect(row?.contractDocStatus).toBe('signed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user