131 lines
4.3 KiB
PHP
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) ? 'لا توجد فواتير مكررة' : 'تم العثور على فواتير مشابهة');
|