# Web Form Request — Example

Example request/response for `GET /api/public/v1/web-forms/{identifier}`, based on
`App\Http\Controllers\PublicApi\V1\WebFormShowController` and the `App\Http\Middleware\RecordWebFormHit`
middleware that records the hit and session after the response is sent.

## Request

```
GET /api/public/v1/web-forms/enrollment-form?product_id=42&utm=utm_source%3Dgoogle%26utm_medium%3Dcpc%26utm_campaign%3Dsummer_promo
Accept: application/json
User-Agent: Mozilla/5.0 (...)
```

### Query parameters

| Param | Required? | Notes |
|---|---|---|
| `product_id` | Required; must exist in `products` | Resolves the product, its utility, and supplier. |
| `enrollment_id` | Required only for `credit_check` type forms, optional otherwise | When present, prepopulates fields from the referenced `EnrollmentRequest.body`. |
| `session_id` | Optional | Reused if it's a valid UUID (`resolveSessionId()`); otherwise a new one is generated. |
| `utm` | Optional | A URL-encoded query string, e.g. `utm_source=google&utm_medium=cpc&utm_campaign=summer_promo`. Parsed and stored on the `WebFormSession` record. |
| Any field name matching a form field | Optional | Used to prepopulate that field's `value` in the response. |

## Response

`200 OK`

```json
{
  "data": {
    "id": 3,
    "slug": "enrollment-form",
    "name": "Standard Enrollment",
    "description": "...",
    "post_path": "/api/public/v1/enrollment-requests",
    "session_id": "b3f1a2c4-1234-4a5b-9cde-abcdef123456",
    "fields": [
      {
        "id": 12,
        "name": "email",
        "label": "Email Address",
        "type": "email",
        "options": null,
        "required": true,
        "placeholder": null,
        "validation_pattern": null,
        "validation_message": null,
        "help_text": null
      },
      {
        "id": null,
        "name": "primary_account_identifier",
        "label": "ESIID",
        "type": "text",
        "options": null,
        "required": true,
        "placeholder": null,
        "validation_pattern": "^[0-9]{17}$",
        "validation_message": null,
        "help_text": "17-digit number found on your utility bill"
      },
      {
        "id": null,
        "name": "affirmation_5_v2",
        "label": "I authorize the enrollment...",
        "type": "checkbox",
        "required": true,
        "affirmation_id": 5,
        "version_id": 2,
        "version": 2
      }
    ],
    "sections": [
      {
        "name": null,
        "label": null,
        "description": null,
        "display_order": 0,
        "fields": [
          {
            "name": "primary_account_identifier",
            "label": "ESIID",
            "type": "text",
            "options": null,
            "required": true,
            "placeholder": null,
            "validation_pattern": "^[0-9]{17}$",
            "validation_message": null,
            "help_text": "17-digit number found on your utility bill",
            "display_order": 0
          }
        ]
      },
      {
        "name": "contact_information",
        "label": "Contact Information",
        "description": "How we reach you.",
        "display_order": 1,
        "fields": [
          {
            "name": "email",
            "label": "Email Address",
            "type": "email",
            "options": null,
            "required": true,
            "placeholder": null,
            "validation_pattern": null,
            "validation_message": null,
            "help_text": null,
            "display_order": 0
          }
        ]
      },
      {
        "name": "authorization",
        "label": "Authorization",
        "description": null,
        "display_order": 2,
        "fields": [
          {
            "name": "affirmation_5_v2",
            "label": "I authorize the enrollment...",
            "type": "checkbox",
            "required": true,
            "affirmation_id": 5,
            "version_id": 2,
            "version": 2,
            "display_order": 0
          }
        ]
      }
    ]
  }
}
```

### `fields` vs. `sections`

`fields` is the original flat array, retained **byte-identical** (including the `id` key) for
backward compatibility — **it is deprecated; prefer `sections`.**

`sections` groups the same fields by their admin-configured
[form section](../app/Models/FormSection.php). Fields with no section are grouped into a single
section with `name`/`label`/`description` all `null`, always emitted **first**. Sections and the
fields inside them each carry a contiguous, 0-based `display_order` (not the same as the
underlying `order` column — it's a render index over what's actually emitted, so it stays
gap-free even when a section with no visible fields is omitted, which happens whenever every
field in it was filtered out, e.g. by energy-type or bill-delivery-preference rules). Neither a
section nor a field inside `sections` carries an `id` — the client doesn't need it.

A web form can include a "remaining utility fields" or "remaining supplier fields" block (see
`FormFormField.is_remaining_utility_fields_block` / `is_remaining_supplier_fields_block`),
placed like the existing affirmations block, to control which section a form's dynamically
resolved utility/supplier fields land in. When such a block isn't placed for a given form, those
fields — like an unplaced affirmations block — fall back to the leading, unsectioned section
in `sections` (this differs from `fields`, where they're always appended at the end).

## Side effects

After the response is sent, `RecordWebFormHit::terminate()` runs:

- A `WebFormHit` row is created, capturing `ip_address`, `user_agent`, `web_form_identifier`,
  `web_form_type`, `product_id`, `enrollment_id`, `response_status`, `duration_ms`, and `session_id`.
- If the request succeeded (`200`) and carries a `session_id`, a `WebFormSession` is created or
  updated (keyed by `session_id`), capturing:
  - `ip_address`
  - `user_agent`
  - `utm` — parsed from the `utm` query parameter above into
    `{"utm_source": "google", "utm_medium": "cpc", "utm_campaign": "summer_promo"}`. If `utm` is
    omitted on a later hit for the same session, the previously captured value is preserved rather
    than being cleared.
  - `started_at`, set the first time the session is seen.
