# 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.
