$input * @return list> */ public function recommend(array $input): array { $query = Opportunity::published(); $factors = []; foreach (['sector_id', 'province', 'maturity_level'] as $field) { if (isset($input[$field]) && $input[$field] !== '') { $query->where($field, $input[$field]); $factors[] = $field; } } if ($factors === []) { return []; } return array_values($query->orderBy('id')->limit(10)->get()->map(fn ($opportunity) => [ 'record_type' => 'opportunity', 'record_id' => $opportunity->id, 'title' => $opportunity->title, 'source_url' => '/oportunidades/'.$opportunity->slug, 'score' => count($factors), 'score_max' => 3, 'score_meaning' => 'matched_declared_filters', 'favourable_factors' => $factors, 'incompatibilities' => [], 'eliminatory_conditions' => ['not_published', 'mismatch_with_any_selected_filter'], 'missing_data' => array_filter(['budget', 'time_horizon', 'risk_profile', $opportunity->last_validated_at ? null : 'validation_date']), 'source_date' => $opportunity->last_validated_at?->toIso8601String(), 'semantic_similarity' => null, ])->all()); } /** @return list> */ public function classify(int $id): array { $interest = InterestRequest::findOrFail($id); $consent = DB::table('consents')->where('subject_ref', $interest->reference)->where('purpose', 'ai_assistance') ->where('status', 'granted')->whereNotNull('granted_at')->where('granted_at', '<=', now())->whereNull('deleted_at')->whereNull('withdrawn_at')->exists(); abort_unless($consent, 422, 'É necessário consentimento específico registado para assistência sobre este pedido.'); // No name, email, phone, message or sensitive attribute is used. return [[ 'record_type' => 'interest', 'record_id' => $interest->id, 'source_url' => '/management/interests/'.$interest->id, 'sector_declared' => $interest->sector_interest, 'investment_range_declared' => $interest->investment_range, 'intention' => $interest->opportunity_id ? 'interest_in_identified_opportunity' : 'general_information', 'priority' => 'human_review_required', 'suggested_owner' => null, 'next_action' => config('investmoz_ai.classification.next_action'), 'missing_data' => ['confirmed_capacity', 'time_horizon', 'responsible_person', 'institutional_priority'], 'source_date' => $interest->created_at?->toIso8601String(), ]]; } /** @return list> */ public function quality(): array { $alerts = []; foreach (Lead::whereIn('data_classification', ['public', 'internal'])->whereNull('owner_user_id')->orderBy('id')->limit(20)->get(['id']) as $lead) { $alerts[] = ['record_type' => 'lead', 'record_id' => $lead->id, 'rule' => 'missing_owner', 'source_url' => '/management/crm/leads/'.$lead->id]; } foreach (Lead::whereIn('data_classification', ['public', 'internal'])->whereIn('status', ['new', 'qualified']) ->whereNotExists(CrmTask::availableForWork()->whereColumn('tasks.lead_id', 'leads.id')->selectRaw('1')->toBase()) ->orderBy('id')->limit(20)->get(['id']) as $lead) { $alerts[] = ['record_type' => 'lead', 'record_id' => $lead->id, 'rule' => 'missing_next_action', 'source_url' => '/management/crm/leads/'.$lead->id]; } foreach (DB::table('ai_sources')->where('valid_until', '<', now()->toDateString())->orderBy('id')->limit(20)->get(['id', 'valid_until']) as $source) { $alerts[] = ['record_type' => 'source', 'record_id' => $source->id, 'rule' => 'expired_source', 'valid_until' => $source->valid_until]; } foreach (DB::table('documents')->whereNull('deleted_at')->whereIn('data_classification', ['public', 'internal']) ->select('sha256')->selectRaw('count(*) as copies')->groupBy('sha256')->havingRaw('count(*) > 1')->limit(20)->get() as $duplicate) { $alerts[] = ['record_type' => 'document_group', 'rule' => 'duplicate_document_checksum', 'copies' => (int) $duplicate->copies]; } foreach (DB::table('geo_geometries')->whereNull('deleted_at')->whereIn('data_classification', ['public', 'internal']) ->whereRaw('NOT ST_IsValid(geom)')->limit(20)->get(['id']) as $geometry) { $alerts[] = ['record_type' => 'geometry', 'record_id' => $geometry->id, 'rule' => 'invalid_geometry']; } return $alerts; } /** @return list> */ public function sla(): array { return array_values(CrmTask::whereIn('data_classification', ['public', 'internal'])->availableForWork()->where('due_at', '<', now())->orderBy('due_at')->limit(50) ->get()->map(fn ($task) => [ 'record_type' => 'task', 'record_id' => $task->id, 'rule' => 'overdue_open_task', 'due_at' => $task->getAttribute('due_at')?->toIso8601String(), 'lead_id' => $task->lead_id, 'source_url' => $task->lead_id ? '/management/crm/leads/'.$task->lead_id : null, 'suggested_action' => 'review_owner_deadline_and_dependencies', 'impact' => 'not_estimated', 'priority' => 'human_review_required', ])->all()); } }