fix(audit): H4 (reservation signing berth rule) + H13 (manual EOI-sign stage parity)

H4: reservation_agreement completion fired the contract_signed berth rule,
flipping the berth to 'sold' one-to-two stages early. Add a dedicated
reservation_signed berth trigger (defaults to under_offer) and fire it.
H13: the manual signed-EOI upload path advanced only to 'eoi' via the
ungated helper while the Documenso-webhook path advanced to 'reservation';
both now use advanceStageIfBehindGated(..., 'reservation', 'eoi_signed') so
manually- and webhook-signed deals reach the same stage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:12:02 +02:00
parent 865ae5c072
commit b51d6d3030
2 changed files with 18 additions and 4 deletions

View File

@@ -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<BerthRuleTrigger, RuleConfig> = {
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' },

View File

@@ -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).