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>
8.4 KiB
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
suggestforeoi_signedstill 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 torejectedbut the interest stays ateoi. 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
cancelledand the interest stays ateoi. Decision needed: should the interest roll back toqualifiedwhen 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_recentsignal. 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_VIEWEDwebhooks (we storeopenedAt). A signer who opened but didn't sign in 7+ days = stalling. Recommendation: -5 per stalling signer.
Cadence escalation (currently flat)
eoi_awaitingandcontract_awaitingboth 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:
- Ship the positive signals (eoi_sent, deposit_received, contract_signed). Biggest visible win. ~1.5h.
- 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.
- Ship the cadence escalation (eoi_awaiting / contract_awaiting laddered scoring). ~30 min.
- Decide on the external-signed-paths gating question.
- 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.