# Deal Pulse & Pipeline Trigger Audit — 2026-05-18 Per MANUAL-TESTING-BACKLOG-2026-05-15 §4.15: map every place that moves an interest's pipeline stage OR contributes to the deal-pulse score, and call out the gaps. --- ## 1. Pipeline-stage auto-advance — call-site map `advanceStageIfBehind(interestId, portId, target, meta, reason?)` is the canonical "advance if not already past target" helper. The `*Gated` variant honours the per-port `stage_advance_rules` setting (auto / suggest / off). | Trigger | Caller | Target | File:line | Gated? | | ------------------------------------ | ----------------------------- | --------------------------------------------------------- | --------------------------------------- | -------------------------------- | | EOI sent (manual rep generate) | `generateAndSign` | `eoi` | `documents.service.ts:843` | gated (eoi_sent) | | EOI signed (all parties via webhook) | `handleDocumentCompleted` | `reservation` | `documents.service.ts:1610` | gated (eoi_signed) | | Reservation signed | `handleDocumentCompleted` | `reservation` (no change, stage stays + status sub-flips) | `documents.service.ts:1640` | gated (reservation_signed) | | Deposit received in full | `recordPayment` | `deposit_paid` | `payments.service.ts:134` | gated (deposit_received) | | Sales contract signed | `handleDocumentCompleted` | `contract` | `documents.service.ts:1671` | gated (contract_signed) | | Deposit invoice paid (alt path) | `markInvoicePaid` | `deposit_paid` | `invoices.ts:684` | gated (deposit_received) | | Custom document upload | `confirmCustomDocumentUpload` | document-type-specific (eoi/reservation/contract) | `custom-document-upload.service.ts:354` | **NOT gated** (uses base helper) | | External-eoi mark-as-signed | inline in handler | `reservation` | `documents.service.ts:859` | **NOT gated** | | Externally-signed contract | inline in handler | `contract` | `documents.service.ts:971` | **NOT gated** | | Manual stage move | `changeInterestStage` | any (with override) | `interests.service.ts:840` | manual / not gated | ### Gaps flagged - **External-signed paths bypass the per-port rules.** A port set to `suggest` for `eoi_signed` still gets an auto-advance when the rep marks the doc externally signed. Decision needed: should the rules table also gate the external-signed paths? Argument for yes: the rep's intent ("I just want to mark this signed") is the same as the webhook case. Argument for no: the rep is explicitly choosing to bypass the digital flow, so an auto-advance is what they expect. - **Custom document upload is not gated.** Same trade-off as above. - **No stage rollback on rejection.** When a signer declines an EOI (`handleDocumentRejected`), the doc flips to `rejected` but the interest stays at `eoi`. Confirm: this is correct — the deal isn't dead, the EOI is. Rep should regenerate. **Verdict: keep as-is.** - **No stage rollback on cancel.** When the rep cancels an in-flight EOI, the doc flips to `cancelled` and the interest stays at `eoi`. Decision needed: should the interest roll back to `qualified` when the only EOI is cancelled with no replacement? **Recommendation: NO** — keeps history honest; a cancel is the rep's deliberate signal that they're regenerating, not retreating. --- ## 2. Deal-pulse signals — `computeDealHealth` map Source: `src/lib/services/deal-health.ts`. Each `signals.push` site documented with its trigger condition + score delta: | Signal | Delta | Condition | File:line | | ------------------- | -------------------- | --------------------------------------------------- | ------------------ | | `active_engagement` | +5 | Any contact-log entries in last 7 days | deal-health.ts:101 | | `contact_recent` | +20 | `dateLastContact <= 7 days` ago | deal-health.ts:115 | | `contact_warm` | +10 | `dateLastContact <= 14 days` (else of above) | deal-health.ts:122 | | `contact_stale` | -15 | `dateLastContact >= 30 days` | deal-health.ts:129 | | `stage_progress` | +10/+20/+30 (capped) | Per pipelineStage index | deal-health.ts:142 | | `stuck_top_funnel` | -10 | `firstDays >= 30` AND stage in {enquiry, qualified} | deal-health.ts:157 | | `eoi_awaiting` | -10 | `eoiSentDays >= 14` AND not signed | deal-health.ts:173 | | `deposit_pending` | -10 | reservation signed >= 21d AND no deposit | deal-health.ts:184 | | `contract_awaiting` | -10 | contract sent >= 14d AND not signed | deal-health.ts:200 | ### Positive signals that are MISSING (gaps) - **EOI sent** — no `eoi_sent_recent` signal. Sending an EOI is the single biggest "this deal just got serious" moment but the score doesn't move when it happens. **Recommendation: +15 at < 7 days.** - **Deposit received** — same gap. A deposit landing should bump the score significantly. **Recommendation: +20, decays over 30 days.** - **Contract signed** — terminal positive event; should ladder the deal to its max. **Recommendation: +30 at < 14 days.** ### Negative signals that are MISSING (gaps) - **Signer declined / EOI rejected** — when the §4.13 rejection path fires, the score should drop noticeably (the deal is suddenly at risk). **Recommendation: -25, decays over 14 days.** - **Interest archived-and-unarchived cycle** — zombie deals that bounce in and out should be flagged. Detect via the audit-log archive/restore pattern. **Recommendation: -10 if archived+restored within last 30 days.** - **Reservation cancelled** — similar to EOI rejected; signals the deal is at risk. **Recommendation: -20.** - **Berth status flipped to sold-to-other** — the deal's primary berth was sold to a different interest. **Recommendation: -30 (catastrophic).** - **Signer engagement** — Documenso fires `RECIPIENT_VIEWED` webhooks (we store `openedAt`). A signer who opened but didn't sign in 7+ days = stalling. **Recommendation: -5 per stalling signer.** ### Cadence escalation (currently flat) - `eoi_awaiting` and `contract_awaiting` both apply a flat -10 at the 14-day threshold. **Recommendation: ladder to -20 at 21d, -30 at 30d** so prolonged stalling shows up more visibly. --- ## 3. Heat tooltip explainer copy The DealPulseChip popover (`src/components/interests/deal-pulse-chip.tsx`) references signals by name. With the gaps above closed, the tooltip's enumerated list needs the new signals added so the in-app copy matches the computation. The new `/docs/deal-pulse` explainer page (shipped this wave, §7.1) should also be kept in sync with the signal set. --- ## 4. Suggested fix wave (decisions needed from Matt) Per the doc structure, these are the punch-list items in priority order: 1. **Ship the positive signals (eoi_sent, deposit_received, contract_signed).** Biggest visible win. ~1.5h. 2. **Ship the rejection / risk signals (eoi_rejected, reservation_cancelled, berth_sold_to_other).** Pairs naturally with the §4.13 rejection cascade we shipped this wave. ~2h. 3. **Ship the cadence escalation (eoi_awaiting / contract_awaiting laddered scoring).** ~30 min. 4. **Decide on the external-signed-paths gating question.** 5. **Decide on the cancel-stage-rollback question.** Each is small individually; combined the deal-pulse model gets meaningfully more accurate. Suggest bundling 1–3 into one PR for review economy.