# Forms Consent Implementation (GDPR)

**Last Updated:** 2026-02-18

Architecture and implementation details for GDPR-compliant HubSpot consent gating on all form and lead-capture flows.

## Form Categories and Legal Basis

| Category | Legal Basis | HubSpot Send | Examples |
|----------|-------------|--------------|----------|
| **Contact request** | Art. 6(1)(b) pre-contractual | Always | Demo, callback, pricing inquiry, webinar, event |
| **Lead gen** | Art. 6(1)(a) consent | Only when hubspotConsent | Template download, calculator export, gated content |

**Contact request forms:** User explicitly requested contact (demo, callback, pricing quote, webinar registration). The act of submission constitutes the request. Cookie consent is not required for this purpose.

**Lead gen forms:** User exchanged data for content (download, export). Sending to HubSpot for marketing/CRM is secondary; respect cookie consent or form-specific consent.

## Form-Specific Consent (Lead Gen)

Lead gen forms may obtain **form-specific consent** via an optional checkbox, separate from the cookie banner. This provides a valid legal basis (Art. 6(1)(a)) even when the user declined cookies.

**Logic:** `hubspotConsent = formCheckboxChecked || loadConsent()?.hubspot`

**Checkbox requirements (GDPR/EDPB):**
- Unchecked by default (pre-ticked invalid per CJEU Planet49)
- Optional (user can submit without checking; content delivered either way)
- Clear label: "Ich stimme zu, dass meine Kontaktdaten an Ordio übermittelt und für Marketingzwecke genutzt werden können."
- ID: `hubspot_form_consent` (for JS to read)
- Placement: Above submit button (before button), minimal styling (`.contact-consent-notice` pattern). Placing before the button improves visibility and opt-in rates; GDPR does not require placement below the button.
- **No animation:** EDPB Guidelines 3/2022 classify pulsing/flashing on consent as "stirring" dark patterns. Use static visibility improvements only. See `docs/systems/gdpr/CONSENT_CHECKBOX_DARK_PATTERNS.md`.

**HTML pattern (place above submit button):**
```html
<label class="contact-consent-notice contact-consent-checkbox contact-consent-visible mb-3">
  <input type="checkbox" id="hubspot_form_consent" name="hubspot_form_consent" value="1">
  <span class="contact-consent-text">Ich stimme zu, dass meine Kontaktdaten an Ordio übermittelt und für Marketingzwecke genutzt werden können.</span>
</label>
<button type="submit">...</button>
```
Use `contact-consent-visible` for GDPR-compliant visibility (readable text, WCAG contrast, subtle Ordio-blue border). No animation.
The `contact-consent-checkbox` class enables a two-column flex layout so the text wraps under itself, not under the checkbox.

**Styling requirements (lead gen forms):**
- **Text alignment:** Consent text must be left-aligned (`text-align: left`), never centered.
- **Checkbox size:** 1.25rem (20px) for consistency with tools pages and better visibility/accessibility.
- **Source:** `src/input.css` (compiled to `dist/output.css`) provides base styling for all pages; `tools-pages.css` and `templates-pages.css` add defensive overrides where loaded.

**Frontend pattern:**
```javascript
const formConsent = document.getElementById('hubspot_form_consent')?.checked === true;
const cookieConsent = (typeof loadConsent === 'function' && loadConsent()?.hubspot) || false;
const hubspotConsent = formConsent || cookieConsent;
```

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend                                                         │
│  hubspotConsent = formCheckboxChecked || loadConsent()?.hubspot │
│  Pass hubspotConsent in API payload or form field (lead gen)     │
└─────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend APIs                                                     │
│  Form category? → contact_request: Always send to HubSpot        │
│                → lead_gen: hubspotConsent? Yes: Send, No: Skip    │
│  Core delivery (redirect, file, success) always works            │
└─────────────────────────────────────────────────────────────────┘
```

## API Contract

**Lead gen APIs** accept optional `hubspotConsent` (boolean or string `"1"`/`"true"`):

- **JSON APIs:** `hubspotConsent` in request body
- **Form POST (form-hs, formtype=content):** `hubspot_consent` field (`"1"` = consented)
- **Default when missing:** Treat as false (do not send to HubSpot)

**Contact request APIs** always send to HubSpot; `hubspotConsent` is ignored.

## Consent Notice UX (Contact Request Forms)

Per ICO/EDPB guidance, consent information should be "clear, concise and not unnecessarily disruptive." Contact consent notices use:

- **Placement:** Above the submit button (improves visibility; user sees consent before submitting). Below-button placement is also acceptable; GDPR does not mandate placement.
- **Styling:** Minimal – small grey text (0.6875rem, #9ca3af), no box/background/border
- **Class:** `.contact-consent-notice` (defined in `src/input.css` and lead-capture-popup embedded styles)

## Frontend Pattern

**With form checkbox (lead gen):**
```javascript
const formConsent = document.getElementById('hubspot_form_consent')?.checked === true;
const cookieConsent = (typeof loadConsent === 'function' && loadConsent()?.hubspot) || false;
hubspotConsent: formConsent || cookieConsent
```

**Without form checkbox (fallback):**
```javascript
hubspotConsent: (typeof loadConsent === 'function' && loadConsent()?.hubspot) || false
```

For form-urlencoded POST (e.g. addon-request):

```javascript
const formConsent = document.getElementById('hubspot_form_consent')?.checked === true;
const cookieConsent = (typeof loadConsent === 'function' && loadConsent()?.hubspot) || false;
formData.append('hubspotConsent', (formConsent || cookieConsent) ? '1' : '0');
```

## Backend Pattern

**Contact request (always send):**
```php
if (!shouldSendToHubSpot($input ?? [], FORM_CATEGORY_CONTACT_REQUEST)) {
    return; // Never reached for contact_request
}
// Proceed with HubSpot API call
```

**Lead gen (respect consent):**
```php
if (!shouldSendToHubSpot($input ?? [], FORM_CATEGORY_LEAD_GEN)) {
    // Skip HubSpot, still return success / deliver content
    return;
}
// Proceed with HubSpot API call
```

**form-hs (POST):** Use `shouldSendToHubSpotFromPost($_POST, $formCategory)`. Formtype `content` = lead_gen; demo/sdr/event/webinar = contact_request.

## Affected Endpoints

| Endpoint | Form Category | HubSpot Gate | Core Action (always) |
|----------|---------------|--------------|---------------------|
| collect-lead.php | lead_gen | hubspotConsent | Return success |
| lead-capture.php | contact_request | Always send | Return success |
| submit-template.php | lead_gen | hubspotConsent | Return Excel URL |
| addon-request.php | contact_request | Always send | Return success |
| export-workdays.php | lead_gen | marketingConsent | Return file |
| shiftops-hubspot.php | lead_gen | hubspotConsent | Return success |
| shiftops-nps.php | lead_gen | hubspotConsent | Return success |
| webinar-registration.php | contact_request | Always send | Send confirmation email |
| payroll-webinar-registration.php | contact_request | Always send | Send confirmation email |
| event-lead-capture.php | contact_request | Always send | Return success |
| generate_excel.php | lead_gen | hubspotConsent | Return Excel file |
| form-hs.php | content=lead_gen, demo/sdr/event/webinar=contact_request | By formtype | Redirect/deliver |

## Testing Checklist

**Contact request (demo, lead capture, addon, webinar, event):**
- [ ] Deny cookies → submit → data in HubSpot, redirect/success works
- [ ] Accept cookies → submit → data in HubSpot

**Lead gen (template download, calculator export, gated content):**
- [ ] Accept cookies → submit → data in HubSpot, content delivered
- [ ] Reject cookies → submit → content delivered, no HubSpot
- [ ] Reject cookies, check form checkbox → submit → data in HubSpot, content delivered
- [ ] Reject then "Zustimmung anpassen" accept → submit → data in HubSpot
- [ ] No consent yet (banner open) → submit → content delivered, no HubSpot

**Automated tests:**
- `php v2/scripts/dev-helpers/test-forms-consent.php [base_url]` – Unit + integration tests for consent logic
- `python3 v2/scripts/dev-helpers/audit-forms-consent.py` – Code audit for form category usage
- `python3 v2/scripts/dev-helpers/audit-lead-gen-forms.py` – Lead gen form inventory and checkbox/JS audit

**Manual test:** After deployment: deny cookies, submit demo form → verify HubSpot receives data; submit template download → verify no HubSpot.

## Lead Gen Form Inventory

All lead gen forms include the consent checkbox and JS reads it.

| Form Type | Location | Checkbox ID | Notes |
|-----------|----------|-------------|-------|
| Template download | templates_template.php | hubspot_form_consent | Modal |
| Gated content | include_form-gated-content.php | hubspot_form_consent | form-hs POST |
| ShiftOps unlock | shiftops-report.php | hubspot_form_consent | Modal |
| ShiftOps NPS | shiftops-report.php | hubspot_form_consent | Modal |
| Tool export modals | tools_*.php (9 tools) | hubspot_form_consent | Modal |
| Gated overlays | Stundenlohn, Arbeitstage, Arbeitszeit, TVöD-SuE | hubspot_form_consent or hubspot_form_consent_unlock | See below |

**Gated overlay pattern:** Tools with both export modal and unlock overlay (Stundenlohnrechner, Arbeitstage-Rechner) use `hubspot_form_consent` for export modal and `hubspot_form_consent_unlock` for overlay. Each submit handler reads only its own checkbox.

**Styling:** Base styles in `src/input.css` (text-align: left, checkbox 1.25rem). Tools pages load `tools-pages.css`; templates load `templates-pages.css`. Consent text must never be centered.

## Next Steps (Post-Implementation)

- **Legal review:** Recommend legal sign-off before production; interpretation may vary by jurisdiction.
- **Double opt-in:** Some EU countries (e.g. Germany) recommend double opt-in for marketing. Current implementation is single opt-in; document for legal review.
- **Manual verification:** Deny cookies → check form checkbox → submit template/export → verify HubSpot receives. Deny cookies → do not check → submit → verify no HubSpot, content delivered.
- **A/B test (optional):** Optional checkbox may increase opt-in vs. previous flow; consider A/B test if desired.

## Related Documentation

- `v2/helpers/consent-helpers.php` – Server-side consent helpers
- `docs/systems/gdpr/GDPR_DSGVO_WEBSITE_AUDIT.md` – Website tracking and consent audit
- `docs/systems/gdpr/GDPR_DSGVO_HUBSPOT_AUDIT.md` – HubSpot integration audit
- `.cursor/rules/forms-gdpr-consent.mdc` – Cursor rule for new forms
