diff --git a/app/Core/AI.php b/app/Core/AI.php index a649bd1..f081a86 100644 --- a/app/Core/AI.php +++ b/app/Core/AI.php @@ -10,7 +10,9 @@ use App\Services\InvoiceExtractionService; */ class AI { - private static string $baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent"; + private static string $baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"; + + private static int $maxRetries = 3; /** * Extract Data from Invoice Image or PDF (Base64) @@ -45,18 +47,49 @@ class AI ] ]; - $ch = curl_init(self::$baseUrl . "?key=" . $apiKey); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + // Retry with exponential backoff for 503/429 errors + for ($attempt = 1; $attempt <= self::$maxRetries; $attempt++) { + $ch = curl_init(self::$baseUrl . "?key=" . $apiKey); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + error_log("AI Error: cURL failed (attempt $attempt): $curlError"); + if ($attempt < self::$maxRetries) { + $wait = pow(2, $attempt) + rand(1, 3); + echo " Retrying in {$wait}s (cURL error)...\n"; + sleep($wait); + continue; + } + return null; + } + + if ($httpCode === 200) { + break; // Success + } + + // Retry on 503 (overloaded) or 429 (rate limit) + if (in_array($httpCode, [503, 429]) && $attempt < self::$maxRetries) { + $wait = pow(2, $attempt) + rand(1, 3); + echo " Gemini $httpCode — retrying in {$wait}s (attempt $attempt/" . self::$maxRetries . ")...\n"; + sleep($wait); + continue; + } + + error_log("AI Error: Gemini API returned code $httpCode. Response: " . $response); + return null; + } if ($httpCode !== 200) { - error_log("AI Error: Gemini API returned code $httpCode. Response: " . $response); + error_log("AI Error: All retries exhausted. Last code: $httpCode"); return null; } diff --git a/app/cron/process_batches.php b/app/cron/process_batches.php index f51623b..b0cfe10 100644 --- a/app/cron/process_batches.php +++ b/app/cron/process_batches.php @@ -111,11 +111,11 @@ try { // Save Lines if (!empty($extracted['lines'])) { - $lineStmt = $db->prepare("INSERT INTO invoice_lines (id, invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total) VALUES (?,?,?,?,?,?,?,?)"); + $lineStmt = $db->prepare("INSERT INTO invoice_lines (id, invoice_id, line_number, description, quantity, unit_price, tax_rate, total_amount) VALUES (?,?,?,?,?,?,?,?)"); foreach ($extracted['lines'] as $idx => $line) { $lineStmt->execute([ vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)), - $invoiceId, $line['line_number'] ?? ($idx + 1), $line['description'] ?? '', $line['quantity'] ?? 1, $line['unit_price'] ?? 0, $line['tax_rate'] ?? 0, $line['line_total'] ?? 0 + $invoiceId, $line['line_number'] ?? ($idx + 1), $line['description'] ?? '', $line['quantity'] ?? 1, $line['unit_price'] ?? 0, $line['tax_rate'] ?? 0, $line['line_total'] ?? $line['total_amount'] ?? 0 ]); } } diff --git a/scripts/complete_migration.sql b/scripts/complete_migration.sql index 3e09264..3ea6008 100644 --- a/scripts/complete_migration.sql +++ b/scripts/complete_migration.sql @@ -1,9 +1,8 @@ -- ════════════════════════════════════════════════════════════ --- مُصادَق — Comprehensive Phase 1 Migration --- Tables for AI Extraction, JoFotara Integration, and Usage Logs +-- مُصادَق — Complete Phase 1 Migration (MySQL 8.0 Compatible) -- ════════════════════════════════════════════════════════════ --- 1. Invoice Line Items (For AI extracted data) +-- 1. Invoice Line Items (AI extracted data) CREATE TABLE IF NOT EXISTS invoice_lines ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), invoice_id CHAR(36) NOT NULL, @@ -29,7 +28,7 @@ CREATE TABLE IF NOT EXISTS jofotara_submissions ( jofotara_uuid VARCHAR(100) NULL, xml_content LONGTEXT NULL, status ENUM('accepted', 'rejected', 'pending') DEFAULT 'pending', - qr_code_raw TEXT NULL, -- Base64 QR from JoFotara + qr_code_raw TEXT NULL, response_body JSON NULL, submitted_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -38,22 +37,7 @@ CREATE TABLE IF NOT EXISTS jofotara_submissions ( FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 3. Update Companies for JoFotara Credentials -ALTER TABLE companies -ADD COLUMN IF NOT EXISTS jofotara_client_id VARCHAR(255) NULL AFTER tax_identification_number, -ADD COLUMN IF NOT EXISTS jofotara_secret_key VARCHAR(255) NULL AFTER jofotara_client_id, -ADD COLUMN IF NOT EXISTS jofotara_status ENUM('active', 'inactive', 'pending') DEFAULT 'inactive' AFTER jofotara_secret_key; - --- 4. Update Invoices for AI and JoFotara metadata -ALTER TABLE invoices -ADD COLUMN IF NOT EXISTS invoice_category ENUM('simplified', 'standard') DEFAULT 'simplified' AFTER invoice_type, -ADD COLUMN IF NOT EXISTS ubl_type_code VARCHAR(10) DEFAULT '388' AFTER invoice_category, -ADD COLUMN IF NOT EXISTS payment_method_code VARCHAR(10) DEFAULT '013' AFTER ubl_type_code, -ADD COLUMN IF NOT EXISTS validation_warnings JSON NULL AFTER qr_code, -ADD COLUMN IF NOT EXISTS ai_confidence DECIMAL(5,2) DEFAULT 0 AFTER validation_warnings, -ADD COLUMN IF NOT EXISTS jofotara_uuid VARCHAR(100) NULL AFTER status; - --- 5. AI Usage Log (Cost & Token tracking) +-- 3. AI Usage Log CREATE TABLE IF NOT EXISTS ai_usage_log ( id INT AUTO_INCREMENT PRIMARY KEY, tenant_id CHAR(36) NOT NULL, @@ -71,7 +55,7 @@ CREATE TABLE IF NOT EXISTS ai_usage_log ( FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 6. Notifications Table +-- 4. Notifications CREATE TABLE IF NOT EXISTS notifications ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), tenant_id CHAR(36) NOT NULL, @@ -85,3 +69,26 @@ CREATE TABLE IF NOT EXISTS notifications ( INDEX idx_user_read (user_id, is_read), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ════════════════════════════════════════════════════════════ +-- 5. Safe ALTER TABLE (MySQL 8 compatible — no IF NOT EXISTS) +-- Run each block separately. If column already exists, +-- MySQL will show "Duplicate column" error — just skip it. +-- ════════════════════════════════════════════════════════════ + +-- 5a. Companies: JoFotara credentials +-- Run these ONE BY ONE. Skip any that say "Duplicate column name" + +ALTER TABLE companies ADD COLUMN jofotara_client_id VARCHAR(255) NULL; +ALTER TABLE companies ADD COLUMN jofotara_secret_key VARCHAR(255) NULL; +ALTER TABLE companies ADD COLUMN jofotara_status ENUM('active', 'inactive', 'pending') DEFAULT 'inactive'; + +-- 5b. Invoices: AI + JoFotara metadata +-- Run these ONE BY ONE. Skip any that say "Duplicate column name" + +ALTER TABLE invoices ADD COLUMN invoice_category ENUM('simplified', 'standard') DEFAULT 'simplified'; +ALTER TABLE invoices ADD COLUMN ubl_type_code VARCHAR(10) DEFAULT '388'; +ALTER TABLE invoices ADD COLUMN payment_method_code VARCHAR(10) DEFAULT '013'; +ALTER TABLE invoices ADD COLUMN validation_warnings JSON NULL; +ALTER TABLE invoices ADD COLUMN ai_confidence DECIMAL(5,2) DEFAULT 0; +ALTER TABLE invoices ADD COLUMN jofotara_uuid VARCHAR(100) NULL; diff --git a/scripts/reset_queue.sql b/scripts/reset_queue.sql new file mode 100644 index 0000000..fd4ee1c --- /dev/null +++ b/scripts/reset_queue.sql @@ -0,0 +1,5 @@ +-- Reset failed queue items so the cron worker retries them +UPDATE invoice_processing_queue SET status = 'pending', error_message = NULL WHERE status = 'failed'; + +-- Verify current queue state +SELECT id, batch_id, status, error_message, created_at FROM invoice_processing_queue ORDER BY created_at;