135 lines
8.4 KiB
Markdown
135 lines
8.4 KiB
Markdown
|
|
# 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.
|