Bundles the user-prioritised follow-ups from the post-audit punch-list.
Batch A — pipeline + EOI safety:
- §1.1 timeline buildAuditDescription renders diff fields ("leadCategory → hot_lead").
- §4.13 EOI rejection cascade: notification to assigned rep + audit row + rose banner.
- §4.10b finish doc-detail: SigningProgress reuse, linked-entity names (server-resolved),
per-event icons + tooltips + show-more in activity panel.
- §7.2 stage guidance card replaces empty Payments slot pre-reservation.
- §4.15 deal-pulse trigger audit (docs/deal-pulse-trigger-audit.md).
Batch B — UX consistency + docs:
- §1.4 quick log-contact button on interest header.
- §2.1 contact-log compose: Dialog → Sheet.
- §7.1 docs/deal-pulse explainer page; /docs/ in PUBLIC_PATHS.
- DocumentStatus now includes 'rejected' + 'declined' across constants, labels, tone maps.
Audit-side residuals:
- M-NEW-1 /me/ports skips port-context requirement.
- M-AU03 audit log CSV export endpoint + UI button.
- M-IN03 dead receipt-scanner.ts deleted; live path already per-port.
- M-P01 pg_trgm GIN indexes (migration 0071).
- §10.1 webhook tests verified passing (was stale).
Deferred per user direction:
- §11.3 email copy refactor (needs old-CRM reference).
- M-EM03 IMAP bounce-to-interest linking.
Tests: 1374/1374. tsc + lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
|