# Conversational collect — HyperSender WhatsApp bot

## Overview

The `conversational` distribution channel sends an opt-in message via HyperSender (`send-text-safe`), then runs a WhatsApp chat bot for **supported question types** from the published survey (flat document order). Completed answers are stored once via `SurveyResponseService::storeResponse()`.

Survey logic (skip rules, carry-forward, validation rules in `ruleSections`) is **not** evaluated in v1.

## Supported question types

| Dialog label | Survey `typeId` | How to reply on WhatsApp |
|---|---|---|
| Single choice | `single_choice` | Number (1–N) or option label |
| Single select dropdown | `dropdown_single` | Number or option label |
| Multiple choice | `multiple_choice` | Comma-separated numbers or labels (e.g. `1, 3`) |
| Textbox | `text_single`, `text_area` | Free text in one message |
| Single NPS rating | `nps_scale` | Integer `0`–`10` |
| Ordering | `ranking_order`, `ranking_drag`, `ranking_click` | Comma-separated option numbers in rank order (e.g. `2, 1, 3`) |

**Not supported** (skipped in the bot): `dropdown_multiple`, `nps_multiple`, `nps_advanced`, `ranking_constant_sum`, `ranking_card_sort`, `ranking_card_swipe`, matrix/media/upload, and all other builder types.

## Environment

| Variable | Purpose |
|----------|---------|
| `HYPERSENDER_API_TOKEN` | API bearer token |
| `HYPERSENDER_INSTANCE_ID` | WhatsApp instance id |
| `HYPERSENDER_API_BASE_URI` | API base (default Hypersender v1 URL) |
| `HYPERSENDER_WEBHOOK_SECRET` | Optional shared secret for inbound webhooks |
| `SURVEY_COLLECT_CONVERSATIONAL_QUEUE` | Queue name (default `conversational-collect`) |
| `SURVEY_COLLECT_CONVERSATIONAL_STAGGER_SECONDS` | Delay between per-recipient sends |
| `SURVEY_COLLECT_CONVERSATIONAL_DISPATCH_CHUNK_SIZE` | Invite batch size per dispatch job |

## Webhook URL

Configure your Hypersender instance to POST `message.any` events to:

```
{APP_URL}/api/v1/webhooks/hypersender/whatsapp
```

When `HYPERSENDER_WEBHOOK_SECRET` is set, send it as header `X-Hypersender-Webhook-Secret` or query `?secret=`.

## Flow

1. User saves conversational collect with opening message + recipients → `DispatchConversationalCollectChannelJob` queues opt-in sends.
2. Recipient replies on WhatsApp → webhook → `ProcessHypersenderWhatsAppWebhookJob` → `ConversationalBotOrchestrator`.
3. Yes → bot asks each supported question in published document order.
4. After last answer → one survey response row + invite marked completed.

## Re-engagement after a closed session

A conversational session is closed when the recipient opts out, completes the survey, or expires. Closed sessions stop processing further inbound replies on purpose, so a single user can't accidentally restart mid-survey by sending a stray message.

A new opt-in dispatch re-opens the conversation:

- **Same channel, same phone:** `SendConversationalCollectRecipientJob` looks up the existing session by `(channel_id, phone_hash)`. If it is `completed`, `opted_out`, or `expired`, the session is reset to `opt_in_pending` (cursor cleared, pending answers cleared) and the stale `whatsapp_chat_id` on the invite is dropped so the next inbound reply re-binds correctly.
- **Different channel (new collector), same phone:** the webhook job's "switch to a newer invite" fallback also fires when the originally-matched invite has a terminal session (not only when the invite status is terminal). The inbound is then routed to the most recent non-terminal invite for the same phone, where its fresh session handles the Yes/No flow.

These two behaviors together mean: every new conversational opt-in send to a number restarts the Yes/No flow on the next reply, regardless of how the previous conversation ended.

## References

- [Hypersender v2 documentation](https://docs.hypersender.com/v2/hypersender)
- [message.any webhook](https://docs.hypersender.com/v2/api-reference/whatsapp-webhooks/messages-any)
- [Avoid getting blocked](https://docs.hypersender.com/v2/hypersender/overview/whatsapp/avoid-getting-blocked.md)
