# Lead to Proposal Flow

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 146  
**Tier:** All tiers  
**Depends on:** `marketing-leads-pipeline`, `marketing-catalogs-campaigns`, `foundation-auth-rbac`  
**Referenced by:** `marketing-leads-pipeline`, `marketing-catalogs-campaigns`, `leads-detail-view`

---

## Overview

Spec 22 (`marketing-leads-pipeline`) defines the lead pipeline and `leads-detail-view` shows lead detail. Spec 23 (`marketing-catalogs-campaigns`) defines proposals. No spec documents the cross-module UI flow for creating a proposal from a lead. This spec defines that transition: how a qualified lead becomes a proposal, and how the lead's context (company, contact, notes) pre-fills the proposal editor.

---

## Entry Points

### From Lead Detail

In the lead detail (spec 100 `leads-detail-view`), the action button area:

```
┌──────────────────────────────────────────────────────────────┐
│  Acme Corp — Lead                                [Actions ▾] │
│  Stage: PROPOSAL   Score: 72                                  │
│                                                              │
│  [Convert to customer]    [Send proposal]                    │
└──────────────────────────────────────────────────────────────┘
```

`[Send proposal]` is visible when the lead is in any stage. It becomes a primary CTA when the stage is `PROPOSAL` or `QUALIFIED`.

### From Lead Kanban

In the lead Kanban (spec 22), drag to `PROPOSAL` column → optional prompt:

```
Moved to Proposal stage.
Would you like to create a proposal for Acme Corp?
[Create proposal]  [Skip]
```

---

## Pre-fill Logic

Clicking `[Send proposal]` → opens the proposal editor (spec 130 `proposal-editor`) pre-filled with:

| Field | Pre-filled from |
|-------|----------------|
| Client name | `leads.company` |
| Contact email | `leads.email` |
| Contact name | `leads.name` |
| Intro note | First 200 chars of most recent `lead_activities.body` of type `note` |
| Proposal title | "Proposal for {leads.company}" |

If the lead is already linked to a `customer_id` (via conversion), the customer FK is set on the proposal.

---

## Proposal → Lead Backlink

After the proposal is created:
1. `proposals.lead_id UUID REFERENCES leads(id) ON DELETE SET NULL` is set
2. Lead detail shows a "Proposals" section in the sidebar:

```
Proposals
─────────────────────────────────────
Proposal for Website Redesign   DRAFT
Created today                   [Edit]

Proposal for SEO Campaign       SENT
Sent 3 days ago                 [View]
```

3. Lead activity feed logs: "Proposal created" with link to proposal

---

## Schema Delta

```sql
ALTER TABLE proposals
  ADD COLUMN IF NOT EXISTS lead_id UUID REFERENCES leads(id) ON DELETE SET NULL;

CREATE INDEX IF NOT EXISTS idx_proposals_lead ON proposals(lead_id);
```

---

## Stage Auto-Advance

When a proposal linked to a lead transitions to `SENT`:
- Lead stage advances to `PROPOSAL` if currently `NEW`, `CONTACTED`, or `QUALIFIED`
- Logged as lead activity: "Proposal sent"

When a proposal linked to a lead transitions to `ACCEPTED`:
- Lead stage advances to `WON`
- Lead activity: "Proposal accepted — lead marked Won"

When a proposal linked to a lead transitions to `REJECTED`:
- Lead stage set to `LOST` (with confirmation prompt — user may want to keep open)

When a proposal linked to a lead transitions to `EXPIRED` (the auto-expiry cron in spec 96 `proposal-expiry-deadline`):
- If the lead is still in `PROPOSAL` stage (i.e. this was the proposal that advanced it), the lead is **reverted to `QUALIFIED`** — it left PROPOSAL because no live proposal remains. Leads already past PROPOSAL (`WON`/`LOST`) or that have *another* non-expired proposal still SENT/VIEWED are left untouched.
- Lead activity logged: `type = 'stage_changed'`, `metadata = { from: 'PROPOSAL', to: 'QUALIFIED', reason: 'proposal_expired' }` — message "Proposal expired — lead returned to Qualified for follow-up." This activity is the follow-up signal staff act on (it surfaces in the lead activity feed and recent-activity filters); no new column is introduced.

This runs in the same `proposal.expired` handling as the staff notification; expiry is the only proposal terminal state that does not itself decide WON/LOST, so the lead must not be stranded in PROPOSAL with no live proposal.

---

## API

```
POST /api/proposals
     → create proposal (extended: accepts lead_id)
       body: { lead_id?, customer_id?, title, ... (existing fields) }
       Requires: marketing:write

GET /api/leads/:id/proposals
    → list proposals linked to lead
      Returns: [{ id, title, status, created_at, public_token }]
      Requires: marketing:read

PATCH /api/proposals/:id/stage-transition
      → (internal: called by proposal webhook on status change)
        Advances linked lead stage per rules above
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `proposals.lead_id` FK | Not a separate `lead_proposals` join table | One proposal belongs to at most one lead origin; a nullable FK is simpler and queryable |
| Stage auto-advance | Not manual only | The automatic transition on proposal-sent/accepted/rejected mirrors the real-world sales process and reduces manual data entry |
| Pre-fill from lead | Not blank editor | The lead already has company/contact data; requiring re-entry is friction that slows proposal creation |
