Servizi e Integrazioni
I servizi nell'applicazione Tidiko AI gestiscono le funzionalità core, le integrazioni esterne e la logica di business complessa. Questi servizi forniscono un'interfaccia pulita e riutilizzabile per le operazioni principali.
🔐 JWTService
Servizio per la gestione dei token JWT per l'autenticazione sicura.
class JWTService
{
/**
* Genera un token JWT
*/
public static function generateJWT(array $payload): string
{
return JWT::encode($payload, config('app.jwt_secret'), 'HS256');
}
/**
* Refresh JWT tokens using a refresh token
*/
public static function refreshJwtTokens(string $refreshToken): array
{
try {
// Verify the refresh token
$decoded = JWT::decode($refreshToken, new \Firebase\JWT\Key(config('app.jwt_secret'), 'HS256'));
// Check if token is expired
if (isset($decoded->exp) && $decoded->exp < time()) {
throw new \Exception('Refresh token expired');
}
// Generate new tokens
$jwt = self::generateJWT([
'companyId' => $decoded->companyId,
'exp' => time() + 60 * 60, // 1 hour
]);
$jwtRefresh = self::generateJWT([
'companyId' => $decoded->companyId,
'exp' => time() + 60 * 60 * 24, // 24 hours
]);
return [
'jwt' => $jwt,
'jwtRefresh' => $jwtRefresh
];
} catch (\Exception $e) {
throw new \Exception('Invalid refresh token: ' . $e->getMessage());
}
}
}
Utilizzo JWTService
// Generazione token per widget
$token = JWTService::generateJWT([
'company_id' => $company->id,
'assistant_id' => $assistant->id,
'exp' => time() + 3600 // 1 ora
]);
// Refresh token
$newTokens = JWTService::refreshJwtTokens($refreshToken);
🌐 WorkerApiService e integrazioni Node
Per upload file, embedding, collection Qdrant e webhook task status usare WorkerApiService (comunicazione diretta col Node Worker).
Per chiamate HTTP al Node Agent (es. riepilogo conversazione) usare NodeAgentHttpClient.
Legacy: la vecchia integrazione Node Agent è stata rimossa (aprile 2026). Riferimento storico: legacy/node-api-service.
⚙️ WorkerApiService
Servizio per la comunicazione diretta con il Node Worker (embedding, upload, webhook). Introdotto con la refactor della pipeline upload (PR #121).
class WorkerApiService
{
public static function upload($model, string $vectorStoreUuid, array $params): array
public static function submitFileTask(File $file, string $vectorStoreUuid): array
public static function cancelFileTask(File $file): bool
public static function deleteItem(File $file, string $vectorStoreUuid, string $fileUuid): bool
public static function createCollection(Assistant|Agent $model): ?string
public static function checkAndUpdateFileTaskStatus(File $file): bool
public static function isHealthy(): bool
}
Webhook task-status
Il worker notifica Laravel al completamento dei task embedding:
POST /api/worker/webhooks/task-status (middleware: worker.webhook)
Vedi: Pipeline Upload Diretta al Worker.
🔐 JWTService
Servizio per la gestione dei token JWT per l'autenticazione sicura.
class JWTService
{
/**
* Genera un token JWT
*/
public static function generateJWT(array $payload): string
{
return JWT::encode($payload, config('app.jwt_secret'), 'HS256');
}
/**
* Refresh JWT tokens using a refresh token
*/
public static function refreshJwtTokens(string $refreshToken): array
{
try {
// Verify the refresh token
$decoded = JWT::decode($refreshToken, new \Firebase\JWT\Key(config('app.jwt_secret'), 'HS256'));
// Check if token is expired
if (isset($decoded->exp) && $decoded->exp < time()) {
throw new \Exception('Refresh token expired');
}
// Generate new tokens
$jwt = self::generateJWT([
'companyId' => $decoded->companyId,
'exp' => time() + 60 * 60, // 1 hour
]);
$jwtRefresh = self::generateJWT([
'companyId' => $decoded->companyId,
'exp' => time() + 60 * 60 * 24, // 24 hours
]);
return [
'jwt' => $jwt,
'jwtRefresh' => $jwtRefresh
];
} catch (\Exception $e) {
throw new \Exception('Invalid refresh token: ' . $e->getMessage());
}
}
}
Utilizzo JWTService
// Generazione token per widget
$token = JWTService::generateJWT([
'company_id' => $company->id,
'assistant_id' => $assistant->id,
'exp' => time() + 3600 // 1 ora
]);
// Refresh token
$newTokens = JWTService::refreshJwtTokens($refreshToken);
📊 SubscriptionService
Servizio per la gestione degli abbonamenti e controllo dei limiti.
class SubscriptionService
{
protected $company;
public function __construct($companyId)
{
$this->company = Company::find($companyId);
}
/**
* Verifica se può creare un nuovo assistente
*/
public function canCreateAssistant(): bool
{
if (!$this->company || !$this->isSubscriptionActive()) {
return false;
}
$subscriptionPlan = $this->company->subscriptionPlan();
if (!$subscriptionPlan) {
return false;
}
$assistantCount = $this->company->assistants()->count();
$agentsCount = $this->company->agents()->count();
return $assistantCount + $agentsCount < $subscriptionPlan->max_assistants;
}
/**
* Verifica se può inviare messaggi
*/
public function canSendMessage(): bool
{
if (!$this->company || !$this->isSubscriptionActive()) {
return false;
}
$subscriptionPlan = $this->company->subscriptionPlan();
if (!$subscriptionPlan) {
return false;
}
$assistants = $this->company->assistants;
$messageCount = 0;
foreach ($assistants as $assistant) {
$conversations = $assistant->conversations()
->whereMonth('started_at', Carbon::now()->month)
->whereYear('started_at', Carbon::now()->year)
->get();
foreach ($conversations as $conversation) {
$messageCount += $conversation->messages()->where('sender', 'user')->count();
}
}
$remainingMessages = $subscriptionPlan->max_messages - $messageCount;
if ($remainingMessages <= $subscriptionPlan->max_messages * 0.2 && $remainingMessages > 0) {
$this->sendNotificationOnce('last_message_warning_sent', new MessagesLimitWarning($remainingMessages));
}
if ($remainingMessages <= 0) {
$this->sendNotificationOnce('last_message_limit_sent', new MessagesLimitReached());
return false;
}
return $remainingMessages > 0;
}
/**
* Verifica se può caricare più file
*/
public function canUploadMoreFiles(): bool
{
if (!$this->company || !$this->isSubscriptionActive()) {
return false;
}
$subscriptionPlan = $this->company->subscriptionPlan();
if (!$subscriptionPlan) {
return false;
}
$fileCount = $this->company->assistants()
->with('files')
->get()
->sum(fn($assistant) => $assistant->files->count());
if ($fileCount >= $subscriptionPlan->max_file_uploads) {
$this->sendNotificationOnce('last_file_limit_sent', new FilesLimitReached());
return false;
}
return true;
}
/**
* Verifica se può caricare file per storage
*/
public function canUploadFileStorage($fileSizeMb): bool
{
if (!$this->company || !$this->isSubscriptionActive()) {
return false;
}
$subscriptionPlan = $this->company->subscriptionPlan();
if (!$subscriptionPlan) {
return false;
}
$usedStorageMb = $this->company->assistants()
->with('files')
->get()
->sum(fn($assistant) => $assistant->files->sum('size') / (1024 * 1024));
$remainingStorageMb = $subscriptionPlan->storage_limit_mb - $usedStorageMb;
if ($remainingStorageMb <= $subscriptionPlan->storage_limit_mb * 0.2 && $remainingStorageMb > $fileSizeMb) {
$this->sendNotificationOnce('last_storage_warning_sent', new StorageLimitWarning($remainingStorageMb));
}
if ($remainingStorageMb <= $fileSizeMb) {
$this->sendNotificationOnce('last_storage_limit_sent', new StorageLimitReached());
return false;
}
return true;
}
/**
* Verifica se l'abbonamento è attivo
*/
public function isSubscriptionActive(): bool
{
if (!$this->company || !$this->company->subscriptionPlan()) {
return false;
}
if (!empty($this->company->end_date)) {
$endDate = Carbon::parse($this->company->end_date);
$daysRemaining = Carbon::now()->diffInDays($endDate, false);
if ($daysRemaining <= 5 && $daysRemaining > 0) {
$this->sendNotificationOnce('last_subscription_warning_sent', new SubscriptionEndingSoon($daysRemaining));
}
if ($daysRemaining <= 0) {
$this->sendNotificationOnce('last_subscription_limit_sent', new SubscriptionInactive());
return false;
}
}
return true;
}
/**
* Invia notifica una sola volta
*/
private function sendNotificationOnce(string $timestampColumn, $notification): void
{
$now = Carbon::now();
if (!$this->company->{$timestampColumn} ||
Carbon::parse($this->company->{$timestampColumn})->lt($now->subDay())) {
$this->company->notify($notification);
$this->company->update([$timestampColumn => $now]);
}
}
}
Controlli Limiti SubscriptionService
| Metodo | Controllo | Notifica |
|---|---|---|
canCreateAssistant() | Limite assistenti/agenti | - |
canSendMessage() | Limite messaggi mensili | MessagesLimitWarning/Reached |
canUploadMoreFiles() | Limite numero file | FilesLimitReached |
canUploadFileStorage() | Limite storage MB | StorageLimitWarning/Reached |
isSubscriptionActive() | Scadenza abbonamento | SubscriptionEndingSoon/Inactive |
💳 StripeApiService
Servizio per l'integrazione con Stripe per la gestione dei pagamenti e abbonamenti.
class StripeApiService
{
protected string $apiKey;
protected string $baseUrl;
public function __construct()
{
$this->baseUrl = 'https://api.stripe.com/v1';
$this->setApiKey();
}
/**
* Ottiene tutti i prodotti con i loro prezzi
*/
public function getProductsWithPrices(array $options = [], bool $testMode = false): array
{
$this->setApiKey($testMode);
try {
$products = $this->getProducts($options);
$result = [];
foreach ($products as $product) {
if ($product['active'] === true) {
$prices = $this->getPricesForProduct($product['id']);
$result[] = [
'product' => $product,
'prices' => $prices
];
}
}
return [
'success' => true,
'data' => $result,
'count' => count($result)
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Crea un piano di abbonamento completo
*/
public function createSubscriptionPlan(array $planData, bool $testMode = false): array
{
try {
$this->setApiKey($testMode);
// 1. Crea il prodotto
$productData = [
'name' => $planData['name'],
'description' => $planData['description'] ?? null,
'metadata' => $planData['metadata'] ?? []
];
$productResult = $this->createProduct($productData);
if (!$productResult['success']) {
return $productResult;
}
$product = $productResult['data'];
$createdPrices = [];
// 2. Crea prezzo mensile
if (isset($planData['monthly_price']) && $planData['monthly_price'] > 0) {
$monthlyResult = $this->createPrice([
'product' => $product['id'],
'unit_amount' => intval($planData['monthly_price'] * 100),
'currency' => $planData['currency'] ?? 'eur',
'recurring' => ['interval' => 'month']
]);
if ($monthlyResult['success']) {
$createdPrices['monthly'] = $monthlyResult['data'];
}
}
return [
'success' => true,
'data' => [
'product' => $product,
'prices' => $createdPrices
]
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}
Funzionalità StripeApiService
- Gestione Prodotti: Creazione, lettura, aggiornamento prodotti Stripe
- Gestione Prezzi: Creazione prezzi mensili/annuali
- Modalità Test/Produzione: Supporto per chiavi sandbox e live
- Formattazione Dati: Metodi per dropdown e selezioni
- Gestione Errori: Logging e gestione eccezioni
🔄 Utilizzo dei Servizi
Esempio di Utilizzo nei Controller
class AssistantController extends Controller
{
public function store(Request $request)
{
// Controlla limiti abbonamento
$subscriptionService = new SubscriptionService(auth()->user()->company_id);
if (!$subscriptionService->canCreateAssistant()) {
return response()->json([
'error' => 'Limite assistenti raggiunto per il tuo piano'
], 403);
}
// Crea assistente
$assistant = Assistant::create($request->validated());
// Crea vector store (Worker)
$vectorStoreUuid = WorkerApiService::createCollection($assistant);
$assistant->update(['vector_store_uuid' => $vectorStoreUuid]);
return response()->json($assistant);
}
}
Esempio di Utilizzo per Upload File
class FileUploadController extends Controller
{
public function upload(Request $request): JsonResponse
{
$user = auth()->user();
$model = $this->resolveUploadModel($request); // Assistant o Agent
return $this->dispatchUpload($request, $user, $model);
// dispatchUpload crea il record File e chiama WorkerApiService::submitFileTask()
}
}
Per il flusso completo (validazione, limiti piano, retry) vedi Upload pipeline.
🏗️ Architettura dei Servizi
Pattern di Design Utilizzati
- Service Layer Pattern: Separazione della logica di business dai controller
- Dependency Injection: Iniezione delle dipendenze per testabilità
- Static Methods: Per operazioni utility (JWTService, WorkerApiService)
- Instance Methods: Per operazioni con stato (SubscriptionService)
Flusso di Comunicazione
I servizi forniscono un'architettura modulare e scalabile per la gestione delle funzionalità core dell'applicazione Tidiko AI, garantendo separazione delle responsabilità e facilità di manutenzione.