# Lead capture form

End-to-end feature that lets the AI agent display a structured contact form
inside the widget and persist the submission as a lead.

## Flow

1. The visitor expresses an explicit recontact intent ("rappelez-moi", "je veux
   être contacté"…).
2. The n8n AI agent calls the `display_lead_form` tool. A post-agent code node
   detects the call via `intermediateSteps` and replies with a non-streamed
   JSON envelope `{ response, form }` (instead of the usual NDJSON stream).
3. The widget receives the envelope, locks the composer, and renders the form
   inline as a bot bubble (`src/lead-form.js`).
4. On submit, the widget POSTs `{ type: "form_submission", botId, formId,
   data, consent, … }` to `/webhook-proxy`.
5. The proxy validates, enforces rate limits, checks the `chatbots.enabled_features.leads`
   flag, then forwards the payload to the same n8n webhook.
6. n8n branches on `type === "form_submission"`, persists the lead in
   `public.leads`, sends the Brevo notification email, writes a memory marker
   so the agent does not re-ask, and answers a short confirmation text.
7. The widget replaces the form with a user "summary" bubble and renders the
   confirmation as a normal bot message; the composer is unlocked.

## Where things live

### Frontend (`src/`)

- `lead-form.js` — schema normalisation, validation, draft persistence,
  honeypot, DOM renderer.
- `transport.js` — `extractFormFromJson` + `postFormSubmission`.
- `animation.js` — `displayLeadForm` and `displaySubmittedSummaryAsUser`.
- `widget.js` — orchestration: `renderLeadFormFromAgent`,
  `handleLeadFormSubmit`, composer lock, history clear hook, destroy cleanup.
- `i18n.js` — FR/EN strings (`leadForm*` keys).
- `styles.css` — `.chatyx-lead-form*` block at the end of the file.

### Backend (`webhook-proxy/`)

- `src/form-operations.ts` — `isFormSubmissionMessage` + `handleFormSubmission`
  (validation, leads-feature flag, in-memory rate limit, forward to n8n).
- `src/index.ts` — routes `form_submission` payloads BEFORE chat detection.
- `_shared/validation-schemas.ts` — `formSubmissionSchema` (Zod).

### Database

- `migrations/20260508_create_leads_table.sql` — `public.leads` with RLS that
  mirrors `chat_messages` (service role writes only, owner reads).

## Activation per chatbot

The feature is gated by the existing `chatbots.enabled_features.leads`
boolean. To turn it on:

```sql
update public.chatbots
   set enabled_features = jsonb_set(coalesce(enabled_features, '{}'::jsonb), '{leads}', 'true')
 where id = '<bot-uuid>';
```

The form schema (which fields are required) lives in the per-chatbot n8n
workflow as a `Set` node, see `LEAD_CAPTURE_INTEGRATION.md` in the workflow
repo. The chatbot's `notification_email` column is used as the Brevo
destination.

## Response shape (n8n → widget)

```json
{
  "response": "Pour qu'un conseiller vous recontacte, merci de remplir ce formulaire :",
  "form": {
    "id": "lead_capture",
    "version": 1,
    "title": "Vos coordonnées",
    "submitLabel": "Envoyer",
    "fields": [
      { "name": "name",         "label": "Prénom ou nom",     "type": "text",     "required": true,  "maxLength": 80,  "autocomplete": "name" },
      { "name": "email",        "label": "Email",             "type": "email",    "required": true,  "maxLength": 120, "autocomplete": "email" },
      { "name": "phone",        "label": "Téléphone",         "type": "tel",      "required": false, "maxLength": 30,  "autocomplete": "tel" },
      { "name": "reason",       "label": "Raison du contact", "type": "textarea", "required": false, "maxLength": 500 },
      { "name": "availability", "label": "Disponibilités",    "type": "text",     "required": false, "maxLength": 200 }
    ],
    "consent": { "label": "J'accepte d'être recontacté(e)", "required": true }
  }
}
```

## Submission shape (widget → webhook-proxy → n8n)

```json
{
  "type": "form_submission",
  "botId": "<chatbot uuid>",
  "formId": "lead_capture",
  "formVersion": 1,
  "user_id": "<chatyx visitor uuid>",
  "session_id": "<same as user_id>",
  "data": { "name": "Alice", "email": "alice@example.com", "phone": "+33…" },
  "consent": true,
  "consentLabel": "J'accepte d'être recontacté(e)",
  "submittedAt": "2026-05-08T10:30:00.000Z"
}
```

## Tests

- `tests/lead-form.test.js` — schema normalisation, validation regexes, DOM
  smoke test, `extractFormFromJson` parser. 16 new tests.
- The full suite (`npm test`) runs 271 tests including these.

## Security & RGPD

- `consent` is mandatory server-side (`webhook-proxy` returns 400 if absent).
- The verbatim `consentLabel` shown at the moment of submission is forwarded
  to n8n (and persisted in `leads.consent_label`) as RGPD proof of
  information.
- IP hashing should happen in the n8n workflow before insert (`pgcrypto`
  available — `encode(digest(ip, 'sha256'), 'hex')`).
- A honeypot field (`website_url`) and an in-memory rate limit (3
  submissions / hour / user-or-IP / chatbot) deter spam.
- Form schema is normalised client-side (max 10 fields, max 1 KB per value)
  and revalidated server-side via Zod.
- The widget never trusts the n8n response blindly: `extractFormFromJson`
  filters non-object payloads and missing `fields` arrays.

## Known gaps / future work

- Rate limit is per edge instance (in-memory). Promote to Redis or a Supabase
  table if abuse appears.
- A consent-revocation endpoint and a leads-export tool for owners are not
  in scope here.
- The `display_lead_form` tool detection in n8n is currently expected to be
  done in a Code node post-agent (see workflow integration doc); if the n8n
  Agent SDK exposes structured tool-call events natively, that is the
  preferred path.
