Update: 2026-05-08 00:43:22
This commit is contained in:
@@ -10,7 +10,9 @@ use App\Services\InvoiceExtractionService;
|
|||||||
*/
|
*/
|
||||||
class AI
|
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)
|
* Extract Data from Invoice Image or PDF (Base64)
|
||||||
@@ -45,18 +47,49 @@ class AI
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
$ch = curl_init(self::$baseUrl . "?key=" . $apiKey);
|
// Retry with exponential backoff for 503/429 errors
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
for ($attempt = 1; $attempt <= self::$maxRetries; $attempt++) {
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
$ch = curl_init(self::$baseUrl . "?key=" . $apiKey);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
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);
|
$response = curl_exec($ch);
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
$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) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,11 +111,11 @@ try {
|
|||||||
|
|
||||||
// Save Lines
|
// Save Lines
|
||||||
if (!empty($extracted['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) {
|
foreach ($extracted['lines'] as $idx => $line) {
|
||||||
$lineStmt->execute([
|
$lineStmt->execute([
|
||||||
vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)),
|
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
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
-- مُصادَق — Comprehensive Phase 1 Migration
|
-- مُصادَق — Complete Phase 1 Migration (MySQL 8.0 Compatible)
|
||||||
-- Tables for AI Extraction, JoFotara Integration, and Usage Logs
|
|
||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
-- 1. Invoice Line Items (For AI extracted data)
|
-- 1. Invoice Line Items (AI extracted data)
|
||||||
CREATE TABLE IF NOT EXISTS invoice_lines (
|
CREATE TABLE IF NOT EXISTS invoice_lines (
|
||||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
invoice_id CHAR(36) NOT NULL,
|
invoice_id CHAR(36) NOT NULL,
|
||||||
@@ -29,7 +28,7 @@ CREATE TABLE IF NOT EXISTS jofotara_submissions (
|
|||||||
jofotara_uuid VARCHAR(100) NULL,
|
jofotara_uuid VARCHAR(100) NULL,
|
||||||
xml_content LONGTEXT NULL,
|
xml_content LONGTEXT NULL,
|
||||||
status ENUM('accepted', 'rejected', 'pending') DEFAULT 'pending',
|
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,
|
response_body JSON NULL,
|
||||||
submitted_at DATETIME NULL,
|
submitted_at DATETIME NULL,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
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
|
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- 3. Update Companies for JoFotara Credentials
|
-- 3. AI Usage Log
|
||||||
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)
|
|
||||||
CREATE TABLE IF NOT EXISTS ai_usage_log (
|
CREATE TABLE IF NOT EXISTS ai_usage_log (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
tenant_id CHAR(36) NOT NULL,
|
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
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- 6. Notifications Table
|
-- 4. Notifications
|
||||||
CREATE TABLE IF NOT EXISTS notifications (
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
tenant_id CHAR(36) NOT NULL,
|
tenant_id CHAR(36) NOT NULL,
|
||||||
@@ -85,3 +69,26 @@ CREATE TABLE IF NOT EXISTS notifications (
|
|||||||
INDEX idx_user_read (user_id, is_read),
|
INDEX idx_user_read (user_id, is_read),
|
||||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) 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;
|
||||||
|
|||||||
5
scripts/reset_queue.sql
Normal file
5
scripts/reset_queue.sql
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user