diff --git a/src/lib/services/berth-rules-engine.ts b/src/lib/services/berth-rules-engine.ts index cabe88a3..219f9314 100644 --- a/src/lib/services/berth-rules-engine.ts +++ b/src/lib/services/berth-rules-engine.ts @@ -14,6 +14,7 @@ import { logger } from '@/lib/logger'; export type BerthRuleTrigger = | 'eoi_sent' | 'eoi_signed' + | 'reservation_signed' | 'deposit_received' | 'contract_signed' | 'interest_archived' @@ -39,6 +40,10 @@ interface RuleConfig { const DEFAULT_RULES: Record = { eoi_sent: { mode: 'suggest', targetStatus: 'under_offer' }, eoi_signed: { mode: 'auto', targetStatus: 'under_offer' }, + // Reservation agreement signed — a commitment short of sale, so the berth + // stays Under offer (audit H4); previously reused the contract_signed rule + // and flipped it to Sold prematurely. + reservation_signed: { mode: 'auto', targetStatus: 'under_offer' }, deposit_received: { mode: 'auto', targetStatus: 'sold' }, contract_signed: { mode: 'auto', targetStatus: 'sold' }, interest_archived: { mode: 'suggest', targetStatus: 'available' }, diff --git a/src/lib/services/documents.service.ts b/src/lib/services/documents.service.ts index ff7ffd23..e5dcbf8d 100644 --- a/src/lib/services/documents.service.ts +++ b/src/lib/services/documents.service.ts @@ -988,13 +988,18 @@ export async function uploadSignedManually( if (interest) { void evaluateRule('eoi_signed', doc.interestId, portId, meta); - // Stage stays at 'eoi' - sub-status badge flips to "signed". - void advanceStageIfBehind( + // EOI signed = formal commitment → advance to 'reservation' via the + // GATED helper, matching the Documenso-webhook path (audit H13). + // Previously this manual-upload path used the ungated helper and only + // reached 'eoi', so manually-signed deals lagged a stage behind + // webhook-signed ones and skewed funnel/stage-duration reports. + void advanceStageIfBehindGated( doc.interestId, portId, - 'eoi', + 'reservation', meta, 'Signed EOI uploaded manually', + 'eoi_signed', ); } } @@ -1680,7 +1685,11 @@ export async function handleDocumentCompleted(eventData: { documentId: string; p 'reservation_signed', ); void import('@/lib/services/berth-rules-engine').then(({ evaluateRule }) => - evaluateRule('contract_signed', doc.interestId!, doc.portId, systemMeta), + // Reservation signing is NOT contract signing — firing 'contract_signed' + // here flipped the berth to 'sold' one-to-two stages early (audit H4). + // Use the dedicated 'reservation_signed' trigger (defaults to + // 'under_offer'). + evaluateRule('reservation_signed', doc.interestId!, doc.portId, systemMeta), ); // Tenancies P3 — auto-create pending tenancies (one per in-bundle berth).