prepare("SELECT * FROM payment_requests WHERE id = ? AND tenant_id = ? AND status IN ('pending','uploaded')"); $stmt->execute([$paymentId, $tenantId]); $payment = $stmt->fetch(); if (!$payment) { json_error('طلب الدفع غير موجود أو تم معالجته بالفعل.', 404); } // Update the payment request with the provided bank reference $stmt = $db->prepare("UPDATE payment_requests SET bank_reference = ? WHERE id = ?"); $stmt->execute([$bankRef, $paymentId]); $payment['bank_reference'] = $bankRef; // 2. Immediate Check: Has the bot already received this transaction? $stmt = $db->prepare("SELECT * FROM bank_transactions WHERE bank_reference = ? AND is_claimed = 0 LIMIT 1"); $stmt->execute([$bankRef]); $transaction = $stmt->fetch(); if ($transaction) { $expectedAmount = (float)$payment['amount_jod']; $actualAmount = (float)$transaction['amount']; if (abs($expectedAmount - $actualAmount) < 0.01) { // MATCH FOUND! Auto activate. activateSubscription($db, $payment, $decoded['user_id']); $stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', verified_at = NOW() WHERE id = ?"); $stmt->execute([$paymentId]); $stmt = $db->prepare("UPDATE bank_transactions SET is_claimed = 1 WHERE id = ?"); $stmt->execute([$transaction['id']]); json_success([ 'status' => 'approved', 'auto_verified' => true, 'message' => 'تم العثور على الحوالة وتفعيل اشتراكك فوراً! شكراً لك.' ], 'تم تفعيل الاشتراك بنجاح'); } } // 3. If no immediate match, save the receipt and wait for AI/Bot backup $uploadDir = STORAGE_PATH . '/receipts/' . $tenantId; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0750, true); } $ext = pathinfo($_FILES['receipt']['name'], PATHINFO_EXTENSION) ?: 'jpg'; $filename = $paymentId . '_' . time() . '.' . $ext; $filepath = $uploadDir . '/' . $filename; if (!move_uploaded_file($_FILES['receipt']['tmp_name'], $filepath)) { json_error('فشل في حفظ صورة الوصل.', 500); } // 3. AI Analysis of receipt image $aiResult = analyzeReceipt($filepath, $payment); // 4. Smart Match: Use AI-extracted reference to search bank_transactions $aiExtractedRef = trim($aiResult['reference_number'] ?? ''); if (!empty($aiExtractedRef) && $aiExtractedRef !== 'unknown') { $stmt = $db->prepare("SELECT * FROM bank_transactions WHERE bank_reference = ? AND is_claimed = 0 LIMIT 1"); $stmt->execute([$aiExtractedRef]); $aiMatchTransaction = $stmt->fetch(); if ($aiMatchTransaction) { $expectedAmount = (float)$payment['amount_jod']; $actualAmount = (float)$aiMatchTransaction['amount']; if (abs($expectedAmount - $actualAmount) < 0.1) { // AI FOUND THE MATCH! activateSubscription($db, $payment, $decoded['user_id']); $stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', bank_reference = ?, verified_at = NOW() WHERE id = ?"); $stmt->execute([$aiExtractedRef, $paymentId]); $stmt = $db->prepare("UPDATE bank_transactions SET is_claimed = 1 WHERE id = ?"); $stmt->execute([$aiMatchTransaction['id']]); json_success([ 'status' => 'approved', 'auto_verified' => true, 'method' => 'ai_ref_matching', 'message' => 'تم العثور على الحوالة بنجاح وتفعيل الاشتراك آلياً!' ], 'تم تفعيل الاشتراك بنجاح'); } } } // 5. Calculate match score (for legacy manual review fallback) $matchScore = calculateMatchScore($aiResult, $payment); // 6. Update payment request with AI data $newStatus = $matchScore >= 85.0 ? 'verified' : 'uploaded'; $stmt = $db->prepare(" UPDATE payment_requests SET receipt_image_path = ?, ai_extracted_data = ?, ai_match_score = ?, status = ?, updated_at = NOW() WHERE id = ? "); $stmt->execute([ $filepath, json_encode($aiResult, JSON_UNESCAPED_UNICODE), $matchScore, $newStatus, $paymentId ]); // 7. Final attempt activation if high confidence match score if ($matchScore >= 90.0) { activateSubscription($db, $payment, $decoded['user_id']); $stmt = $db->prepare("UPDATE payment_requests SET status = 'approved', verified_at = NOW() WHERE id = ?"); $stmt->execute([$paymentId]); json_success([ 'status' => 'approved', 'match_score' => $matchScore, 'message' => 'تم التحقق من الوصل وتفعيل الاشتراك تلقائياً بنسبة مطابقة عالية.' ], 'تم تفعيل الاشتراك'); } json_success([ 'status' => $newStatus, 'match_score' => $matchScore, 'message' => $matchScore >= 70 ? 'تم استلام الوصل بنجاح، جاري المراجعة النهائية.' : 'تم استلام الوصل، بانتظار تأكيد الحوالة من البنك.' ], 'تم رفع الوصل'); } catch (\Exception $e) { error_log("Payment Receipt Upload Error: " . $e->getMessage()); json_error('حدث خطأ أثناء معالجة وصل الدفع.', 500); } /** * Analyze receipt image using Gemini AI */ function analyzeReceipt(string $imagePath, array $payment): array { $apiKey = env('GEMINI_API_KEY'); if (!$apiKey) { return ['error' => 'AI API key not configured']; } $imageData = base64_encode(file_get_contents($imagePath)); $mimeType = mime_content_type($imagePath) ?: 'image/jpeg'; $prompt = <<, "currency": "<العملة: JOD/USD/etc>", "sender_name": "<اسم المرسل/الدافع>", "receiver_name": "<اسم المستقبل>", "reference_number": "<رقم المرجع أو رقم العملية>", "transfer_date": "<تاريخ التحويل YYYY-MM-DD>", "bank_name": "<اسم البنك>", "is_valid_receipt": , "confidence": <نسبة الثقة 0-100> } المبلغ المتوقع: {$payment['amount_jod']} دينار أردني رقم المرجع المتوقع: {$payment['reference_number']} الاسم المستعار CliQ: {$payment['cliq_alias']} PROMPT; $model = env('GEMINI_MODEL'); $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}"; $payload = [ 'contents' => [ [ 'parts' => [ ['text' => $prompt], [ 'inline_data' => [ 'mime_type' => $mimeType, 'data' => $imageData ] ] ] ] ], 'generationConfig' => [ 'responseMimeType' => 'application/json', 'temperature' => 0.1 ] ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_TIMEOUT => 30 ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { error_log("Gemini Receipt Analysis Error: $response"); return ['error' => 'AI analysis failed', 'is_valid_receipt' => false]; } $respData = json_decode($response, true); $jsonText = $respData['candidates'][0]['content']['parts'][0]['text'] ?? ''; $parsed = json_decode($jsonText, true); return $parsed ?: ['error' => 'Failed to parse AI response', 'is_valid_receipt' => false]; } /** * Calculate match score between AI extraction and expected payment */ function calculateMatchScore(array $aiResult, array $payment): float { if (!($aiResult['is_valid_receipt'] ?? false)) return 0.0; $score = 0.0; // Amount match (40 points) $extractedAmount = (float)($aiResult['amount'] ?? 0); $expectedAmount = (float)$payment['amount_jod']; if (abs($extractedAmount - $expectedAmount) < 0.01) { $score += 40; } elseif (abs($extractedAmount - $expectedAmount) < 1.0) { $score += 20; } // Reference number match (30 points) $extractedRef = strtoupper(trim($aiResult['reference_number'] ?? '')); $expectedRef = strtoupper(trim($payment['reference_number'])); if ($extractedRef === $expectedRef) { $score += 30; } elseif (str_contains($extractedRef, $expectedRef) || str_contains($expectedRef, $extractedRef)) { $score += 15; } // Receiver name / CliQ alias match (15 points) $receiverName = strtolower($aiResult['receiver_name'] ?? ''); $cliqAlias = strtolower($payment['cliq_alias']); if (str_contains($receiverName, $cliqAlias) || str_contains($cliqAlias, $receiverName)) { $score += 15; } // AI confidence boost (15 points) $confidence = (float)($aiResult['confidence'] ?? 0); $score += ($confidence / 100) * 15; return min(round($score, 2), 100.0); } /** * Auto-activate subscription upon verified payment */ function activateSubscription(\PDO $db, array $payment, string $userId): void { $stmt = $db->prepare("SELECT * FROM subscription_plans WHERE id = ? AND is_active = 1"); $stmt->execute([$payment['plan_id']]); $plan = $stmt->fetch(); if (!$plan) return; $startDate = date('Y-m-d H:i:s'); $endDate = date('Y-m-d H:i:s', strtotime('+30 days')); $stmt = $db->prepare(" INSERT INTO subscriptions (tenant_id, plan_id, max_companies, max_invoices_per_month, max_users, price_jod, status, current_period_start, current_period_end, updated_at) VALUES (:t_id, :p_id, :max_c, :max_i, :max_u, :price, 'active', :start, :end, NOW()) ON DUPLICATE KEY UPDATE plan_id = VALUES(plan_id), max_companies = VALUES(max_companies), max_invoices_per_month = VALUES(max_invoices_per_month), max_users = VALUES(max_users), price_jod = VALUES(price_jod), status = 'active', current_period_start = VALUES(current_period_start), current_period_end = VALUES(current_period_end), updated_at = NOW() "); $stmt->execute([ 't_id' => $payment['tenant_id'], 'p_id' => $plan['id'], 'max_c' => $plan['max_companies'], 'max_i' => $plan['max_invoices_month'], 'max_u' => $plan['max_users'], 'price' => $plan['price_jod'], 'start' => $startDate, 'end' => $endDate ]); // Log activation $logStmt = $db->prepare("INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id, new_data) VALUES (?, ?, 'subscription.activated', 'payment', ?, ?)"); $logStmt->execute([ $payment['tenant_id'], $userId, $payment['id'], json_encode(['plan_id' => $plan['id'], 'auto_verified' => true]) ]); }