Files
pn-new-crm/docs/deal-pulse-trigger-audit.md
Matt 0f99f054b3 feat(post-audit): batch A+B quick-wins + audit-side residuals
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>
2026-05-18 14:22:11 +02:00

8.4 KiB
Raw Blame History

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 13 into one PR for review economy.