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/Segment/SegmentOperatorCatalog.php
<?php

namespace App\Services\Segment;

/**
 * F4.2 — single source of truth for segment operator metadata. Mirrors the
 * scattered static helpers on `App\Model\Segment` (`operators()`,
 * `dateOperators()`, `verificationOperators()`, `tagOperators()`, etc.) but
 * carries richer descriptive metadata used by `segment.explain_operator` +
 * `segment.build_from_nl` heuristics. The legacy model methods stay as the
 * shim the wizard renders dropdowns from; the catalog is the agent-facing
 * presentation layer.
 *
 * Read-only. Static. Zero injection. Adds zero coupling to the legacy model.
 */
class SegmentOperatorCatalog
{
    /**
     * Operator → metadata mapping. `family` matches the entity-list shape in
     * `segment.json` so the UI / AI surface can group operators
     * consistently.
     *
     * @return array<string, array{family:string, description:string, accepts:string, applies_to:list<string>, examples:list<string>}>
     */
    public static function map(): array
    {
        return [
            // Generic equality
            'equal' => [
                'family' => 'generic-equality',
                'description' => 'Exact match. Case-insensitive on text fields, numeric on number-typed fields.',
                'accepts' => 'string|number',
                'applies_to' => ['text', 'number', 'select'],
                'examples' => ["country equal 'Vietnam'", "age equal 21"],
            ],
            'not_equal' => [
                'family' => 'generic-equality',
                'description' => 'Negation of `equal`. Matches subscribers whose value differs (or is NULL).',
                'accepts' => 'string|number',
                'applies_to' => ['text', 'number', 'select'],
                'examples' => ["status not_equal 'unsubscribed'"],
            ],

            // Substring
            'contains' => [
                'family' => 'generic-substring',
                'description' => 'Substring match anywhere in the value. Case-insensitive.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["email contains '@gmail'"],
            ],
            'not_contains' => [
                'family' => 'generic-substring',
                'description' => 'Field does NOT contain the substring (NULL also matches per Acelle convention).',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["bio not_contains 'spam'"],
            ],
            'starts' => [
                'family' => 'generic-substring',
                'description' => 'Value begins with the given prefix. Case-insensitive.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["first_name starts 'Mar'"],
            ],
            'starts_with' => [
                'family' => 'generic-substring',
                'description' => 'Synonym for `starts`. Some Acelle versions emit either form.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["first_name starts_with 'Mar'"],
            ],
            'ends' => [
                'family' => 'generic-substring',
                'description' => 'Value ends with the given suffix. Case-insensitive.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["email ends '@gmail.com'"],
            ],
            'ends_with' => [
                'family' => 'generic-substring',
                'description' => 'Synonym for `ends`. Some Acelle versions emit either form.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["email ends_with '@gmail.com'"],
            ],
            'not_starts' => [
                'family' => 'generic-substring',
                'description' => 'Value does NOT begin with the given prefix.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["email not_starts 'no-reply@'"],
            ],
            'not_ends' => [
                'family' => 'generic-substring',
                'description' => 'Value does NOT end with the given suffix.',
                'accepts' => 'string',
                'applies_to' => ['text'],
                'examples' => ["email not_ends '@example.com'"],
            ],

            // Comparison
            'greater' => [
                'family' => 'generic-comparison',
                'description' => 'Numeric or lexicographic greater-than. Number-typed fields cast to SIGNED INT.',
                'accepts' => 'number|string',
                'applies_to' => ['number', 'text'],
                'examples' => ["age greater 21"],
            ],
            'greater_than' => [
                'family' => 'generic-comparison',
                'description' => 'Synonym for `greater`.',
                'accepts' => 'number|string',
                'applies_to' => ['number', 'text'],
                'examples' => ["score greater_than 80"],
            ],
            'less' => [
                'family' => 'generic-comparison',
                'description' => 'Numeric or lexicographic less-than. Number-typed fields cast to SIGNED INT.',
                'accepts' => 'number|string',
                'applies_to' => ['number', 'text'],
                'examples' => ["age less 18"],
            ],
            'less_than' => [
                'family' => 'generic-comparison',
                'description' => 'Synonym for `less`.',
                'accepts' => 'number|string',
                'applies_to' => ['number', 'text'],
                'examples' => ["score less_than 50"],
            ],

            // Blank
            'blank' => [
                'family' => 'generic-blank',
                'description' => 'Field is empty or NULL.',
                'accepts' => 'none',
                'applies_to' => ['text', 'number', 'select'],
                'examples' => ["first_name blank"],
            ],
            'not_blank' => [
                'family' => 'generic-blank',
                'description' => 'Field has a non-empty value.',
                'accepts' => 'none',
                'applies_to' => ['text', 'number', 'select'],
                'examples' => ["industry not_blank"],
            ],

            // Engagement — open
            'last_open_email_less_than_days' => [
                'family' => 'engagement-open',
                'description' => 'Subscriber\'s most recent email open is within the past N days. Joins `tracking_logs` + `open_logs`.',
                'accepts' => 'integer (day count)',
                'applies_to' => ['last_open_email'],
                'examples' => ["last_open_email less_than_days 7", "last_open_email less_than_days 30"],
            ],
            'last_open_email_greater_than_days' => [
                'family' => 'engagement-open',
                'description' => 'Subscriber\'s most recent email open is OLDER than N days, OR they have never opened. Useful for dormant detection.',
                'accepts' => 'integer (day count)',
                'applies_to' => ['last_open_email'],
                'examples' => ["last_open_email greater_than_days 90"],
            ],

            // Engagement — click
            'last_link_click_less_than_days' => [
                'family' => 'engagement-click',
                'description' => 'Subscriber\'s most recent click is within the past N days. Joins `tracking_logs` + `click_logs`.',
                'accepts' => 'integer (day count)',
                'applies_to' => ['last_link_click'],
                'examples' => ["last_link_click less_than_days 14"],
            ],
            'last_link_click_greater_than_days' => [
                'family' => 'engagement-click',
                'description' => 'Subscriber\'s most recent click is OLDER than N days OR they have never clicked.',
                'accepts' => 'integer (day count)',
                'applies_to' => ['last_link_click'],
                'examples' => ["last_link_click greater_than_days 60"],
            ],

            // Tag membership
            'tag_contains' => [
                'family' => 'tag-membership',
                'description' => 'Subscriber has the named tag (case-sensitive match against `subscribers.tags` JSON column).',
                'accepts' => 'string (tag name)',
                'applies_to' => ['tags'],
                'examples' => ["tag_contains 'VIP'"],
            ],
            'tag_not_contains' => [
                'family' => 'tag-membership',
                'description' => 'Subscriber does NOT have the named tag.',
                'accepts' => 'string (tag name)',
                'applies_to' => ['tags'],
                'examples' => ["tag_not_contains 'churned'"],
            ],

            // Verification
            'verification_equal' => [
                'family' => 'verification-status',
                'description' => 'Email Verification Status (EVS plugin) equals the given value.',
                'accepts' => 'string (verified|unverified|disposable|risky|unknown)',
                'applies_to' => ['verification_status'],
                'examples' => ["verification_equal 'verified'"],
            ],
            'verification_not_equal' => [
                'family' => 'verification-status',
                'description' => 'Email Verification Status differs from the given value (NULL also matches).',
                'accepts' => 'string',
                'applies_to' => ['verification_status'],
                'examples' => ["verification_not_equal 'disposable'"],
            ],

            // Created date
            'created_date_greater' => [
                'family' => 'created-date',
                'description' => 'Subscriber\'s sign-up timestamp is AFTER the given datetime.',
                'accepts' => 'datetime (Y-m-d H:i:s)',
                'applies_to' => ['created_at'],
                'examples' => ["created_date_greater '2026-01-01 00:00:00'"],
            ],
            'created_date_less' => [
                'family' => 'created-date',
                'description' => 'Subscriber\'s sign-up timestamp is BEFORE the given datetime.',
                'accepts' => 'datetime (Y-m-d H:i:s)',
                'applies_to' => ['created_at'],
                'examples' => ["created_date_less '2026-04-01 00:00:00'"],
            ],
            'created_date_last_x_days' => [
                'family' => 'created-date',
                'description' => 'Subscriber signed up within the last N days. Convenience operator — Acelle internally translates to `UNIX_TIMESTAMP(created_at) >= now() - N*86400`.',
                'accepts' => 'integer (day count)',
                'applies_to' => ['created_at'],
                'examples' => ["created_date_last_x_days 14"],
            ],
        ];
    }

    /**
     * Look up a single operator's metadata. Returns null when unknown — caller
     * decides whether to throw or return a graceful fallback.
     *
     * @return array{family:string, description:string, accepts:string, applies_to:list<string>, examples:list<string>}|null
     */
    public static function get(string $operatorCode): ?array
    {
        $map = self::map();

        return $map[$operatorCode] ?? null;
    }

    /**
     * All operator codes the catalog knows about. Used by the
     * `segment.explain_operator` enum hint + Pest reflection tests.
     *
     * @return list<string>
     */
    public static function knownOperators(): array
    {
        return array_keys(self::map());
    }

    /**
     * Returns the family bucket every operator code belongs to. Mirrors the
     * `entities.operator_families[].id` keys in `segment.json`.
     *
     * @return list<string>
     */
    public static function knownFamilies(): array
    {
        return [
            'generic-equality',
            'generic-substring',
            'generic-comparison',
            'generic-blank',
            'engagement-open',
            'engagement-click',
            'tag-membership',
            'verification-status',
            'created-date',
        ];
    }
}