# Implementation Plan: CRM Automation Engine & AI Features

## Problem Statement

The CRM currently lacks workflow automation and AI capabilities. This plan covers:

1. **Automation Engine** — A configurable hybrid automation system with pre-built workflow templates AND admin-created custom workflows via API
2. **AI Features** — A provider-agnostic AI layer for lead scoring, smart suggestions, natural language queries, and content generation

---

## Requirements

### Automation Engine
- Hybrid approach: pre-built system templates + admin-created custom workflows via API
- Admins can create workflows by selecting trigger events, defining conditions, and choosing actions — no code needed
- 5 initial system templates: auto-assign leads (round-robin), auto-escalate stale leads, meeting reminders, auto-close stale raw leads, high-value sale notification
- All workflows respect existing multi-tenancy (`account_id` scoping) and permission hierarchy
- Async execution via queue (doesn't slow API responses)
- Full execution logging for observability

### AI Features
- Provider-agnostic abstraction (OpenAI, Anthropic, Gemini, local models swappable via config)
- Lead scoring using full historical context: attributes + behavioral signals + sales outcomes
- Natural language queries scoped to user's accessible data
- Smart suggestions (next-best-action recommendations)
- Content generation (follow-up messages, emails, quotation descriptions, meeting summaries)
- Lead enrichment via AI web search + pluggable third-party provider abstraction
  - On-demand (user triggers, reviews, decides what to apply)
  - Smart delta caching (only re-fetches stale/missing data categories)
  - Search history preserved for every lead
  - Privacy-safe: only email domain + country code sent externally, never full PII
- Per-account feature toggles and rate limiting
- Usage tracking with cost estimation

---

## Background & Current State

| Aspect | Current State |
|--------|--------------|
| Event System | No events, listeners, or observers — business logic is procedural in controllers |
| Workflow Engine | None — no state machine or automation pattern |
| Queue/Jobs | Solid — 13 jobs, database driver, scheduled tasks in `routes/console.php` |
| Permission System | Spatie RBAC with `PermissionCatalog` — dot-notation `module.sub_module.action` |
| Data for AI | Rich — leads with status pipeline (raw->verified->client), follow-ups, meetings, demos, sales with payment tracking, ownership histories, activity logs |
| Multi-tenancy | Account-based via `AccountValidation` middleware |
| Scale | 76 controllers, 123 models, 32 services, 130 migrations |

### Key Patterns to Follow
- `ExecuteCampaignJob` — chunked queue processing with retry and error handling
- `SendsPushNotifications` trait — notification dispatch
- `PermissionCatalog` — single source of truth for permission names
- `RecordsActivity` trait — audit logging
- `ResolvesAccessibleUserIds` trait — hierarchy-based data access scoping

---

## Architecture Overview

```
Phase 1: Automation Engine          Phase 2: AI Features
========================           ======================

┌─────────────────────┐            ┌──────────────────────┐
│   Event System      │            │  AI Service Layer    │
│  (CrmEvent base +   │            │  (Provider-agnostic) │
│   domain events)    │            │                      │
└────────┬────────────┘            │  ┌────────────────┐  │
         │                         │  │ OpenAI Provider│  │
         ▼                         │  ├────────────────┤  │
┌─────────────────────┐            │  │Anthropic Prov. │  │
│  Event Listener     │            │  ├────────────────┤  │
│  (dispatches job)   │            │  │ Gemini Provider│  │
└────────┬────────────┘            │  ├────────────────┤  │
         │                         │  │ Local Provider │  │
         ▼                         │  └────────────────┘  │
┌─────────────────────┐            └──────────┬───────────┘
│  Automation Engine  │                       │
│  Service            │                       ▼
│  ┌───────────────┐  │            ┌──────────────────────┐
│  │Condition Eval.│  │            │   AI Features        │
│  ├───────────────┤  │            │  ┌────────────────┐  │
│  │Action Executor│  │            │  │ Lead Scoring   │  │
│  └───────────────┘  │            │  ├────────────────┤  │
└────────┬────────────┘            │  │ NL Queries     │  │
         │                         │  ├────────────────┤  │
         ▼                         │  │ Suggestions    │  │
┌─────────────────────┐            │  ├────────────────┤  │
│  Workflow DB        │            │  │ Content Gen    │  │
│  (definitions +     │            │  └────────────────┘  │
│   execution logs)   │            └──────────────────────┘
└─────────────────────┘
```

---

## Phase 1: Automation Engine

### Task 1: Event System Foundation

**Objective:** Introduce Laravel Events as the foundation for automation triggers, without breaking existing controller logic.

**Implementation:**

- Create a base `CrmEvent` abstract class in `app/Events/` with:
  - Common properties: `account_id`, `user_id`, `timestamp`, `model_type`, `model_id`, model instance
  - Static `eventName(): string` method returning human-readable identifier (e.g., `lead.created`)
  - Implements `ShouldDispatchAfterCommit` to avoid firing on rolled-back transactions

- Create domain events extending `CrmEvent`:
  - `LeadCreated`, `LeadStatusChanged`, `LeadAssigned`
  - `FollowUpCreated`, `FollowUpOverdue` (synthetic, for scheduled checks)
  - `MeetingScheduled`, `MeetingCompleted`, `MeetingCancelled`, `MeetingUpcoming` (synthetic)
  - `SaleCreated`, `SaleApproved`, `SalePaymentReceived`
  - `TaskCreated`, `TaskCompleted`
  - `LeadStale` (synthetic, for scheduled checks)

- Each event carries: model instance + actor (User) + metadata array

- Dispatch events from existing controller methods using `event()` helper — at the END of methods, after the response is prepared

- Create `EventCatalog` support class (similar to `PermissionCatalog`) that lists all available trigger events with:
  - Event name (e.g., `lead.created`)
  - Description (e.g., "Fires when a new lead is created")
  - Available model fields for conditions (e.g., `source`, `priority`, `status`, `company.name`)
  - This powers the admin UI for custom workflow creation

**Files to create:**
- `app/Events/CrmEvent.php`
- `app/Events/Automation/LeadCreated.php`
- `app/Events/Automation/LeadStatusChanged.php`
- `app/Events/Automation/LeadAssigned.php`
- `app/Events/Automation/FollowUpCreated.php`
- `app/Events/Automation/FollowUpOverdue.php`
- `app/Events/Automation/MeetingScheduled.php`
- `app/Events/Automation/MeetingCompleted.php`
- `app/Events/Automation/MeetingCancelled.php`
- `app/Events/Automation/MeetingUpcoming.php`
- `app/Events/Automation/SaleCreated.php`
- `app/Events/Automation/SaleApproved.php`
- `app/Events/Automation/SalePaymentReceived.php`
- `app/Events/Automation/TaskCreated.php`
- `app/Events/Automation/TaskCompleted.php`
- `app/Events/Automation/LeadStale.php`
- `app/Support/EventCatalog.php`

**Files to modify (add event dispatch):**
- `app/Http/Controllers/LeadController.php` — dispatch `LeadCreated`, `LeadStatusChanged`, `LeadAssigned`
- `app/Http/Controllers/LeadSaleController.php` — dispatch `SaleCreated`
- `app/Http/Controllers/MeetingController.php` — dispatch `MeetingScheduled`, `MeetingCompleted`, `MeetingCancelled`
- `app/Http/Controllers/TaskController.php` — dispatch `TaskCreated`, `TaskCompleted`
- `app/Http/Controllers/FinanceController.php` — dispatch `SaleApproved`, `SalePaymentReceived`

**Tests:**
- Events dispatched on corresponding actions (`Event::fake()`)
- Event payloads contain expected data (account_id, model, actor)
- Existing controller behavior completely unchanged (no regressions)
- Events don't fire on rolled-back transactions
- `EventCatalog::all()` returns all events with correct metadata

**Demo:** Create a lead via API -> verify `LeadCreated` event fires. Call `EventCatalog::all()` -> see all available trigger events.

---

### Task 2: Workflow Engine Core — Schema & Models

**Objective:** Build the automation engine's data layer.

**Migrations to create:**

```php
// automation_workflows
Schema::create('automation_workflows', function (Blueprint $table) {
    $table->id();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->text('description')->nullable();
    $table->string('trigger_event'); // matches EventCatalog event name
    $table->boolean('is_active')->default(true);
    $table->boolean('is_system')->default(false); // true for pre-built templates
    $table->json('config')->nullable(); // template-specific settings
    $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
    $table->timestamps();

    $table->index(['account_id', 'trigger_event', 'is_active']);
});

// automation_conditions
Schema::create('automation_conditions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('workflow_id')->constrained('automation_workflows')->cascadeOnDelete();
    $table->string('field'); // supports dot notation e.g. "company.credit_limit"
    $table->enum('operator', [
        'equals', 'not_equals',
        'greater_than', 'less_than',
        'greater_than_or_equal', 'less_than_or_equal',
        'contains', 'not_contains',
        'in', 'not_in',
        'is_null', 'is_not_null',
        'starts_with', 'ends_with'
    ]);
    $table->json('value')->nullable();
    $table->enum('logical_operator', ['AND', 'OR'])->default('AND');
    $table->unsignedInteger('order')->default(0);
    $table->timestamps();
});

// automation_actions
Schema::create('automation_actions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('workflow_id')->constrained('automation_workflows')->cascadeOnDelete();
    $table->enum('action_type', [
        'assign_lead', 'send_push', 'send_sms',
        'update_field', 'create_task', 'create_follow_up',
        'send_notification', 'webhook'
    ]);
    $table->json('action_config');
    $table->unsignedInteger('order')->default(0);
    $table->unsignedInteger('delay_minutes')->default(0);
    $table->timestamps();
});

// automation_execution_logs
Schema::create('automation_execution_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('workflow_id')->constrained('automation_workflows')->cascadeOnDelete();
    $table->string('trigger_event');
    $table->string('model_type');
    $table->unsignedBigInteger('model_id');
    $table->enum('status', ['success', 'failed', 'skipped']);
    $table->dateTime('executed_at');
    $table->text('error_message')->nullable();
    $table->unsignedInteger('execution_time_ms')->default(0);
    $table->json('actions_executed')->nullable(); // per-action results
    $table->timestamps();

    $table->index(['workflow_id', 'executed_at']);
    $table->index(['model_type', 'model_id']);
});

// automation_states (for round-robin tracking, reminder dedup, etc.)
Schema::create('automation_states', function (Blueprint $table) {
    $table->id();
    $table->foreignId('workflow_id')->constrained('automation_workflows')->cascadeOnDelete();
    $table->string('key');
    $table->json('value');
    $table->timestamp('updated_at');

    $table->unique(['workflow_id', 'key']);
});
```

**Models to create:**
- `app/Models/AutomationWorkflow.php` — with scopes: `active()`, `forEvent()`, `forAccount()`, `system()`, `custom()`
- `app/Models/AutomationCondition.php`
- `app/Models/AutomationAction.php`
- `app/Models/AutomationExecutionLog.php`
- `app/Models/AutomationState.php`

**Tests:**
- Migrations run and rollback cleanly
- Model relationships work (workflow hasMany conditions, actions, logs)
- Scopes filter correctly
- JSON columns store/retrieve complex configs
- Cascade delete removes children

**Demo:** Seed a workflow in Tinker and query back the full structure.

---

### Task 3: Workflow Engine Core — Execution Engine

**Objective:** Build the engine that evaluates conditions and executes actions when events fire.

**Files to create:**

- `app/Services/Automation/AutomationEngineService.php` — main orchestrator
  - `processEvent(CrmEvent $event): void`
  - Queries matching active workflows for the event and account
  - Evaluates conditions, executes actions, logs results

- `app/Services/Automation/ConditionEvaluator.php`
  - `evaluate(AutomationWorkflow $workflow, Model $model): bool`
  - Supports all operators against model attributes
  - Supports dot-notation for related fields (e.g., `company.credit_limit`)
  - AND/OR grouping logic
  - Empty conditions = always true

- `app/Services/Automation/ActionExecutor.php`
  - `execute(AutomationAction $action, Model $model, User $actor): ActionResult`
  - Action handlers per type:
    - `assign_lead` — update `assigned_to`, fire `LeadAssigned`
    - `send_push` — use `PushNotificationService`
    - `send_notification` — use `PushNotificationService`
    - `update_field` — update model field
    - `create_task` — create Task linked to lead
    - `create_follow_up` — create LeadFollowup
    - `send_sms` — use existing SMS logic
    - `webhook` — HTTP POST to URL with event payload
  - Supports **template variables** in action_config: `{{lead.name}}`, `{{actor.name}}`, `{{model.field}}`
  - Returns `ActionResult` DTO (success/failure + message)

- `app/Services/Automation/TemplateVariableResolver.php`
  - Resolves `{{variable}}` placeholders in action config using model data

- `app/DTOs/ActionResult.php`

- `app/Listeners/ProcessAutomationEventListener.php`
  - Listens to `CrmEvent` base class
  - Dispatches `ProcessAutomationJob` (queued, async)

- `app/Jobs/ProcessAutomationJob.php`
  - Calls `AutomationEngineService::processEvent()`

- `app/Jobs/ExecuteDelayedActionJob.php`
  - For actions with `delay_minutes > 0`

**Tests:**
- Condition evaluation for each operator
- Dot-notation field access
- Empty conditions = always matches
- AND/OR grouping
- Action execution for each type (mock services)
- Only active workflows processed
- Account scoping (no cross-account execution)
- Both system and custom workflows fire
- Delayed actions dispatch correctly
- One action failure doesn't block others
- Execution logging with per-action results
- Template variable resolution

**Demo:** Create workflow "when lead.created and source = 'website', send push to creator". Create matching lead -> queue processes -> notification sent + log recorded.

---

### Task 4: Pre-built Template — Auto-assign Leads (Round Robin)

**Objective:** Distribute new leads across a team in round-robin fashion.

**Implementation:**

- Seed workflow via `AutomationSeeder`:
  - `is_system = true`, trigger = `lead.created`
  - Config: `{ "user_pool": [], "strategy": "round_robin" }`

- Create `app/Services/Automation/Strategies/LeadAssignmentStrategy.php`:
  - `roundRobin(array $userIds, int $workflowId): int`
  - Uses `automation_states` to track last index
  - Skips inactive users

- Action handler for `assign_lead`:
  - Updates `Lead::assigned_to`
  - Dispatches `LeadAssigned` event

- If user_pool empty or all inactive → log "skipped", leave unassigned

**Files to create:**
- `app/Services/Automation/Strategies/LeadAssignmentStrategy.php`
- `database/seeders/AutomationSeeder.php`

**Tests:**
- Round-robin distributes evenly (3 users, 9 leads → 3-3-3)
- Inactive users skipped
- Empty pool → skipped log
- Disabled workflow → no assignment
- Manual lead creation unaffected when disabled

**Demo:** Enable with 3 users. Create 6 leads → distributed 2-2-2.

---

### Task 5: Pre-built Template — Auto-escalate Stale Leads

**Objective:** Escalate leads without follow-up activity beyond a configurable threshold.

**Implementation:**

- Create `app/Jobs/CheckStaleLeadsJob.php` (scheduled hourly):
  - Per account: find verified leads where last follow-up > threshold hours
  - For each stale lead, fire synthetic `FollowUpOverdue` event
  - `AutomationEngineService` picks up matching workflows

- Seed system workflow:
  - Trigger: `follow_up.overdue`
  - Config: `{ "threshold_hours": 24, "notify_supervisor": true, "update_priority": true }`
  - Actions: send push to assignee + supervisors, update priority to "high"

- Add to `routes/console.php`: `Schedule::job(CheckStaleLeadsJob::class)->hourly()`

**Tests:**
- Recent follow-ups → not flagged
- Exceeds threshold → flagged
- Notifications reach assignee + supervisor
- Priority updated when configured
- Threshold is configurable (24h vs 48h)
- Only verified leads checked

**Demo:** Verified lead with no follow-up for 25+ hours. Run job → priority = high, assignee notified.

---

### Task 6: Pre-built Templates — Meeting Reminder, Auto-close, High-value Sale

**Objective:** Implement the remaining three system templates.

#### Template: Meeting Reminder

- Create `app/Jobs/SendAutomatedMeetingRemindersJob.php` (every 15 minutes)
- Finds meetings starting within configured window (default 60 min) not yet reminded
- Uses `automation_states` to track reminded meeting IDs
- Config: `{ "reminder_minutes": 60 }`
- Fires synthetic `MeetingUpcoming` event
- Action: push notification to meeting creator

#### Template: Auto-close Stale Raw Leads

- Create `app/Jobs/CloseStaleRawLeadsJob.php` (daily at 02:30)
- Raw leads with zero activity (no follow-ups, meetings, callbacks) for X days
- Config: `{ "staleness_days": 30 }`
- Fires synthetic `LeadStale` event
- Actions: set `is_deleted = 1`, notify creator

#### Template: High-value Sale Notification

- Trigger: `sale.created` (standard event path)
- Condition: `total >= threshold` (via ConditionEvaluator)
- Config: `{ "threshold_amount": 100000 }`
- Action: push notification to users with `report_management.sales.override`

**Files to create:**
- `app/Jobs/SendAutomatedMeetingRemindersJob.php`
- `app/Jobs/CloseStaleRawLeadsJob.php`

**Tests:**
- Meeting reminder: correct timing, no duplicates, respects window config
- Auto-close: only raw leads, zero activity, respects threshold, notifies creator
- High-value sale: only above threshold, notifies override-permission users
- Each independently enable/disable

---

### Task 7: Automation Admin API — Full CRUD with Custom Workflow Creation

**Objective:** API endpoints for managing workflows — configuring system templates AND creating custom workflows.

**Endpoints:**

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/automation/workflows/fetch` | POST | List all workflows (system + custom) |
| `/api/automation/workflow/fetch` | POST | Single workflow with conditions, actions, recent logs |
| `/api/automation/workflow/create` | POST | **Create custom workflow** |
| `/api/automation/workflow/update` | POST | Update workflow (custom: full edit; system: config only) |
| `/api/automation/workflow/delete` | POST | Delete custom workflow (system = 403) |
| `/api/automation/workflow/toggle` | POST | Enable/disable any workflow |
| `/api/automation/workflow/duplicate` | POST | Clone as new custom workflow |
| `/api/automation/execution-logs/fetch` | POST | Paginated logs with filters |
| `/api/automation/triggers/fetch` | POST | Available trigger events from EventCatalog |
| `/api/automation/actions/fetch` | POST | Available action types with config schemas |

**Custom workflow creation payload:**

```json
{
    "name": "Notify manager on high-priority lead reassignment",
    "description": "When a high-priority lead is reassigned, notify the new assignee's manager",
    "trigger_event": "lead.assigned",
    "conditions": [
        {
            "field": "priority",
            "operator": "equals",
            "value": "high",
            "logical_operator": "AND"
        }
    ],
    "actions": [
        {
            "action_type": "send_push",
            "action_config": {
                "to": "supervisor",
                "title": "High-priority lead assigned",
                "body": "Lead {{lead.name}} has been assigned to {{assignee.name}}"
            },
            "order": 1,
            "delay_minutes": 0
        },
        {
            "action_type": "create_task",
            "action_config": {
                "name": "Follow up with {{lead.name}}",
                "description": "High-priority lead needs immediate attention",
                "due_hours": 4
            },
            "order": 2,
            "delay_minutes": 0
        }
    ]
}
```

**Template variables available in action_config:**
- `{{lead.name}}`, `{{lead.source}}`, `{{lead.priority}}`, `{{lead.status}}`
- `{{actor.name}}`, `{{actor.email}}`
- `{{assignee.name}}`, `{{assignee.email}}`
- `{{model.<field>}}` — any field on the trigger's model
- `{{company.name}}` — related model fields via dot notation

**Permissions (add to PermissionCatalog):**
- `setting.automation.create`
- `setting.automation.read`
- `setting.automation.update`
- `setting.automation.delete`
- `setting.automation.override`

**Validation rules:**
- `trigger_event` must exist in `EventCatalog`
- `conditions[].field` must be valid for the trigger's model
- `conditions[].operator` must be valid enum
- `actions[].action_type` must be valid enum
- `actions[].action_config` validated per action type schema

**Files to create:**
- `app/Http/Controllers/AutomationController.php`
- `app/Http/Requests/Automation/CreateWorkflowRequest.php`
- `app/Http/Requests/Automation/UpdateWorkflowRequest.php`

**Tests:**
- Create custom workflow with conditions and actions
- Validation rejects invalid triggers, fields, operators
- Update modifies conditions/actions correctly
- Delete only works on custom (system = 403)
- Toggle works for both types
- Duplicate creates copy as custom
- Execution logs paginate and filter
- Template variable resolution works at execution time
- Permission checks enforced
- Activity log records all mutations
- Custom workflows fire when trigger event occurs

**Demo:** Admin creates: "When sale.created with total > 50000, create task for assignee to send thank-you within 24h." Create qualifying sale → task auto-created.

---

## Phase 2: AI Features

### Task 8: AI Service Abstraction Layer

**Objective:** Provider-agnostic AI service with pre-built adapters for all major providers. Admin can switch provider and update API credentials entirely via the API — no code changes, no .env edits, no redeployment needed.

**Supported Providers (pre-built adapters):**

| Provider | Default Model | Notes |
|----------|---------------|-------|
| OpenAI | `gpt-5.5` | Most popular, best function calling |
| Anthropic | `claude-sonnet-4-6` | Strong reasoning, long context |
| Google Gemini | `gemini-3.7-flash` | Good balance of speed/quality |
| Mistral | `mistral-medium-3-5` | EU-based, good for GDPR |
| Cohere | `command-a-plus-05-2026` | Strong RAG capabilities |
| DeepSeek | `deepseek-v4-flash` | Cost-effective |
| Groq | `openai/gpt-oss-120b` | Ultra-fast inference |
| Local (Ollama/vLLM) | `qwen3.8` | Full privacy, no external calls |

**Model selection is optional and flexible (like tags):**
- Admin only needs to pick a provider + enter API key. A sensible default model is used automatically.
- The system provides **pre-seeded model suggestions** per provider (shown as options in the UI):

  | Provider | Pre-seeded Models | Default |
  |----------|-------------------|---------|
  | OpenAI | `gpt-5.6`, `gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.3-codex` | `gpt-5.5` |
  | Anthropic | `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5` | `claude-sonnet-4-6` |
  | Google Gemini | `gemini-3.7-flash`, `gemini-3.6-flash`, `gemini-3.5-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-flash-lite` | `gemini-3.7-flash` |
  | Mistral | `mistral-medium-3-5`, `mistral-small-2603`, `mistral-large-3`, `ministral-3-14b`, `ministral-3-8b`, `ministral-3-3b` | `mistral-medium-3-5` |
  | Cohere | `command-a-plus-05-2026`, `command-a-03-2025`, `command-a-reasoning-08-2025`, `command-r7b-12-2024` | `command-a-plus-05-2026` |
  | DeepSeek | `deepseek-v4-pro`, `deepseek-v4-flash` | `deepseek-v4-flash` |
  | Groq | `openai/gpt-oss-120b`, `openai/gpt-oss-20b`, `qwen/qwen3.6-27b` | `openai/gpt-oss-120b` |
  | Local (Ollama) | `qwen3.8`, `deepseek-v4-flash`, `kimi-k3`, `nemotron-3.5-lightning`, `muse-glimmer`, `granite4.2`, `gemma4` | `qwen3.8` |

- Admin can also **type any custom model name** (like adding a tag) — the system accepts it and passes it to the provider. If invalid, the provider API error is surfaced.
- Pre-seeded suggestions are stored in the `ai_model_suggestions` table and can be updated without code changes (just insert/update rows via DB or a future admin endpoint).
- This ensures non-technical admins get guidance while power users stay unblocked when new models launch.

**Key Design: Credentials stored in DB (per-account) only**

The admin configures AI credentials through the API. Credentials are stored encrypted in the `ai_settings` table per account. No fallback .env keys — if an account hasn't configured credentials, AI features simply return "AI not configured. Please add your provider credentials in settings."

This means:
- Admin changes provider from OpenAI to Anthropic → immediate, no restart
- Admin updates API key → immediate, no deployment
- Different accounts can use different providers
- No server access needed to manage AI configuration
- Each account manages their own API costs independently

**Files to create:**

- `config/ai.php` (adapter registry + defaults only — NO credentials):
```php
return [
    'default_provider' => 'openai', // suggestion only, account must configure credentials

    // Provider adapter registry (class mappings — NOT credentials)
    'adapters' => [
        'openai' => \App\Services\Ai\Providers\OpenAiProvider::class,
        'anthropic' => \App\Services\Ai\Providers\AnthropicProvider::class,
        'gemini' => \App\Services\Ai\Providers\GeminiProvider::class,
        'mistral' => \App\Services\Ai\Providers\MistralProvider::class,
        'cohere' => \App\Services\Ai\Providers\CohereProvider::class,
        'deepseek' => \App\Services\Ai\Providers\DeepSeekProvider::class,
        'groq' => \App\Services\Ai\Providers\GroqProvider::class,
        'local' => \App\Services\Ai\Providers\LocalProvider::class,
    ],

    // Default models per provider (fallback if no model specified by admin)
    // These should be updated to latest stable models at time of deployment
    'default_models' => [
        'openai' => 'gpt-5.5',
        'anthropic' => 'claude-sonnet-4-6',
        'gemini' => 'gemini-3.7-flash',
        'mistral' => 'mistral-medium-3-5',
        'cohere' => 'command-a-plus-05-2026',
        'deepseek' => 'deepseek-v4-flash',
        'groq' => 'openai/gpt-oss-120b',
        'local' => 'qwen3.8',
    ],

    'rate_limits' => [
        'per_user_per_hour' => 50,
        'per_account_per_day' => 1000,
    ],

    'retry' => [
        'attempts' => 3,
        'delay_ms' => 1000,
        'multiplier' => 2,
    ],
];
```

- `app/Contracts/AiProviderInterface.php`:
```php
interface AiProviderInterface
{
    public function complete(string $prompt, array $options = []): AiResponse;
    public function chat(array $messages, array $options = []): AiResponse;
    public function embed(string|array $text): array;
    public function isConfigured(): bool;
    public function providerName(): string;
    public function defaultModel(): string;
}
```

- `app/Services/Ai/AiService.php` — main service:
  - Resolves active provider from DB settings (falls back to config/env)
  - Handles retry with exponential backoff
  - Rate limiting per user and per account
  - Usage logging on every call
  - `getAvailableProviders(): array` — returns all providers with their configured status (for admin UI)

- Provider adapters (all implement `AiProviderInterface`):
  - `app/Services/Ai/Providers/OpenAiProvider.php`
  - `app/Services/Ai/Providers/AnthropicProvider.php`
  - `app/Services/Ai/Providers/GeminiProvider.php`
  - `app/Services/Ai/Providers/MistralProvider.php`
  - `app/Services/Ai/Providers/CohereProvider.php`
  - `app/Services/Ai/Providers/DeepSeekProvider.php`
  - `app/Services/Ai/Providers/GroqProvider.php`
  - `app/Services/Ai/Providers/LocalProvider.php`

- `app/DTOs/AiResponse.php` — content, usage (prompt_tokens, completion_tokens), model, provider, latency_ms

**Migration:**
```php
// ai_usage_logs
Schema::create('ai_usage_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->string('provider');
    $table->string('model');
    $table->unsignedInteger('prompt_tokens')->default(0);
    $table->unsignedInteger('completion_tokens')->default(0);
    $table->decimal('estimated_cost', 10, 6)->default(0);
    $table->string('purpose'); // lead_scoring, nl_query, suggestion, content_generation, enrichment
    $table->json('request_context')->nullable();
    $table->timestamps();

    $table->index(['account_id', 'created_at']);
    $table->index(['purpose', 'created_at']);
});

// ai_model_suggestions (pre-seeded, updatable without code changes)
Schema::create('ai_model_suggestions', function (Blueprint $table) {
    $table->id();
    $table->string('provider'); // openai, anthropic, etc.
    $table->string('model_name'); // gpt-4o, claude-sonnet-4-20250514, etc.
    $table->string('display_name')->nullable(); // "GPT-4o (Latest)" for UI
    $table->boolean('is_default')->default(false); // one default per provider
    $table->unsignedInteger('sort_order')->default(0);
    $table->timestamps();

    $table->unique(['provider', 'model_name']);
    $table->index('provider');
});
```

**DB-stored credentials (in `ai_settings` table from Task 14):**
```json
{
    "provider": "openai",
    "credentials": {
        "openai": { "api_key": "sk-...", "model": "gpt-4o" },
        "anthropic": { "api_key": "sk-ant-...", "model": null }
    },
    "features": { "lead_scoring": true, "nl_queries": true, "suggestions": true, "content_generation": true, "lead_enrichment": true },
    "rate_limits": { "queries_per_hour": 20, "generations_per_hour": 20, "enrichments_per_hour": 10 }
}
```
- `model` is optional (nullable) — if null, uses the provider's default model from config
- `model` is free-text — admin can enter any model name the provider supports
- Credentials stored using Laravel's `encrypted` cast — encrypted at rest in MySQL

**Credential resolution:**
1. DB `ai_settings` for the account (only source)
2. If not configured → return error: "AI not configured. Please add your provider credentials in settings."

No .env fallback. No server-side credentials. Each account is fully self-service.

**No .env vars needed for AI providers** — all credentials managed via admin API and stored in DB.

**Package to install:** `openai-php/client` (others use Laravel HTTP client directly)

**Tests:**
- Provider resolution from DB settings only (no .env fallback)
- No credentials configured → returns clear "not configured" error
- Each of the 8 providers formats requests correctly (mock HTTP)
- Retry on transient failures (429, 500, 503)
- Rate limiting per user per hour and per account per day
- Usage logging captures provider, model, tokens, cost
- Switching provider via admin API immediately changes behavior (no restart)
- Credentials stored encrypted (verify no plaintext in DB)
- `isConfigured()` returns false when API key missing
- `getAvailableProviders()` shows configured status per provider
- Custom model name passed correctly to provider API
- Invalid model name returns clear error from provider (surfaced to admin)
- Null model uses provider's default model
- Different accounts can use different providers independently

**Demo:** Admin calls `POST /api/ai/settings/update` with `{ "provider": "anthropic", "credentials": { "anthropic": { "api_key": "sk-ant-xxx" } } }` (no model specified — uses default `claude-sonnet-4-6`). Immediately, all AI features use Anthropic. Later admin updates model to `claude-opus-4-7` — switches instantly. Another account with no credentials configured gets "AI not configured" response on any AI endpoint.

---

### Task 9: Lead Scoring — Feature Engineering & Scoring Service

**Objective:** Compute 0-100 conversion likelihood score for leads.

**Files to create:**

- `app/Services/Ai/LeadScoringService.php`:
  - `calculateScore(Lead $lead): LeadScore`
  - `batchScore(Collection $leads): Collection`
  - `getScoreBreakdown(Lead $lead): array`

- `app/Services/Ai/LeadFeatureExtractor.php` — computes features:

| Category | Max Points | Factors |
|----------|-----------|---------|
| Profile | 20 | source conversion rate, has_company, has_designation, priority, services count |
| Engagement | 35 | follow-up count/frequency, callback responsiveness, days since last activity, notes |
| Meeting/Demo | 25 | meeting count, completion rate, interest expressed, demo attendance |
| Historical | 20 | same-source conversion rate, same-product conversion rate, assignee conversion rate |

- `app/DTOs/LeadScore.php` — score, breakdown, confidence (low/medium/high), factors (top 3)

- `app/Jobs/ScoreLeadsJob.php` — scheduled daily, batch-scores all active leads

**Migrations:**

```php
// lead_scoring_weights
Schema::create('lead_scoring_weights', function (Blueprint $table) {
    $table->id();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->string('category'); // profile, engagement, meeting, historical
    $table->string('factor');
    $table->decimal('weight', 5, 2)->default(1.0);
    $table->timestamps();

    $table->unique(['account_id', 'category', 'factor']);
});

// lead_scores
Schema::create('lead_scores', function (Blueprint $table) {
    $table->id();
    $table->foreignId('lead_id')->unique()->constrained()->cascadeOnDelete();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->unsignedTinyInteger('score'); // 0-100
    $table->json('breakdown');
    $table->json('factors'); // top contributing factors
    $table->enum('confidence', ['low', 'medium', 'high']);
    $table->text('insight')->nullable(); // AI-generated (Task 10)
    $table->dateTime('scored_at');
    $table->string('model_version')->default('v1');
    $table->timestamps();

    $table->index(['account_id', 'score']);
});
```

- Add `latestScore` relation on `Lead` model

**Tests:**
- Feature extraction for known states
- Higher scores for engaged leads
- Batch scoring 1000+ leads < 30 seconds
- Score breakdown meaningful
- Daily job updates scores
- Account scoping

**Demo:** Run `ScoreLeadsJob`. Query leads by score desc → engaged leads rank highest.

---

### Task 10: Lead Scoring — AI-Enhanced Insights

**Objective:** Natural language insights about lead scores and recommended actions.

**Implementation:**

- Add `getAiInsight(Lead $lead): string` to `LeadScoringService`
- Prompt includes: score breakdown, recent 5 activities, products of interest, lead profile
- AI generates: 2-3 sentence explanation + 2-3 recommended actions
- Create `app/Jobs/GenerateLeadInsightsJob.php` — runs for top 20% leads
- Add `insight` text column to `lead_scores` (already in Task 9 schema)
- Cache — regenerate when score changes >10 points or weekly
- Endpoint: `POST /api/ai/lead-insight` (input: `lead_id`)
- Graceful fallback without AI (return score only)

**Tests:**
- Prompt includes relevant data
- Caching prevents redundant calls
- Regeneration on score change
- Graceful degradation
- Permission checks (reuses lead read permissions)

**Demo:** `POST /api/ai/lead-insight` → `{ "score": 82, "insight": "Strong conversion signals..." }`

---

### Task 11: Natural Language Query Engine

**Objective:** Ask CRM questions in natural language, get structured answers.

**Files to create:**

- `app/Services/Ai/NaturalLanguageQueryService.php`:
  - `query(string $question, User $user): QueryResult`

- Query tools in `app/Services/Ai/QueryTools/`:
  - `SearchLeadsTool` — search with filters (status, source, date range, assignee, score)
  - `CountRecordsTool` — count with conditions
  - `GetMetricsTool` — dashboard metrics (revenue, conversion rate, meetings)
  - `GetUserActivityTool` — user activity summary

- `app/DTOs/QueryResult.php` — answer (string), data (array), tool_used, query_params

**Flow:**
1. User question → AI determines tool + parameters (function calling)
2. Execute tool via Eloquent (scoped by `accessibleUserIdsFor($user)`)
3. AI formats structured response as natural language

**Security:**
- ALL queries use parameterized Eloquent — no raw SQL
- User scope enforced via `ResolvesAccessibleUserIds`
- Rate limit: 20 per user per hour

**Endpoint:** `POST /api/ai/query` (input: `question`)

**Tests:**
- Common queries translate correctly
- Permission scoping enforced
- SQL injection impossible
- Ambiguous queries handled gracefully
- Rate limiting works
- Usage logged

**Demo:** "Show me verified leads this month without follow-ups" → structured list scoped to user's access.

---

### Task 12: Smart Suggestions Service

**Objective:** Next-best-action suggestions for leads and daily dashboard.

**Files to create:**

- `app/Services/Ai/SmartSuggestionService.php`:
  - `suggestForLead(Lead $lead, User $user): array`
  - `suggestForDashboard(User $user): array`

- `app/Jobs/GenerateDailySuggestionsJob.php`

**Suggestion types:** `follow_up`, `schedule_meeting`, `send_quotation`, `escalate`, `call_back`, `close_lead`, `create_task`

**Rule logic:**

| Condition | Suggestion |
|-----------|-----------|
| High score, no meeting scheduled | `schedule_meeting` |
| Interested, >3 days no activity | `follow_up` |
| Verified, meetings + interest expressed | `send_quotation` |
| Raw, 14+ days no activity | `close_lead` or `escalate` |
| Overdue callback | `call_back` |
| High score, no task assigned | `create_task` |

**Migration:**
```php
// ai_suggestions
Schema::create('ai_suggestions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->foreignId('lead_id')->nullable()->constrained()->nullOnDelete();
    $table->string('type'); // follow_up, schedule_meeting, etc.
    $table->unsignedTinyInteger('priority'); // 1-5
    $table->string('title');
    $table->text('description');
    $table->text('reason');
    $table->boolean('is_dismissed')->default(false);
    $table->dateTime('suggested_at');
    $table->dateTime('expires_at');
    $table->timestamps();

    $table->index(['user_id', 'is_dismissed', 'expires_at']);
});
```

**Endpoints:**
- `POST /api/ai/suggestions/lead` (lead_id)
- `POST /api/ai/suggestions/dashboard`
- `POST /api/ai/suggestions/dismiss` (suggestion_id)

**Tests:**
- Correct suggestions for different lead states
- Dashboard scoped to user's accessible leads
- Dismissal removes from responses
- Suggestions respect lead status
- Daily job generates correctly

**Demo:** Dashboard → `[{ "type": "follow_up", "priority": 1, "title": "Follow up with Acme Corp", "reason": "Score 78, 4 days idle" }]`

---

### Task 13: Content Generation Service

**Objective:** Generate follow-up messages, emails, quotation descriptions, meeting summaries.

**Files to create:**

- `app/Services/Ai/ContentGenerationService.php`:
  - `generateFollowUpMessage(Lead $lead, string $tone = 'professional'): string`
  - `generateEmailDraft(Lead $lead, string $purpose, array $keyPoints = []): string`
  - `generateQuotationDescription(array $productIds, Lead $lead): string`
  - `generateMeetingSummary(Meeting $meeting): string`

**Migration:**
```php
// ai_generated_content (audit trail)
Schema::create('ai_generated_content', function (Blueprint $table) {
    $table->id();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('content_type'); // follow_up, email, quotation, meeting_summary
    $table->json('input_context');
    $table->text('generated_content');
    $table->foreignId('lead_id')->nullable()->constrained()->nullOnDelete();
    $table->timestamps();

    $table->index(['account_id', 'content_type', 'created_at']);
});
```

**Endpoints:**
- `POST /api/ai/generate/follow-up` (lead_id, tone)
- `POST /api/ai/generate/email` (lead_id, purpose, key_points[])
- `POST /api/ai/generate/quotation-description` (product_ids[], lead_id)
- `POST /api/ai/generate/meeting-summary` (meeting_id)

**Rate limit:** 20 per user per hour

**Content is always a suggestion — never auto-sent.**

**Tests:**
- Prompts include relevant data
- Rate limiting enforced
- Audit trail captures content
- Graceful AI failure
- Account scoping

**Demo:** `POST /api/ai/generate/follow-up` → contextual message referencing lead's interests and last interaction.

---

### Task 14: AI Admin Configuration & Usage Dashboard

**Objective:** Admin endpoints for configuring AI and monitoring usage.

**Files to create:**

- `app/Http/Controllers/AiSettingController.php`:
  - `fetch_ai_config`
  - `update_ai_config`
  - `fetch_ai_usage`
  - `fetch_ai_logs`

**Migration:**
```php
// ai_settings
Schema::create('ai_settings', function (Blueprint $table) {
    $table->id();
    $table->foreignId('account_id')->unique()->constrained()->cascadeOnDelete();
    $table->json('config');
    $table->timestamps();
});
```

**Config structure:**
```json
{
    "provider": "openai",
    "features": {
        "lead_scoring": true,
        "nl_queries": true,
        "suggestions": true,
        "content_generation": true,
        "lead_enrichment": true
    },
    "rate_limits": {
        "queries_per_hour": 20,
        "generations_per_hour": 20,
        "enrichments_per_hour": 10
    }
}
```

**Endpoints:**
- `POST /api/ai/settings/fetch`
- `POST /api/ai/settings/update`
- `POST /api/ai/usage/fetch`
- `POST /api/ai/logs/fetch`

**Permissions (add to PermissionCatalog):**
- `setting.ai_configuration.read`
- `setting.ai_configuration.update`
- `setting.ai_configuration.override`

Disabled features return 403 with "This AI feature is not enabled for your account."

**Tests:**
- Toggle off → endpoints return 403
- Usage stats aggregate correctly
- Config persists and takes effect
- Per-account isolation
- Permission checks

**Demo:** View usage, disable content generation, verify endpoints return disabled.

---

### Task 15: Lead Enrichment — AI Web Search + Pluggable Provider Abstraction

**Objective:** On-demand lead/company research by searching the internet for publicly available data. Results shown as a preview — user reviews and decides which fields to apply to the CRM record. Includes search history and smart delta caching.

**Data Points to Surface:**

| Category | Fields |
|----------|--------|
| Company | Brief description of what they do, industry/sector, website, location (city/country), employee range, founded year, key products/services they offer, social links (LinkedIn, Twitter) |
| Person | LinkedIn profile URL, current job title/role verification, professional background summary, social profiles |
| Signals | Recent news/press mentions, product launches, partnerships, expansions, funding rounds |

> **Not included:** hiring signals, tech stack deep dives. Focus is on what helps a sales rep understand "who am I talking to and what does their company do."

**Architecture:**

```
┌─────────────────────────────────────────────────┐
│         LeadEnrichmentService                    │
│  (orchestrates providers, manages cache/delta,   │
│   merges results)                                │
└────────┬──────────────────────────┬─────────────┘
         │                          │
         ▼                          ▼
┌─────────────────┐     ┌──────────────────────┐
│ AiWebSearch     │     │ Third-party Provider │
│ Provider        │     │ (pluggable: Apollo,  │
│ (default)       │     │  Clearbit, etc.)     │
└─────────────────┘     └──────────────────────┘
```

**Search Context for Disambiguation (Privacy-Safe):**

When searching, the system uses existing DB data to disambiguate results — critical for companies with common names. **No PII (full email, full phone) is ever sent to external APIs.**

| Data Sent to Search | Source | Purpose |
|---------------------|--------|---------|
| Company name | `companies.name` | Primary search term |
| Email domain only | e.g., `acmecorp.in` (extracted from lead email, NOT the full address) | Strongest disambiguator |
| Country code only | e.g., `+91` → India (NOT the full phone number) | Geographic hint |
| Lead name | `leads.name` | Find the right person |
| Designation/title | `leads.designation` | Verify correct person at correct company |

**Example AI search prompt:**
> "Find information about company 'Acme Corp' with domain acmecorp.in, based in India. Contact person: Rajesh Kumar, Sales Director."

**Smart Delta Caching:**

On re-search, the system doesn't re-fetch everything. It checks what's cached and only fetches what's stale or missing:

| Data Category | Cache TTL | Reason |
|---------------|-----------|--------|
| Company description, industry, size, location | 30 days | Rarely changes |
| Person title, LinkedIn, background | 14 days | Changes occasionally |
| News, funding, press mentions | Always refresh | Time-sensitive |

**Flow:**
1. User hits "Research" on a lead → `POST /api/ai/enrich/lead`
2. System checks cache: if fully fresh, return cached immediately
3. If partially stale, identify which categories need refresh (delta)
4. Dispatch `EnrichLeadJob` for stale/missing categories only
5. Merge fresh results with cached data into complete response
6. Store full result in `lead_enrichments` (serves as search history)
7. User reviews the data preview
8. User selects specific fields to apply → `POST /api/ai/enrich/apply`
9. System updates only the selected lead/company fields

**Search History:**

Every enrichment request is stored in `lead_enrichments`. Users can:
- View all past searches for a lead (who searched, when, what was found)
- See which data was applied from previous enrichments
- Access cached results from colleagues (avoids redundant searches/costs)
- Compare data across time (see how company info evolved)

**Files to create:**

- `app/Contracts/LeadEnrichmentProviderInterface.php`
- `app/Services/Enrichment/LeadEnrichmentService.php` (orchestration, caching, delta logic)
- `app/Services/Enrichment/Providers/AiWebSearchProvider.php` (default provider)
- `app/Services/Enrichment/EnrichmentResultMerger.php` (deduplicates multi-provider results)
- `app/Services/Enrichment/SearchContextBuilder.php` (extracts safe search context from lead/company data)
- `app/Http/Controllers/LeadEnrichmentController.php`
- `app/Jobs/EnrichLeadJob.php`
- `app/DTOs/CompanyEnrichmentResult.php`
- `app/DTOs/PersonEnrichmentResult.php`
- `app/DTOs/EnrichmentResult.php`
- `config/enrichment.php`

**Interface:**
```php
interface LeadEnrichmentProviderInterface
{
    public function enrichCompany(string $companyName, array $context = []): CompanyEnrichmentResult;
    public function enrichPerson(string $name, array $context = []): PersonEnrichmentResult;
    public function getSignals(string $companyName, array $context = []): array;
    public function isAvailable(): bool;
    public function providerName(): string;
}
```

**Config (`config/enrichment.php`):**
```php
return [
    'default_provider' => env('ENRICHMENT_DEFAULT_PROVIDER', 'ai_web_search'),
    'providers' => [
        'ai_web_search' => [
            'enabled' => true,
            'class' => \App\Services\Enrichment\Providers\AiWebSearchProvider::class,
            'search_api' => env('ENRICHMENT_SEARCH_API', 'tavily'), // tavily, serper, or perplexity
            'search_api_key' => env('ENRICHMENT_SEARCH_API_KEY'),
        ],
        // Adding a new provider:
        // 1. Create class implementing LeadEnrichmentProviderInterface
        // 2. Add entry here
        // 3. Done — immediately available
        //
        // 'apollo' => [
        //     'enabled' => true,
        //     'class' => \App\Services\Enrichment\Providers\ApolloProvider::class,
        //     'api_key' => env('APOLLO_API_KEY'),
        // ],
    ],
    'cache_ttl' => [
        'company' => 30 * 24, // 30 days in hours
        'person' => 14 * 24,  // 14 days in hours
        'signals' => 0,       // always refresh
    ],
    'rate_limit_per_user_per_hour' => 10,
];
```

**Migration:**
```php
// lead_enrichments (cache + search history)
Schema::create('lead_enrichments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('lead_id')->constrained()->cascadeOnDelete();
    $table->foreignId('account_id')->constrained()->cascadeOnDelete();
    $table->foreignId('requested_by')->constrained('users')->cascadeOnDelete();
    $table->json('company_data')->nullable();
    $table->json('person_data')->nullable();
    $table->json('signals')->nullable();
    $table->json('search_context')->nullable(); // what was sent to search (for audit)
    $table->string('provider');
    $table->enum('status', ['pending', 'completed', 'partial', 'failed'])->default('pending');
    $table->text('error_message')->nullable();
    $table->boolean('is_applied')->default(false);
    $table->json('applied_fields')->nullable(); // which specific fields were applied
    $table->json('categories_refreshed')->nullable(); // which categories were fetched (for delta tracking)
    $table->timestamps();

    $table->index(['lead_id', 'created_at']);
    $table->index(['account_id', 'status']);
});
```

**Endpoints:**

| Endpoint | Input | Description |
|----------|-------|-------------|
| `POST /api/ai/enrich/lead` | `lead_id` | Trigger enrichment (returns cached or dispatches job) |
| `POST /api/ai/enrich/status` | `enrichment_id` | Check status of pending enrichment |
| `POST /api/ai/enrich/apply` | `enrichment_id`, `fields` | Apply selected data to lead/company |
| `POST /api/ai/enrich/history` | `lead_id` | Paginated search history for a lead |

**Apply endpoint `fields` payload example:**
```json
{
    "enrichment_id": 42,
    "fields": {
        "company": {
            "description": true,
            "industry": true,
            "website": true,
            "employee_count": false,
            "location": true
        },
        "lead": {
            "designation": true
        }
    }
}
```

**Adding a new third-party provider (developer workflow):**
1. Create class implementing `LeadEnrichmentProviderInterface`
2. Add entry to `config/enrichment.php` → `providers` array with `enabled`, `class`, and credentials
3. Done — `LeadEnrichmentService` automatically picks it up. No other code changes needed.

Multiple providers can be enabled simultaneously — `EnrichmentResultMerger` deduplicates and picks the highest-confidence data from each.

**Tests:**
- AI web search provider returns structured data (mock HTTP)
- Only email domain + country code sent externally (no full PII)
- Cache prevents re-fetching within TTL for each category
- Delta logic: only stale categories are re-fetched
- Rate limiting enforced (10 per user per hour)
- Apply endpoint updates only selected lead/company fields
- History endpoint returns all past enrichments for a lead
- Provider interface contract enforced
- Multiple providers merge results correctly
- Failed enrichment logged with error, doesn't break
- Account scoping (can't enrich another account's leads)
- Permission checks

**Demo:** Hit `POST /api/ai/enrich/lead` for a lead at company "Infosys" with email domain `infosys.com` → get: description ("Global IT services and consulting company..."), industry ("Information Technology"), employees ("300,000+"), location ("Bangalore, India"), website, recent news (Q4 results announcement). User selects industry + description to apply → company record updated. Hit again 2 days later → company data served from cache, only signals refreshed.

---

## Appendix A: New Permissions Summary

### Automation
```
setting.automation.create
setting.automation.read
setting.automation.update
setting.automation.delete
setting.automation.override
```

### AI
```
setting.ai_configuration.read
setting.ai_configuration.update
setting.ai_configuration.override
```

### Lead Enrichment
```
lead_management.raw_lead.enrich
lead_management.verified_lead.enrich
lead_management.client.enrich
```

---

## Appendix B: New Database Tables Summary

### Automation Engine
- `automation_workflows`
- `automation_conditions`
- `automation_actions`
- `automation_execution_logs`
- `automation_states`

### AI Features
- `ai_usage_logs`
- `ai_model_suggestions`
- `lead_scoring_weights`
- `lead_scores`
- `ai_suggestions`
- `ai_generated_content`
- `ai_settings`
- `lead_enrichments`

---

## Appendix C: New Scheduled Jobs

| Job | Schedule | Purpose |
|-----|----------|---------|
| `ProcessAutomationJob` | Queue (on-demand) | Process automation events async |
| `ExecuteDelayedActionJob` | Queue (delayed) | Delayed automation actions |
| `CheckStaleLeadsJob` | Hourly | Detect leads without follow-up |
| `SendAutomatedMeetingRemindersJob` | Every 15 min | Meeting reminders |
| `CloseStaleRawLeadsJob` | Daily 02:30 | Auto-close inactive raw leads |
| `ScoreLeadsJob` | Daily 03:00 | Batch lead scoring |
| `GenerateLeadInsightsJob` | Daily 04:00 | AI insights for top leads |
| `GenerateDailySuggestionsJob` | Daily 05:00 | Pre-compute dashboard suggestions |
| `EnrichLeadJob` | Queue (on-demand) | Lead/company enrichment via web search |

---

## Appendix D: New Packages

| Package | Purpose |
|---------|---------|
| `openai-php/client` | OpenAI API client (used by OpenAI adapter) |

> All other providers (Anthropic, Gemini, Mistral, Cohere, DeepSeek, Groq, Local) use Laravel's HTTP client directly — no additional packages needed. Most follow similar REST API patterns.
> Tavily/Serper for web search also use HTTP client directly (no dedicated package needed).

---

## Appendix E: Task Dependencies

```
Task 1 (Events) ─────┐
                      ├──► Task 3 (Execution Engine) ──► Task 4, 5, 6 (Templates)
Task 2 (Schema) ──────┘                                        │
                                                                ▼
                                                        Task 7 (Admin API)

Task 8 (AI Abstraction) ──► Task 9 (Scoring) ──► Task 10 (Insights)
         │                                              │
         ├──► Task 11 (NL Queries)                      │
         ├──► Task 12 (Suggestions) ◄───────────────────┘
         ├──► Task 13 (Content Gen)
         └──► Task 15 (Lead Enrichment)

Task 14 (AI Admin) depends on Tasks 8-13, 15
```

---

## Appendix F: Estimated Effort

| Task | Effort | Dependencies |
|------|--------|-------------|
| Task 1: Event System | 2-3 days | None |
| Task 2: Workflow Schema | 1-2 days | None |
| Task 3: Execution Engine | 3-4 days | Tasks 1, 2 |
| Task 4: Round-Robin Template | 1-2 days | Task 3 |
| Task 5: Stale Lead Escalation | 1-2 days | Task 3 |
| Task 6: Remaining Templates | 2-3 days | Task 3 |
| Task 7: Admin API + Custom Workflows | 3-4 days | Tasks 1-6 |
| Task 8: AI Abstraction | 2-3 days | None |
| Task 9: Lead Scoring | 3-4 days | Task 8 |
| Task 10: AI Insights | 1-2 days | Tasks 8, 9 |
| Task 11: NL Queries | 3-4 days | Task 8 |
| Task 12: Smart Suggestions | 2-3 days | Tasks 8, 9 |
| Task 13: Content Generation | 2-3 days | Task 8 |
| Task 14: AI Admin | 2-3 days | Tasks 8-13, 15 |
| Task 15: Lead Enrichment | 3-4 days | Task 8 |

**Total estimated: ~31-44 days (6-9 weeks)**
