> ## Documentation Index
> Fetch the complete documentation index at: https://docs.switchbord.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Futura campaign go-live testing

> Operational test plan for typed audiences, recipient materialization, personalized templates, button replies, consent opt-out, and manual follow-up before a Futura campaign goes live.

# Futura campaign go-live testing

Use this runbook before launching a Futura WhatsApp campaign. The flow depends on typed segment definitions, a materialized recipient snapshot, template personalization, normalized button replies, consent handling, and staffed manual follow-up for interested replies.

<Note>
  The durable BORD-433/BORD-441 launch package lives in `planning/campaigns/futura-launch-runbook.md`, `planning/campaigns/futura-dry-run-fixtures.json`, `planning/futura/futura-strategy-templates-compliance.md`, and `planning/campaigns/bord-431-campaign-journey-scope note.md`.
</Note>

<Warning>
  BORD-431 campaign response `start_journey` is not implemented for the MVP launch. Futura production route metadata must use tag/contact attribute/open conversation/opt-out actions only, with manual human follow-up for interested contacts.
</Warning>

<Warning>
  Do not use this checklist to send to a production audience until the campaign has a frozen recipient snapshot, an approved template, and an explicit opt-out path.
</Warning>

## Scope

The just-merged campaign flow supports:

* audience definitions with `all`, `tags`, or `segment` targeting;
* typed custom-field filters for segment audiences;
* recipient materialization into `campaign_recipients` before dispatch;
* template binding by approved template name or template ID, locale, and body variables;
* normalized button reply IDs for campaign and journey routing;
* campaign-button opt-out events written to the consent ledger;
* deterministic route actions for contact tags, contact attributes, open conversations, and opt-outs.

The MVP launch explicitly does not include BORD-431 campaign response `start_journey` execution.

## Campaign metadata contract

Futura campaign drafts should carry the v1 campaign metadata alongside the legacy campaign columns. The metadata is the go-live contract operators review before materialization.

```json theme={null}
{
  "audienceDefinition": {
    "kind": "tags",
    "tagIds": ["tag-vip"],
    "tagLabels": ["Futura"],
    "snapshotAt": "2026-04-27T07:00:00.000Z"
  },
  "templateBinding": {
    "templateName": "futura_repeat_guest_check_it",
    "locale": "it",
    "variables": [
      {
        "component": "body",
        "index": 0,
        "source": "contact.first_name",
        "fallback": "cliente"
      }
    ]
  },
  "responseRoutes": {
    "futura_not_booked": {
      "label": "Non ho ancora prenotato",
      "actions": [
        { "type": "tag_contact", "tagLabel": "futura_not_booked" },
        { "type": "open_conversation", "queue": "sales" },
        { "type": "set_contact_attribute", "attribute": "futura_response", "value": "not_booked" }
      ]
    },
    "futura_already_booked": {
      "label": "Ho già prenotato",
      "actions": [
        { "type": "tag_contact", "tagLabel": "futura_already_booked" },
        { "type": "set_contact_attribute", "attribute": "futura_response", "value": "already_booked" }
      ]
    },
    "marketing_opt_out": {
      "label": "Non voglio ricevere messaggi",
      "actions": [{ "type": "record_opt_out", "reason": "marketing_button" }]
    }
  },
  "launchSafety": {
    "requireDryRun": true,
    "maxRecipients": 500,
    "requireOptOutFooter": true
  }
}
```

## Preflight checklist

<Steps>
  <Step title="Confirm the audience source">
    Choose `tags` for the initial Futura list or `segment` when the campaign depends on contact attributes. Avoid `all` unless the workspace explicitly approved a full-list send.
  </Step>

  <Step title="Validate typed segment filters">
    For `segment` audiences, check every filter before materialization. Supported fields are `tags`, `name`, `phone`, and `custom_fields`. Custom fields use `key=value` syntax; `gt` and `lt` comparisons must compare number or date values, either from custom field definitions or values that parse as numbers or ISO-style dates.
  </Step>

  <Step title="Preview the segment count">
    Preview the segment in the app or via the same backend path used by `previewSegment`. Confirm the count and a sample of contacts against the campaign brief.
  </Step>

  <Step title="Check consent before snapshotting">
    The materializer skips contacts whose `consent_state` is `opted_out` or whose legacy `subscriber_status` is `unsubscribed`. Confirm the expected opt-out count with the campaign owner before continuing.
  </Step>

  <Step title="Verify the template binding">
    Confirm the WhatsApp template is approved in Meta and in Switchbord, the `locale` matches the approved language code, every body variable has a source plus fallback, body value propositions use WhatsApp bolding, and the opt-out quick reply is visible as `Stop promozioni` or `Non scrivermi` rather than `STOP`.
  </Step>

  <Step title="Freeze routes and safety limits">
    Confirm `responseRoutes` only uses the supported button IDs: `futura_not_booked`, `futura_already_booked`, and `marketing_opt_out`. Confirm route actions do not include `start_journey` for MVP launch. Confirm `launchSafety.maxRecipients` is at or below the approved send size.
  </Step>
</Steps>

## Materialize recipients

Materialization turns the audience definition into durable `campaign_recipients` rows. Treat this as the audience snapshot for the send.

Expected behavior:

* reads the campaign `audienceDefinition` from v1 metadata;
* resolves `all`, tag, or segment audiences within the workspace;
* deduplicates contacts by ID;
* skips opted-out or unsubscribed contacts;
* inserts new `queued` recipients with a unique `campaign_id,contact_id` key;
* updates campaign metadata with `materialization.materializedCount`, `skippedOptOutCount`, `duplicateCount`, and `materializedAt`.

After materialization, verify:

* `materializedCount` matches the approved count after consent skips;
* `skippedOptOutCount` is explainable and nonzero only when expected;
* a second materialization run reports duplicates instead of creating duplicate recipients;
* no `campaign_recipients` rows were created for contacts outside the workspace;
* no rows exist for known opted-out test contacts.

## Dispatch and personalization checks

Campaign dispatch only processes recipients in `queued` status while the campaign is `running`. It sends in batches and requeues `campaign.dispatch` until no queued recipients remain.

For each test recipient, verify:

* a conversation exists or is created in the expected channel;
* an outbound `messages` row is queued with `send_mode: template` and `author_label: Campaign`;
* `payload.templateName` is the bound template name;
* `payload.templateLocale` is present when a locale is configured;
* `payload.templateComponents` includes body parameters in index order;
* `contact.first_name` resolves from `contacts.metadata.first_name` first, then from the first token of `contacts.name`, then to the configured fallback;
* the rendered message body preview replaces `{{1}}`, `{{2}}`, and later placeholders consistently with the template components;
* opted-out recipients encountered at dispatch are marked failed with `error_code: skipped_opt_out` and are not sent.

<Note>
  Recipient materialization skips known opt-outs up front, but dispatch rechecks consent so contacts who opt out between materialization and send are still blocked.
</Note>

## Button reply normalization

Inbound webhook processing normalizes both WhatsApp button formats into a single button ID before route handling:

* legacy reply buttons: `message.button.payload`;
* interactive buttons: `message.interactive.button_reply.id`;
* interactive list replies: `message.interactive.list_reply.id`.

Test all campaign buttons using their exact payload IDs:

| Button ID               | Expected result                                                                                   |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| `futura_not_booked`     | The contact can be tagged, marked as not booked, and the conversation opened for sales follow-up. |
| `futura_already_booked` | The contact can be tagged/marked as already booked without opt-out or automatic journey start.    |
| `marketing_opt_out`     | A consent opt-out event is recorded and the contact is marked unsubscribed.                       |

Reject route definitions that use button labels, translated titles, spaces, or casing variants as keys. Routes must use normalized IDs, not display text.

## Consent opt-out test

For the `marketing_opt_out` button:

1. Send a test campaign message to a consenting seed contact.
2. Reply by tapping the opt-out button, not by typing a STOP keyword.
3. Confirm a `consent_events` row is written with `event_type: opted_out`, `source: campaign_button`, `actor: system`, and metadata containing the `buttonId` and provider message ID.
4. Confirm the contact `subscriber_status` is updated to `unsubscribed` and the contact is no longer eligible for subsequent campaign materialization.
5. Confirm later dispatch attempts skip the contact before creating a Meta send.

Keyword opt-out handling still applies for text STOP/START style messages, but campaign-button opt-out must work without keyword parsing.

## Manual follow-up instead of campaign-started journeys

BORD-431 campaign response `start_journey` execution is scope noted from the MVP launch. A route action with `type: "start_journey"` is accepted by the domain schema but is not executed as a campaign-to-journey attachment/start path. Operators must not depend on it for Futura launch follow-up.

Use this manual follow-up path:

* `futura_not_booked` tags the contact, records `futura_response=not_booked`, and opens/routes the conversation to sales;
* a human operator reviews the conversation and sends the next approved reply manually;
* `futura_already_booked` tags or marks the contact as already booked and requires no automatic journey;
* `marketing_opt_out` records consent opt-out and must never trigger a marketing follow-up.

The later BORD-431 follow-up PR must add idempotent journey start/attachment with campaign context before this runbook can rely on automated journey starts.

## Operational go-live checklist

Before production launch:

* [ ] Campaign metadata validates against v1 schema.
* [ ] Segment preview or tag count matches the signed-off audience.
* [ ] Typed custom-field filters were tested with at least one matching and one non-matching seed contact.
* [ ] Recipient materialization counts were recorded in launch notes.
* [ ] Approved template name, locale, and body-variable fallbacks were verified.
* [ ] Template component payloads were inspected for at least two seed contacts.
* [ ] Button IDs were tested from WhatsApp, not only from mocked webhook payloads.
* [ ] `marketing_opt_out` created a `campaign_button` consent ledger event.
* [ ] Post-opt-out materialization and dispatch both skipped the seed contact.
* [ ] Manual follow-up staffing is assigned for `futura_not_booked` and any optional `futura_already_booked` replies.
* [ ] Production route metadata contains no `start_journey` actions.
* [ ] `campaign.dispatch` and `message.dispatch` outbox jobs drained without unexpected failures.
* [ ] Delivery, read, failed, and skipped counts were compared with campaign detail metrics.
* [ ] Incident owner and rollback/pause decision point are documented.

## Rollback and pause criteria

Pause the campaign and stop queue processing if any of these happen:

* materialization includes contacts outside the approved audience;
* opt-out contacts receive campaign messages;
* button replies are recorded as display labels instead of normalized IDs;
* launch planning depends on automatic campaign-to-journey start before BORD-431 is implemented;
* Meta returns sustained policy or template errors for the Futura template;
* delivery failures exceed the campaign owner's agreed threshold.

When pausing, record campaign ID, workspace ID, materialization counts, current queued/accepted/failed recipient counts, and representative outbox job IDs in the incident notes.
