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);
🌐 NodeApiService
Servizio per la comunicazione con il backend Node.js che gestisce l'AI e i vector store.
class NodeApiService
{
protected $baseUrl;
public function __construct()
{
$this->baseUrl = config('app.node_server_url');
}
/**
* Crea una nuova collezione per un assistente o agente
*/
public static function createCollection($model)
{
$jwt = self::getJWT($model);
$url = config('app.node_server_url') . "/collections";
try {
$response = Commons::retry(function () use ($url, $jwt) {
return Http::withHeaders([
'Authorization' => "Bearer {$jwt}",
])->post($url);
});
$json = $response->json();
if (isset($json['success']) && $json['success']) {
$data = $json['data'] ?? null;
return self::extractUuid($data);
}
return null;
} catch (\Exception $e) {
Log::error('Error creating collection: ' . $e->getMessage());
return null;
}
}
/**
* Upload di file al vector store
*/
public static function upload($model, $vectorStoreUuid, $parameters)
{
if (empty($model->id) || empty($vectorStoreUuid) || empty($parameters) || !isset($parameters['type'])) {
throw new \Exception('Invalid parameters for upload to Agent Node.js server');
}
$collection = self::getCollection($model);
if (empty($collection)) {
throw new \Exception('Error getting collection for upload to Agent Node.js server');
}
try {
$jwt = JWTService::generateJWT([
'companyId' => $model->company_id,
'exp' => time() + 60 * 60,
]);
switch ($parameters['type']) {
case 'file':
$url = config('app.node_server_url') . "/upload/files/{$collection}";
$file = Storage::disk('public')->get($parameters['file']);
$fileName = basename($parameters['file']);
$response = Http::attach('file', $file, $fileName)
->withHeaders(['Authorization' => "Bearer {$jwt}"])
->post($url);
break;
case 'single':
case 'sitemap':
$url = config('app.node_server_url') . "/upload/url/{$collection}";
$response = Http::withHeaders(['Authorization' => "Bearer {$jwt}"])
->post($url, $parameters);
break;
case 'plugin':
$url = config('app.node_server_url') . "/upload/products/{$collection}";
$response = Http::withHeaders(['Authorization' => "Bearer {$jwt}"])
->post($url, $parameters);
break;
}
return $response->json();
} catch (\Exception $e) {
Log::error('Error uploading file to Agent Node.js server: ' . $e->getMessage());
return ['success' => false, 'data' => ['model' => $model->id, 'parameters' => $parameters, 'error' => $e->getMessage()]];
}
}
}
Gestione Task Asincroni
/**
* Controlla e aggiorna lo stato di un task di upload
*/
public static function checkAndUpdateFileTaskStatus(File $file): bool
{
if (strtolower($file->status) === 'completed' || $file->task_id === null) {
if (strtolower($file->status) === 'completed' && $file->task_id !== null) {
$file->task_id = null;
$file->processed = true;
$file->save();
return true;
}
return false;
}
try {
$jwt = JWTService::generateJWT([
'companyId' => $file->company_id,
'exp' => time() + 60 * 60,
]);
$url = config('app.node_server_url') . '/queue/tasks/' . $file->task_id;
$response = Http::withHeaders(['Authorization' => "Bearer {$jwt}"])->get($url);
$responseData = $response->json();
$changed = false;
if (array_key_exists('status', $responseData)) {
$oldStatus = $file->status;
$file->status = $responseData['status'];
if ($oldStatus !== $file->status) {
$changed = true;
}
}
if (strtolower($file->status) === 'completed' && $file->task_id !== null) {
$file->task_id = null;
$file->processed = true;
$changed = true;
}
if ($changed) {
$file->save();
$file->refresh();
}
return $changed;
} catch (\Exception $e) {
Log::error('NodeApiService::checkTaskStatus - Error: ' . $e->getMessage());
return false;
}
}
🔐 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
$vectorStoreUuid = NodeApiService::createCollection($assistant);
$assistant->update(['vector_store_uuid' => $vectorStoreUuid]);
return response()->json($assistant);
}
}
Esempio di Utilizzo per Upload File
class FileController extends Controller
{
public function upload(Request $request)
{
$subscriptionService = new SubscriptionService(auth()->user()->company_id);
// Controlla limiti storage
$fileSizeMb = $request->file('file')->getSize() / (1024 * 1024);
if (!$subscriptionService->canUploadFileStorage($fileSizeMb)) {
return response()->json([
'error' => 'Limite storage raggiunto'
], 403);
}
// Upload file
$file = File::create($fileData);
// Carica nel vector store
NodeApiService::upload($assistant, $assistant->vector_store_uuid, [
'type' => 'file',
'file_id' => $file->id
]);
return response()->json($file);
}
}
🏗️ 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, NodeApiService)
- 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.