285 lines
9.6 KiB
PHP
285 lines
9.6 KiB
PHP
<?php
|
|
/**
|
|
* Upload Payment Receipt (Admin/Accountant)
|
|
* POST /api/v1/payments/upload-receipt
|
|
*
|
|
* Receives a screenshot/photo of the CliQ payment receipt.
|
|
* AI analyzes the image and matches against the payment request.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Core\Database;
|
|
use App\Core\AI;
|
|
use App\Middleware\AuthMiddleware;
|
|
|
|
$decoded = AuthMiddleware::check();
|
|
|
|
if (!in_array($decoded['role'], ['admin', 'accountant'])) {
|
|
json_error('غير مصرح لك برفع وصل الدفع.', 403);
|
|
}
|
|
|
|
$paymentId = $_POST['payment_id'] ?? null;
|
|
if (!$paymentId) {
|
|
json_error('معرف طلب الدفع مطلوب.', 422);
|
|
}
|
|
|
|
if (!isset($_FILES['receipt']) || $_FILES['receipt']['error'] !== UPLOAD_ERR_OK) {
|
|
json_error('صورة وصل الدفع مطلوبة.', 422);
|
|
}
|
|
|
|
$db = Database::getInstance();
|
|
$tenantId = $decoded['tenant_id'];
|
|
|
|
try {
|
|
// 1. Verify payment request exists and belongs to this tenant
|
|
$stmt = $db->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);
|
|
}
|
|
|
|
// 2. Save receipt image
|
|
$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. Calculate match score
|
|
$matchScore = calculateMatchScore($aiResult, $payment);
|
|
|
|
// 5. Update payment request
|
|
$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
|
|
]);
|
|
|
|
// 6. If high confidence match, auto-activate subscription
|
|
if ($matchScore >= 85.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' => 'تم التحقق من الدفع وتفعيل الاشتراك تلقائياً!',
|
|
'extracted' => $aiResult,
|
|
], 'تم اعتماد الدفع وتفعيل الاشتراك');
|
|
}
|
|
|
|
json_success([
|
|
'status' => $newStatus,
|
|
'match_score' => $matchScore,
|
|
'message' => $matchScore >= 60
|
|
? 'تم رفع الوصل. جاري المراجعة من الإدارة.'
|
|
: 'لم نتمكن من التحقق التلقائي. تم إرسال الطلب للمراجعة اليدوية.',
|
|
'extracted' => $aiResult,
|
|
], 'تم رفع وصل الدفع');
|
|
|
|
} 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 = <<<PROMPT
|
|
أنت محلل وصولات دفع ذكي. حلل صورة وصل الدفع/التحويل البنكي واستخرج المعلومات التالية بدقة.
|
|
أرجع JSON فقط بدون أي نص إضافي:
|
|
|
|
{
|
|
"amount": <المبلغ المحول كرقم>,
|
|
"currency": "<العملة: JOD/USD/etc>",
|
|
"sender_name": "<اسم المرسل/الدافع>",
|
|
"receiver_name": "<اسم المستقبل>",
|
|
"reference_number": "<رقم المرجع أو رقم العملية>",
|
|
"transfer_date": "<تاريخ التحويل YYYY-MM-DD>",
|
|
"bank_name": "<اسم البنك>",
|
|
"is_valid_receipt": <true/false>,
|
|
"confidence": <نسبة الثقة 0-100>
|
|
}
|
|
|
|
المبلغ المتوقع: {$payment['amount_jod']} دينار أردني
|
|
رقم المرجع المتوقع: {$payment['reference_number']}
|
|
الاسم المستعار CliQ: {$payment['cliq_alias']}
|
|
PROMPT;
|
|
|
|
$model = env('GEMINI_MODEL', 'gemini-1.5-flash');
|
|
$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, details) VALUES (?, ?, 'subscription.activated', 'payment', ?, ?)");
|
|
$logStmt->execute([
|
|
$payment['tenant_id'],
|
|
$userId,
|
|
$payment['id'],
|
|
json_encode(['plan_id' => $plan['id'], 'auto_verified' => true])
|
|
]);
|
|
}
|