Files
Investidor-V2/app/Services/Ai/SourceIngestion.php
T
nala 250ac35fd1
tests / ci (push) Canceled after 0s
first commit
2026-09-28 10:01:09 +02:00

52 lines
3.2 KiB
PHP

<?php
namespace App\Services\Ai;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
final class SourceIngestion
{
/** @param array<string, mixed> $data */
public function ingest(array $data, int $actor): string
{
return DB::transaction(function () use ($data, $actor): string {
$document = DB::table('documents')->where('id', $data['document_id'])->whereNull('deleted_at')->lockForUpdate()->first();
if (! $document || $document->status !== 'active' || $document->media_type !== 'text/plain'
|| ! in_array($document->data_classification, ['public', 'internal'], true)) {
throw ValidationException::withMessages(['document_id' => 'Documento activo de texto autorizado necessário; fontes confidenciais/restritas não são admitidas.']);
}
$root = realpath(storage_path('app/private'));
$path = realpath(Storage::disk('local')->path($document->storage_key));
if (! $root || ! $path || ! str_starts_with(str_replace('\\', '/', $path), str_replace('\\', '/', $root).'/')
|| ! is_file($path) || filesize($path) > (int) config('investmoz_ai.max_text_bytes')) {
throw ValidationException::withMessages(['document_id' => 'Ficheiro privado inexistente ou acima do limite.']);
}
$text = file_get_contents($path);
if ($text === false || ! mb_check_encoding($text, 'UTF-8') || str_contains($text, "\0") || trim($text) === ''
|| ! hash_equals($document->sha256, hash('sha256', $text))) {
throw ValidationException::withMessages(['document_id' => 'Formato, conteúdo ou checksum inválido.']);
}
if (DB::table('ai_sources')->where('document_id', $document->id)->where('document_version', $document->version_no)->exists()) {
throw ValidationException::withMessages(['document_id' => 'Esta versão já foi registada. Reveja a fonte existente.']);
}
$id = (string) Str::uuid();
DB::table('ai_sources')->insert([
...$data, 'id' => $id, 'document_version' => $document->version_no, 'document_checksum' => $document->sha256,
'submitted_by' => $actor, 'status' => 'pending', 'purpose' => 'investment_information', 'created_at' => now(), 'updated_at' => now(),
]);
$size = max(100, min(5000, (int) config('investmoz_ai.chunk_characters', 1800)));
foreach (mb_str_split($text, $size) as $position => $chunk) {
DB::table('ai_chunks')->insert(['id' => (string) Str::uuid(), 'source_id' => $id, 'position' => $position,
'language' => $data['language'], 'content' => $chunk, 'checksum' => hash('sha256', $chunk)]);
}
DB::table('audit_events')->insert(['actor_user_id' => $actor, 'entity_type' => 'ai_sources', 'entity_id' => $id,
'action' => 'submitted', 'source_system' => 'ai-gateway', 'after_data' => json_encode(['document_version' => $document->version_no], JSON_THROW_ON_ERROR)]);
return $id;
});
}
}