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

135 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.