Passa al contenuto principale

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.

app/Services/JWTService.php
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

app/Http/Controllers/AuthController.php
// 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.

app/Services/NodeApiService.php
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

app/Services/NodeApiService.php
/**
* 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.

app/Services/JWTService.php
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

app/Http/Controllers/AuthController.php
// 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.

app/Services/SubscriptionService.php
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

MetodoControlloNotifica
canCreateAssistant()Limite assistenti/agenti-
canSendMessage()Limite messaggi mensiliMessagesLimitWarning/Reached
canUploadMoreFiles()Limite numero fileFilesLimitReached
canUploadFileStorage()Limite storage MBStorageLimitWarning/Reached
isSubscriptionActive()Scadenza abbonamentoSubscriptionEndingSoon/Inactive

💳 StripeApiService

Servizio per l'integrazione con Stripe per la gestione dei pagamenti e abbonamenti.

app/Services/StripeApiService.php
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

app/Http/Controllers/AssistantController.php
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

app/Http/Controllers/FileController.php
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

  1. Service Layer Pattern: Separazione della logica di business dai controller
  2. Dependency Injection: Iniezione delle dipendenze per testabilità
  3. Static Methods: Per operazioni utility (JWTService, NodeApiService)
  4. 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.