Files
musadaq-saas/app/modules_app/invoices/check_duplicate.php
2026-05-08 01:41:28 +03:00

131 lines
4.3 KiB
PHP

<?php
/**
* Check Duplicate Invoices
* POST /v1/invoices/check-duplicate
* Checks if similar invoice exists before processing
*/
use App\Core\Database;
use App\Core\Encryption;
use App\Middleware\AuthMiddleware;
$decoded = AuthMiddleware::check();
$data = input();
$db = Database::getInstance();
$tenantId = $decoded['tenant_id'];
$invoiceNumber = $data['invoice_number'] ?? null;
$supplierTin = $data['supplier_tin'] ?? null;
$grandTotal = $data['grand_total'] ?? null;
$invoiceDate = $data['invoice_date'] ?? null;
$excludeId = $data['exclude_id'] ?? null;
$duplicates = [];
// 1. Exact match on invoice number
if ($invoiceNumber) {
$sql = "SELECT id, invoice_number, invoice_date, grand_total, status, supplier_name
FROM invoices WHERE invoice_number = ? AND tenant_id = ?";
$params = [$invoiceNumber, $tenantId];
if ($excludeId) {
$sql .= " AND id != ?";
$params[] = $excludeId;
}
$stmt = $db->prepare($sql);
$stmt->execute($params);
$matches = $stmt->fetchAll();
foreach ($matches as $m) {
$decName = Encryption::decrypt($m['supplier_name']);
$duplicates[] = [
'id' => $m['id'],
'invoice_number' => $m['invoice_number'],
'invoice_date' => $m['invoice_date'],
'grand_total' => $m['grand_total'],
'status' => $m['status'],
'supplier_name' => ($decName !== false && $decName !== null) ? $decName : $m['supplier_name'],
'match_type' => 'exact_number',
'confidence' => 100,
];
}
}
// 2. Fuzzy match: same supplier TIN + same total + same date
if ($supplierTin && $grandTotal && $invoiceDate && empty($duplicates)) {
$sql = "SELECT id, invoice_number, invoice_date, grand_total, status, supplier_name, supplier_tin
FROM invoices
WHERE tenant_id = ?
AND invoice_date = ?
AND ABS(grand_total - ?) < 0.01";
$params = [$tenantId, $invoiceDate, $grandTotal];
if ($excludeId) {
$sql .= " AND id != ?";
$params[] = $excludeId;
}
$stmt = $db->prepare($sql);
$stmt->execute($params);
$matches = $stmt->fetchAll();
foreach ($matches as $m) {
$decTin = Encryption::decrypt($m['supplier_tin']);
$decName = Encryption::decrypt($m['supplier_name']);
if ($decTin === $supplierTin || $m['supplier_tin'] === $supplierTin) {
$duplicates[] = [
'id' => $m['id'],
'invoice_number' => $m['invoice_number'],
'invoice_date' => $m['invoice_date'],
'grand_total' => $m['grand_total'],
'status' => $m['status'],
'supplier_name' => ($decName !== false && $decName !== null) ? $decName : $m['supplier_name'],
'match_type' => 'fuzzy_tin_total_date',
'confidence' => 90,
];
}
}
}
// 3. Near match: same total + near date (±3 days)
if ($grandTotal && $invoiceDate && empty($duplicates)) {
$sql = "SELECT id, invoice_number, invoice_date, grand_total, status, supplier_name
FROM invoices
WHERE tenant_id = ?
AND ABS(grand_total - ?) < 0.01
AND ABS(DATEDIFF(invoice_date, ?)) <= 3";
$params = [$tenantId, $grandTotal, $invoiceDate];
if ($excludeId) {
$sql .= " AND id != ?";
$params[] = $excludeId;
}
$sql .= " LIMIT 5";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$matches = $stmt->fetchAll();
foreach ($matches as $m) {
$decName = Encryption::decrypt($m['supplier_name']);
$duplicates[] = [
'id' => $m['id'],
'invoice_number' => $m['invoice_number'],
'invoice_date' => $m['invoice_date'],
'grand_total' => $m['grand_total'],
'status' => $m['status'],
'supplier_name' => ($decName !== false && $decName !== null) ? $decName : $m['supplier_name'],
'match_type' => 'near_total_date',
'confidence' => 60,
];
}
}
json_success([
'is_duplicate' => !empty($duplicates),
'matches' => $duplicates,
'count' => count($duplicates),
], empty($duplicates) ? 'لا توجد فواتير مكررة' : 'تم العثور على فواتير مشابهة');