Update: 2026-05-08 01:15:44

This commit is contained in:
Hamza-Ayed
2026-05-08 01:15:44 +03:00
parent 1a6ed52a52
commit 928e8e27e3
10 changed files with 991 additions and 4 deletions

View File

@@ -0,0 +1,133 @@
<?php
/**
* Export Invoices as CSV (Excel-compatible)
* GET /v1/invoices/export
* Downloads a CSV file with invoice data + line items
*/
use App\Core\Database;
use App\Core\Encryption;
use App\Middleware\AuthMiddleware;
$decoded = AuthMiddleware::check();
$db = Database::getInstance();
$tenantId = $decoded['tenant_id'];
$role = $decoded['role'];
$companyId = $_GET['company_id'] ?? null;
$dateFrom = $_GET['date_from'] ?? null;
$dateTo = $_GET['date_to'] ?? null;
$status = $_GET['status'] ?? null;
// Build query with filters
$where = [];
$params = [];
if ($role !== 'super_admin') {
$where[] = 'i.tenant_id = ?';
$params[] = $tenantId;
}
if ($companyId) {
$where[] = 'i.company_id = ?';
$params[] = $companyId;
}
if ($dateFrom) {
$where[] = 'i.invoice_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo) {
$where[] = 'i.invoice_date <= ?';
$params[] = $dateTo;
}
if ($status) {
$where[] = 'i.status = ?';
$params[] = $status;
}
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$stmt = $db->prepare("
SELECT i.*, c.name as company_name_raw
FROM invoices i
JOIN companies c ON i.company_id = c.id
$whereClause
ORDER BY i.invoice_date DESC
LIMIT 5000
");
$stmt->execute($params);
$invoices = $stmt->fetchAll();
// Decrypt helper
$dec = function($val) {
if (empty($val)) return '';
$result = Encryption::decrypt((string)$val);
return ($result !== false && $result !== null) ? $result : (string)$val;
};
// UTF-8 BOM for Excel compatibility
$output = "\xEF\xBB\xBF";
// CSV headers
$output .= implode(',', [
'رقم الفاتورة',
'تاريخ الفاتورة',
'الشركة',
'اسم المورّد',
'الرقم الضريبي للمورّد',
'عنوان المورّد',
'اسم العميل',
'الرقم الضريبي للعميل',
'نوع الفاتورة',
'المبلغ قبل الضريبة',
'قيمة الخصم',
'قيمة الضريبة',
'الإجمالي',
'العملة',
'الحالة',
'JoFotara UUID',
'تاريخ الإنشاء',
]) . "\n";
foreach ($invoices as $inv) {
$statusAr = match($inv['status']) {
'extracted' => 'مستخرجة',
'approved' => 'معتمدة',
'submitted' => 'مقدمة لجوفتورة',
'rejected' => 'مرفوضة',
default => $inv['status']
};
$row = [
'"' . str_replace('"', '""', $inv['invoice_number'] ?? '') . '"',
$inv['invoice_date'] ?? '',
'"' . str_replace('"', '""', $dec($inv['company_name_raw'] ?? '')) . '"',
'"' . str_replace('"', '""', $dec($inv['supplier_name'])) . '"',
'"' . $dec($inv['supplier_tin']) . '"',
'"' . str_replace('"', '""', $dec($inv['supplier_address'])) . '"',
'"' . str_replace('"', '""', $dec($inv['buyer_name'])) . '"',
'"' . $dec($inv['buyer_tin']) . '"',
$inv['invoice_type'] ?? 'cash',
$inv['subtotal'] ?? '0',
$inv['discount_total'] ?? '0',
$inv['tax_amount'] ?? '0',
$inv['grand_total'] ?? '0',
$inv['currency_code'] ?? 'JOD',
$statusAr,
$inv['jofotara_uuid'] ?? '',
$inv['created_at'] ?? '',
];
$output .= implode(',', $row) . "\n";
}
// Send as download
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="musadaq_invoices_' . date('Y-m-d') . '.csv"');
header('Cache-Control: no-cache');
echo $output;
exit;

View File

@@ -0,0 +1,116 @@
<?php
/**
* Update Invoice (Before Approval Only)
* POST /v1/invoices/update
* Allows editing extracted data before final approval.
*/
use App\Core\Database;
use App\Core\Encryption;
use App\Core\AuditLogger;
use App\Middleware\AuthMiddleware;
$decoded = AuthMiddleware::check();
$data = input();
$id = $data['id'] ?? null;
if (!$id) json_error('معرّف الفاتورة مطلوب', 422);
$db = Database::getInstance();
$tenantId = $decoded['tenant_id'];
$role = $decoded['role'];
// 1. Fetch & verify access
$query = $role === 'super_admin'
? "SELECT * FROM invoices WHERE id = ?"
: "SELECT * FROM invoices WHERE id = ? AND tenant_id = ?";
$params = $role === 'super_admin' ? [$id] : [$id, $tenantId];
$stmt = $db->prepare($query);
$stmt->execute($params);
$invoice = $stmt->fetch();
if (!$invoice) json_error('الفاتورة غير موجودة', 404);
// 2. Only allow editing extracted (not yet approved) invoices
if (!in_array($invoice['status'], ['extracted', 'pending'])) {
json_error('لا يمكن تعديل الفاتورة بعد اعتمادها', 403);
}
$db->beginTransaction();
try {
// 3. Update main invoice fields
$fields = [];
$values = [];
$plainFields = ['invoice_number', 'invoice_date', 'invoice_type', 'invoice_category',
'subtotal', 'tax_amount', 'discount_total', 'grand_total', 'currency_code'];
foreach ($plainFields as $f) {
if (isset($data[$f])) {
$fields[] = "$f = ?";
$values[] = $data[$f];
}
}
// Encrypted fields
$encryptedFields = [
'supplier_name' => 'supplier_name',
'supplier_tin' => 'supplier_tin',
'supplier_address' => 'supplier_address',
'buyer_name' => 'buyer_name',
'buyer_tin' => 'buyer_tin',
'buyer_national_id' => 'buyer_national_id',
];
foreach ($encryptedFields as $key => $column) {
if (isset($data[$key])) {
$fields[] = "$column = ?";
$values[] = !empty($data[$key]) ? Encryption::encrypt($data[$key]) : '';
}
}
if (!empty($fields)) {
$fields[] = 'updated_at = NOW()';
$values[] = $id;
$sql = "UPDATE invoices SET " . implode(', ', $fields) . " WHERE id = ?";
$db->prepare($sql)->execute($values);
}
// 4. Update line items (if provided)
if (isset($data['items']) && is_array($data['items'])) {
// Delete old lines
$db->prepare("DELETE FROM invoice_lines WHERE invoice_id = ?")->execute([$id]);
// Insert new lines
$lineStmt = $db->prepare(
"INSERT INTO invoice_lines (id, invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
foreach ($data['items'] as $idx => $item) {
$lineStmt->execute([
Database::generateUuid(),
$id,
$item['line_number'] ?? ($idx + 1),
$item['description'] ?? '',
$item['quantity'] ?? 1,
$item['unit_price'] ?? 0,
$item['tax_rate'] ?? 0,
$item['line_total'] ?? 0,
]);
}
}
$db->commit();
AuditLogger::log('invoice.updated', 'invoice', $id, null, [
'fields_updated' => array_keys($data),
], $decoded);
json_success(null, 'تم تحديث بيانات الفاتورة بنجاح');
} catch (\Exception $e) {
$db->rollBack();
error_log("Invoice Update Error: " . $e->getMessage());
json_error('فشل تحديث الفاتورة: ' . $e->getMessage(), 500);
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Monthly Tax Report API
* GET /v1/reports/tax-summary
* Returns monthly summary of tax, revenue, and invoice statistics
*/
use App\Core\Database;
use App\Middleware\AuthMiddleware;
$decoded = AuthMiddleware::check();
$db = Database::getInstance();
$tenantId = $decoded['tenant_id'];
$role = $decoded['role'];
$companyId = $_GET['company_id'] ?? null;
$month = $_GET['month'] ?? date('m');
$year = $_GET['year'] ?? date('Y');
$where = ["MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?"];
$params = [$month, $year];
if ($role !== 'super_admin') {
$where[] = 'i.tenant_id = ?';
$params[] = $tenantId;
}
if ($companyId) {
$where[] = 'i.company_id = ?';
$params[] = $companyId;
}
$whereClause = 'WHERE ' . implode(' AND ', $where);
// 1. Main aggregation
$stmt = $db->prepare("
SELECT
COUNT(*) as total_invoices,
SUM(CASE WHEN status = 'approved' OR status = 'submitted' THEN 1 ELSE 0 END) as approved_count,
SUM(CASE WHEN status = 'extracted' THEN 1 ELSE 0 END) as pending_count,
SUM(CASE WHEN status = 'submitted' THEN 1 ELSE 0 END) as submitted_count,
COALESCE(SUM(subtotal), 0) as total_subtotal,
COALESCE(SUM(tax_amount), 0) as total_tax,
COALESCE(SUM(discount_total), 0) as total_discount,
COALESCE(SUM(grand_total), 0) as total_grand,
COALESCE(AVG(grand_total), 0) as avg_invoice_amount,
COALESCE(MAX(grand_total), 0) as max_invoice_amount,
COALESCE(MIN(grand_total), 0) as min_invoice_amount
FROM invoices i
$whereClause
");
$stmt->execute($params);
$summary = $stmt->fetch();
// 2. Daily breakdown for chart
$stmtDaily = $db->prepare("
SELECT
DAY(i.invoice_date) as day_num,
COUNT(*) as count,
COALESCE(SUM(grand_total), 0) as daily_total,
COALESCE(SUM(tax_amount), 0) as daily_tax
FROM invoices i
$whereClause
GROUP BY DAY(i.invoice_date)
ORDER BY day_num
");
$stmtDaily->execute($params);
$dailyBreakdown = $stmtDaily->fetchAll();
// 3. Invoice type breakdown
$stmtType = $db->prepare("
SELECT
invoice_type,
COUNT(*) as count,
COALESCE(SUM(grand_total), 0) as total
FROM invoices i
$whereClause
GROUP BY invoice_type
");
$stmtType->execute($params);
$typeBreakdown = $stmtType->fetchAll();
// 4. Top 5 suppliers
$stmtSuppliers = $db->prepare("
SELECT
supplier_name,
COUNT(*) as invoice_count,
COALESCE(SUM(grand_total), 0) as total_amount
FROM invoices i
$whereClause
GROUP BY supplier_name
ORDER BY total_amount DESC
LIMIT 5
");
$stmtSuppliers->execute($params);
$topSuppliers = $stmtSuppliers->fetchAll();
// Decrypt supplier names
foreach ($topSuppliers as &$s) {
$decrypted = \App\Core\Encryption::decrypt($s['supplier_name']);
$s['supplier_name'] = ($decrypted !== false && $decrypted !== null) ? $decrypted : $s['supplier_name'];
}
unset($s);
// 5. Comparison with previous month
$prevMonth = $month == 1 ? 12 : $month - 1;
$prevYear = $month == 1 ? $year - 1 : $year;
$prevWhere = str_replace(
"MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?",
"MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?",
implode(' AND ', $where)
);
$prevParams = [$prevMonth, $prevYear];
if ($role !== 'super_admin') $prevParams[] = $tenantId;
if ($companyId) $prevParams[] = $companyId;
$stmtPrev = $db->prepare("
SELECT
COUNT(*) as total_invoices,
COALESCE(SUM(grand_total), 0) as total_grand,
COALESCE(SUM(tax_amount), 0) as total_tax
FROM invoices i
WHERE MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?
" . ($role !== 'super_admin' ? " AND i.tenant_id = ?" : "")
. ($companyId ? " AND i.company_id = ?" : "")
);
$stmtPrev->execute($prevParams);
$previous = $stmtPrev->fetch();
// Calculate growth
$growth = [
'invoices' => $previous['total_invoices'] > 0
? round((($summary['total_invoices'] - $previous['total_invoices']) / $previous['total_invoices']) * 100, 1)
: 0,
'revenue' => $previous['total_grand'] > 0
? round((($summary['total_grand'] - $previous['total_grand']) / $previous['total_grand']) * 100, 1)
: 0,
'tax' => $previous['total_tax'] > 0
? round((($summary['total_tax'] - $previous['total_tax']) / $previous['total_tax']) * 100, 1)
: 0,
];
json_success([
'month' => (int)$month,
'year' => (int)$year,
'summary' => $summary,
'daily_breakdown' => $dailyBreakdown,
'type_breakdown' => $typeBreakdown,
'top_suppliers' => $topSuppliers,
'previous_month' => $previous,
'growth' => $growth,
], 'تقرير ضريبة المبيعات الشهري');