HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/app/Services/Form/FormFieldCatalog.php
<?php

namespace App\Services\Form;

/**
 * F4.4 — Static catalog of field-recommendation presets per signup intent.
 *
 * Runtime SoT for `form.suggest_fields`. Mirrors `form.json` →
 * `entities.field_recommendations[]`; the F38 Pest test asserts every
 * preset id here appears in the KB and vice-versa (D80 dual-source
 * dual-source pattern: PHP catalog drives runtime, KB mirrors prompt-side).
 */
class FormFieldCatalog
{
    /**
     * @return list<array{
     *     id:string,
     *     label:string,
     *     summary:string,
     *     recommended_fields:list<array{tag:string,label:string,type:string,required:bool,rationale:string}>,
     *     optional_fields:list<array{tag:string,label:string,type:string,required:bool,rationale:string}>,
     *     avoid_fields:list<string>,
     *     max_visible_fields:int
     * }>
     */
    public static function presets(): array
    {
        return [
            [
                'id' => 'newsletter',
                'label' => 'Newsletter signup',
                'summary' => 'Recurring marketing emails. Conversion is the goal; fewer fields = better. Email-only is a valid baseline.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Email', 'type' => 'email', 'required' => true, 'rationale' => 'Required for any signup. Every list has exactly one email field.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'First name', 'type' => 'text', 'required' => false, 'rationale' => 'Worth collecting only if you personalise email greetings. Optional, not required.'],
                ],
                'optional_fields' => [
                    ['tag' => 'INTERESTS', 'label' => 'Topics of interest', 'type' => 'select', 'required' => false, 'rationale' => 'Use only if you actually segment by interest. Otherwise it\'s overcollection.'],
                ],
                'avoid_fields' => ['PHONE', 'ADDRESS', 'COMPANY', 'LAST_NAME', 'JOB_TITLE', 'BIRTHDAY'],
                'max_visible_fields' => 3,
            ],
            [
                'id' => 'lead_gen',
                'label' => 'Lead generation (general)',
                'summary' => 'Sales follow-up. More fields = better lead quality, but each field cuts conversion. Aim for 3 visible fields.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Work email', 'type' => 'email', 'required' => true, 'rationale' => 'Work email signals a real lead. Generic free providers (gmail, hotmail) self-segment as lower-priority.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'First name', 'type' => 'text', 'required' => true, 'rationale' => 'Sales outreach needs a name to address. Required.'],
                    ['tag' => 'COMPANY', 'label' => 'Company', 'type' => 'text', 'required' => true, 'rationale' => 'Lets sales pre-qualify before outreach. Required for B2B.'],
                ],
                'optional_fields' => [
                    ['tag' => 'PHONE', 'label' => 'Phone (optional)', 'type' => 'text', 'required' => false, 'rationale' => 'Only ask if your sales team actually calls — otherwise it\'s friction without payoff.'],
                    ['tag' => 'JOB_TITLE', 'label' => 'Role / title', 'type' => 'text', 'required' => false, 'rationale' => 'Useful for ICP scoring. Keep optional.'],
                ],
                'avoid_fields' => ['ADDRESS', 'BIRTHDAY', 'GENDER'],
                'max_visible_fields' => 5,
            ],
            [
                'id' => 'b2b_demo',
                'label' => 'B2B demo / discovery call',
                'summary' => 'Higher-intent signup. Buyers expect to fill more fields. 4-5 fields is normal here.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Work email', 'type' => 'email', 'required' => true, 'rationale' => 'Work email gates self-qualification. Add a soft warning for free-provider domains.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'First name', 'type' => 'text', 'required' => true, 'rationale' => 'Required for sales follow-up.'],
                    ['tag' => 'COMPANY', 'label' => 'Company', 'type' => 'text', 'required' => true, 'rationale' => 'Required — drives ICP fit + lead routing.'],
                    ['tag' => 'COMPANY_SIZE', 'label' => 'Company size', 'type' => 'select', 'required' => true, 'rationale' => 'Select with 5-7 brackets (1-10 / 11-50 / 51-200 / 201-1000 / 1001+). Drives sales tier routing.'],
                ],
                'optional_fields' => [
                    ['tag' => 'JOB_TITLE', 'label' => 'Role / title', 'type' => 'text', 'required' => false, 'rationale' => 'Useful for ICP. Keep optional — buyers don\'t all want to share.'],
                    ['tag' => 'USE_CASE', 'label' => 'What are you evaluating us for?', 'type' => 'textarea', 'required' => false, 'rationale' => 'Optional textarea — high-intent buyers fill it; low-intent skip. Free signal.'],
                ],
                'avoid_fields' => ['PHONE', 'ADDRESS', 'BIRTHDAY'],
                'max_visible_fields' => 6,
            ],
            [
                'id' => 'event_rsvp',
                'label' => 'Event RSVP (one-time event)',
                'summary' => 'Single-event signup. Collect what the event actually needs (attendance count, dietary, etc.). After the event, archive or convert to newsletter via tag.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Email', 'type' => 'email', 'required' => true, 'rationale' => 'Required. Use for the calendar invite + reminders.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'Full name', 'type' => 'text', 'required' => true, 'rationale' => 'Required for the attendee badge / calendar invite.'],
                    ['tag' => 'ATTENDEES', 'label' => 'Number of attendees', 'type' => 'number', 'required' => true, 'rationale' => 'Required for venue capacity planning.'],
                ],
                'optional_fields' => [
                    ['tag' => 'DIETARY', 'label' => 'Dietary requirements', 'type' => 'text', 'required' => false, 'rationale' => 'Only if your event includes food. Keep optional.'],
                    ['tag' => 'COMPANY', 'label' => 'Company (optional)', 'type' => 'text', 'required' => false, 'rationale' => 'Useful for B2B events; skip for consumer events.'],
                ],
                'avoid_fields' => ['PHONE', 'BIRTHDAY', 'ADDRESS'],
                'max_visible_fields' => 5,
            ],
            [
                'id' => 'webinar',
                'label' => 'Webinar registration',
                'summary' => 'Pre-event signup with reminder + replay-link follow-up. Slightly more fields than a newsletter — buyers want context but expect a quick form.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Work email', 'type' => 'email', 'required' => true, 'rationale' => 'Required — replay link + reminder go here.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'First name', 'type' => 'text', 'required' => true, 'rationale' => 'Required for personalised reminder.'],
                ],
                'optional_fields' => [
                    ['tag' => 'COMPANY', 'label' => 'Company (optional)', 'type' => 'text', 'required' => false, 'rationale' => 'Useful for B2B webinars; skip for consumer.'],
                    ['tag' => 'JOB_TITLE', 'label' => 'Role / title (optional)', 'type' => 'text', 'required' => false, 'rationale' => 'Helps the host tailor the live Q&A. Optional.'],
                ],
                'avoid_fields' => ['PHONE', 'ADDRESS', 'BIRTHDAY'],
                'max_visible_fields' => 4,
            ],
            [
                'id' => 'community',
                'label' => 'Community / Slack invite',
                'summary' => 'Signup for a chat community. Identity matters more than commerce — first name + email is enough.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Email', 'type' => 'email', 'required' => true, 'rationale' => 'Required. Used for invite delivery.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'First name', 'type' => 'text', 'required' => true, 'rationale' => 'Required — community profiles need a name.'],
                ],
                'optional_fields' => [
                    ['tag' => 'INTERESTS', 'label' => 'What brings you here?', 'type' => 'textarea', 'required' => false, 'rationale' => 'Optional textarea — moderators use it to route the invite to the right channel.'],
                ],
                'avoid_fields' => ['PHONE', 'COMPANY', 'ADDRESS', 'BIRTHDAY', 'JOB_TITLE'],
                'max_visible_fields' => 3,
            ],
            [
                'id' => 'general',
                'label' => 'General signup (default)',
                'summary' => 'Catch-all for forms whose intent isn\'t yet clear. Mirrors the newsletter preset but flags the unclear intent.',
                'recommended_fields' => [
                    ['tag' => 'EMAIL', 'label' => 'Email', 'type' => 'email', 'required' => true, 'rationale' => 'Required for any signup.'],
                    ['tag' => 'FIRST_NAME', 'label' => 'First name', 'type' => 'text', 'required' => false, 'rationale' => 'Optional. Only collect if you\'ll use it.'],
                ],
                'optional_fields' => [],
                'avoid_fields' => ['PHONE', 'ADDRESS', 'BIRTHDAY'],
                'max_visible_fields' => 3,
            ],
        ];
    }

    /**
     * @return array{
     *     id:string,label:string,summary:string,
     *     recommended_fields:list<array<string,mixed>>,
     *     optional_fields:list<array<string,mixed>>,
     *     avoid_fields:list<string>,
     *     max_visible_fields:int
     * }|null
     */
    public static function preset(string $id): ?array
    {
        foreach (self::presets() as $preset) {
            if ($preset['id'] === $id) {
                return $preset;
            }
        }

        return null;
    }

    /**
     * @return list<string>
     */
    public static function presetIds(): array
    {
        return array_map(fn (array $p) => $p['id'], self::presets());
    }
}