52 lines
3.2 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|