Update: 2026-05-05 01:44:06
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Core\AI;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\QuotaMiddleware;
|
||||
|
||||
// 1. Auth Check
|
||||
$decoded = AuthMiddleware::check();
|
||||
@@ -12,33 +15,31 @@ $tenantId = $decoded['tenant_id'];
|
||||
$userId = $decoded['user_id'];
|
||||
|
||||
// --- QUOTA CHECK ---
|
||||
\App\Middleware\QuotaMiddleware::checkInvoiceQuota($tenantId);
|
||||
QuotaMiddleware::checkInvoiceQuota($tenantId);
|
||||
// -------------------
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
$allowedRoles = ['admin', 'accountant', 'employee'];
|
||||
if (!in_array($decoded['role'], $allowedRoles)) {
|
||||
json_error('Unauthorized to upload invoices', 403);
|
||||
json_error('غير مصرح لك برفع الفواتير', 403);
|
||||
}
|
||||
|
||||
// 2. Validate Request
|
||||
$data = input();
|
||||
$companyId = $data['company_id'] ?? null;
|
||||
|
||||
if (!$companyId || !isset($_FILES['invoice'])) {
|
||||
json_error('Company ID and invoice file are required', 422);
|
||||
json_error('رقم الشركة وملف الفاتورة مطلوبان', 422);
|
||||
}
|
||||
|
||||
// 3. Permission Check
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$userId = $decoded['user_id'];
|
||||
|
||||
// Everyone (except Super Admin) must belong to the same tenant as the company
|
||||
$stmt = $db->prepare("SELECT id FROM companies WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL");
|
||||
$stmt->execute([$companyId, $tenantId]);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
json_error('Access denied to this company or invalid company ID', 403);
|
||||
json_error('الوصول مرفوض لهذه الشركة أو رقم الشركة غير صحيح', 403);
|
||||
}
|
||||
|
||||
// 4. Handle File Upload (Step-by-step for permission safety)
|
||||
@@ -70,23 +71,30 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
|
||||
json_error('فشل في قراءة الملف المرفوع', 500);
|
||||
}
|
||||
$base64Data = base64_encode($fileContent);
|
||||
|
||||
$extracted = \App\Core\AI::extractInvoiceData($base64Data, $mimeType);
|
||||
|
||||
|
||||
$extracted = AI::extractInvoiceData($base64Data, $mimeType);
|
||||
|
||||
if (!$extracted) {
|
||||
// Still save basic record if AI fails
|
||||
$stmt = $db->prepare("INSERT INTO invoices (tenant_id, company_id, uploaded_by, original_file_path, status, created_at) VALUES (?, ?, ?, ?, 'uploaded', NOW())");
|
||||
$stmt->execute([$tenantId, $companyId, $userId, $targetFile]);
|
||||
json_success(['id' => $db->lastInsertId()], 'تم رفع الفاتورة ولكن فشل استخراج البيانات تلقائياً');
|
||||
// Still save basic record if AI fails, ensuring all NOT NULL and new columns are met
|
||||
$invoiceId = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4));
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO invoices (
|
||||
id, tenant_id, company_id, uploaded_by, original_file_path, status, created_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, 'uploaded', NOW()
|
||||
)
|
||||
");
|
||||
$stmt->execute([$invoiceId, $tenantId, $companyId, $userId, $targetFile]);
|
||||
json_success(['id' => $invoiceId], 'تم رفع الفاتورة ولكن فشل استخراج البيانات تلقائياً');
|
||||
}
|
||||
|
||||
// 6. Save Extracted Data with Encryption
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// 5.5 Duplicate Prevention Check (Now inside transaction for safety)
|
||||
// 5.5 Duplicate Prevention Check
|
||||
$supplierTin = $extracted['supplier']['tin'] ?? '';
|
||||
$invoiceNum = $extracted['invoice_number'] ?? '';
|
||||
$invoiceNum = $extracted['invoice_number'] ?? '';
|
||||
$invoiceDate = $extracted['invoice_date'] ?? '';
|
||||
|
||||
$invoiceHash = null;
|
||||
@@ -102,10 +110,6 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
|
||||
}
|
||||
}
|
||||
|
||||
$invoiceId = bin2hex(random_bytes(16)); // Generate UUID
|
||||
// Let's use a standard UUID format if possible, but MySQL CHAR(36) accepts anything.
|
||||
// Actually, let's just use the DB's UUID() function but FETCH it back or generate it here.
|
||||
// I'll use a better UUID generator logic.
|
||||
$invoiceId = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4));
|
||||
|
||||
$stmt = $db->prepare("
|
||||
@@ -127,38 +131,40 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
'id' => $invoiceId,
|
||||
'tenant_id' => $tenantId,
|
||||
'id' => $invoiceId,
|
||||
'tenant_id' => $tenantId,
|
||||
'company_id' => $companyId,
|
||||
'uploaded_by'=> $userId,
|
||||
'path' => $targetFile,
|
||||
'num' => $extracted['invoice_number'] ?? null,
|
||||
'date' => $extracted['invoice_date'] ?? null,
|
||||
'type' => $extracted['invoice_type'] ?? 'cash',
|
||||
'cat' => $extracted['invoice_category'] ?? 'simplified',
|
||||
's_tin' => \App\Core\Encryption::encrypt($extracted['supplier']['tin'] ?? ''),
|
||||
's_name' => \App\Core\Encryption::encrypt($extracted['supplier']['name'] ?? ''),
|
||||
's_addr' => \App\Core\Encryption::encrypt($extracted['supplier']['address'] ?? ''),
|
||||
'b_tin' => \App\Core\Encryption::encrypt($extracted['buyer']['tin'] ?? ''),
|
||||
'b_name' => \App\Core\Encryption::encrypt($extracted['buyer']['name'] ?? ''),
|
||||
'b_nid' => \App\Core\Encryption::encrypt($extracted['buyer']['national_id'] ?? ''),
|
||||
'sub' => $extracted['subtotal'] ?? 0,
|
||||
'tax' => $extracted['tax_amount'] ?? 0,
|
||||
'disc' => $extracted['discount_total'] ?? 0,
|
||||
'total' => $extracted['grand_total'] ?? 0,
|
||||
'cur' => $extracted['currency_code'] ?? 'JOD',
|
||||
'hash' => $invoiceHash,
|
||||
'warnings' => isset($extracted['validation_warnings']) && !empty($extracted['validation_warnings']) ? json_encode($extracted['validation_warnings']) : null
|
||||
'uploaded_by' => $userId,
|
||||
'path' => $targetFile,
|
||||
'num' => $extracted['invoice_number'] ?? null,
|
||||
'date' => $extracted['invoice_date'] ?? null,
|
||||
'type' => $extracted['invoice_type'] ?? 'cash',
|
||||
'cat' => $extracted['invoice_category'] ?? 'simplified',
|
||||
's_tin' => Encryption::encrypt($extracted['supplier']['tin'] ?? ''),
|
||||
's_name' => Encryption::encrypt($extracted['supplier']['name'] ?? ''),
|
||||
's_addr' => Encryption::encrypt($extracted['supplier']['address'] ?? ''),
|
||||
'b_tin' => Encryption::encrypt($extracted['buyer']['tin'] ?? ''),
|
||||
'b_name' => Encryption::encrypt($extracted['buyer']['name'] ?? ''),
|
||||
'b_nid' => Encryption::encrypt($extracted['buyer']['national_id'] ?? ''),
|
||||
'sub' => $extracted['subtotal'] ?? 0,
|
||||
'tax' => $extracted['tax_amount'] ?? 0,
|
||||
'disc' => $extracted['discount_total'] ?? 0,
|
||||
'total' => $extracted['grand_total'] ?? 0,
|
||||
'cur' => $extracted['currency_code'] ?? 'JOD',
|
||||
'hash' => $invoiceHash,
|
||||
'warnings' => isset($extracted['validation_warnings']) && !empty($extracted['validation_warnings']) ? json_encode($extracted['validation_warnings']) : null
|
||||
]);
|
||||
|
||||
// Save Line Items
|
||||
if (!empty($extracted['lines'])) {
|
||||
$lineStmt = $db->prepare("
|
||||
INSERT INTO invoice_lines (invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO invoice_lines (id, invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
foreach ($extracted['lines'] as $item) {
|
||||
$lineId = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4));
|
||||
$lineStmt->execute([
|
||||
$lineId,
|
||||
$invoiceId,
|
||||
$item['line_number'] ?? 1,
|
||||
$item['description'] ?? 'N/A',
|
||||
@@ -171,11 +177,11 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
|
||||
// --- INCREMENT QUOTA ---
|
||||
\App\Middleware\QuotaMiddleware::incrementInvoiceUsage($tenantId);
|
||||
QuotaMiddleware::incrementInvoiceUsage($tenantId);
|
||||
// -----------------------
|
||||
|
||||
|
||||
json_success(['id' => $invoiceId], 'تم رفع الفاتورة واستخراج البيانات بنجاح');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@@ -187,4 +193,4 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
|
||||
$uploadError = $_FILES['invoice']['error'] ?? 'Unknown';
|
||||
error_log("UPLOAD ERROR: move_uploaded_file failed. Error Code: $uploadError. Target: $targetFile. Tmp: " . ($_FILES['invoice']['tmp_name'] ?? 'N/A'));
|
||||
json_error('Failed to save uploaded file. PHP Error Code: ' . $uploadError, 500);
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,251 @@ CREATE TABLE audit_logs (
|
||||
);
|
||||
```
|
||||
|
||||
## File: `migrate_phase2.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Phase 2 Migration: AI Pre-Audit & Duplicate Prevention
|
||||
* Run: php scripts/migrate_phase2.php
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "═══════════════════════════════════════════\n";
|
||||
echo " مُصادَق — Phase 2 Migration\n";
|
||||
echo " AI Pre-Audit & Duplicate Prevention\n";
|
||||
echo "═══════════════════════════════════════════\n\n";
|
||||
|
||||
$migrations = [
|
||||
|
||||
// 1. Add invoice_hash for duplicate prevention
|
||||
'add_invoice_hash' => "ALTER TABLE invoices ADD COLUMN invoice_hash VARCHAR(64) NULL",
|
||||
|
||||
// 2. Add validation_warnings for AI Pre-Audit
|
||||
'add_validation_warnings' => "ALTER TABLE invoices ADD COLUMN validation_warnings JSON NULL",
|
||||
|
||||
// 2.5 Add deleted_at for soft delete (Missing in Phase 1 for this table)
|
||||
'add_invoices_soft_delete' => "ALTER TABLE invoices ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL",
|
||||
|
||||
// 3. Create Unique Index to prevent duplicates within the same tenant & company
|
||||
// Using a regular index for now, application logic will handle uniqueness to allow nulls
|
||||
'add_hash_index' => "CREATE INDEX idx_invoice_hash ON invoices(tenant_id, company_id, invoice_hash)",
|
||||
];
|
||||
|
||||
$success = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($migrations as $name => $sql) {
|
||||
try {
|
||||
$db->exec($sql);
|
||||
echo " ✅ {$name}\n";
|
||||
$success++;
|
||||
} catch (\PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
// Ignore "duplicate column" (1060), "duplicate key name" (1061), or "already exists" errors
|
||||
if (str_contains($msg, 'Duplicate column') || str_contains($msg, 'Duplicate key name') || str_contains($msg, 'already exists')) {
|
||||
echo " ⏭️ {$name} (already exists)\n";
|
||||
$skipped++;
|
||||
} else {
|
||||
echo " ❌ {$name}: {$msg}\n";
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n═══════════════════════════════════════════\n";
|
||||
echo " Migration Complete!\n";
|
||||
echo " ✅ Success: {$success} | ⏭️ Skipped: {$skipped} | ❌ Failed: {$failed}\n";
|
||||
echo "═══════════════════════════════════════════\n";
|
||||
|
||||
```
|
||||
|
||||
## File: `migrate_phase1.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Phase 1 Migration: Subscriptions & Quota System
|
||||
* Run: php scripts/migrate_phase1.php
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "═══════════════════════════════════════════\n";
|
||||
echo " مُصادَق — Phase 1 Migration\n";
|
||||
echo " Subscriptions & Quota System\n";
|
||||
echo "═══════════════════════════════════════════\n\n";
|
||||
|
||||
$migrations = [
|
||||
|
||||
// 1. Add deleted_at to companies
|
||||
'companies_soft_delete' => "ALTER TABLE companies ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL",
|
||||
|
||||
// 2. Add deleted_at to users
|
||||
'users_soft_delete' => "ALTER TABLE users ADD COLUMN deleted_at DATETIME NULL DEFAULT NULL",
|
||||
|
||||
// 3. Add email_hash to users (if missing)
|
||||
'users_email_hash' => "ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) NULL",
|
||||
|
||||
// 4. Create subscription_plans table
|
||||
'subscription_plans_table' => "
|
||||
CREATE TABLE IF NOT EXISTS subscription_plans (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
name_ar VARCHAR(100) NOT NULL,
|
||||
name_en VARCHAR(100) NOT NULL,
|
||||
max_companies INT NOT NULL DEFAULT 1,
|
||||
max_invoices_month INT NOT NULL DEFAULT 30,
|
||||
max_users INT NOT NULL DEFAULT 2,
|
||||
price_jod DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
ai_features BOOLEAN DEFAULT FALSE,
|
||||
jofotara_enabled BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INT DEFAULT 0,
|
||||
features_json JSON NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
",
|
||||
|
||||
// 4.5 Fix collation if table already exists
|
||||
'subscription_plans_collation_fix' => "ALTER TABLE subscription_plans CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
|
||||
// 5. Ensure subscriptions table exists with all needed columns
|
||||
'subscriptions_table' => "
|
||||
CREATE TABLE IF NOT EXISTS subscriptions (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NOT NULL UNIQUE,
|
||||
plan_id VARCHAR(20) NOT NULL DEFAULT 'free',
|
||||
max_companies INT NOT NULL DEFAULT 1,
|
||||
max_invoices_per_month INT NOT NULL DEFAULT 15,
|
||||
max_users INT NOT NULL DEFAULT 1,
|
||||
price_jod DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
invoices_used_this_month INT NOT NULL DEFAULT 0,
|
||||
status ENUM('active','past_due','cancelled','trial') DEFAULT 'trial',
|
||||
current_period_start DATETIME NULL,
|
||||
current_period_end DATETIME NULL,
|
||||
trial_ends_at DATETIME NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (plan_id) REFERENCES subscription_plans(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
",
|
||||
|
||||
// 5.5 Fix collation if table already exists
|
||||
'subscriptions_collation_fix' => "ALTER TABLE subscriptions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
|
||||
// 6. Add plan_id column to subscriptions if upgrading from old schema
|
||||
'subscriptions_plan_id' => "ALTER TABLE subscriptions ADD COLUMN plan_id VARCHAR(20) NOT NULL DEFAULT 'free'",
|
||||
|
||||
// 7. Add max_users column to subscriptions if missing
|
||||
'subscriptions_max_users' => "ALTER TABLE subscriptions ADD COLUMN max_users INT NOT NULL DEFAULT 1",
|
||||
|
||||
// 8. Add trial_ends_at to subscriptions if missing
|
||||
'subscriptions_trial' => "ALTER TABLE subscriptions ADD COLUMN trial_ends_at DATETIME NULL",
|
||||
|
||||
// 9. Index on subscriptions status
|
||||
'subscriptions_status_idx' => "CREATE INDEX idx_sub_status ON subscriptions(status)",
|
||||
];
|
||||
|
||||
$success = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($migrations as $name => $sql) {
|
||||
try {
|
||||
$db->exec($sql);
|
||||
echo " ✅ {$name}\n";
|
||||
$success++;
|
||||
} catch (\PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
// Ignore "duplicate column" (1060), "duplicate key name" (1061), or "already exists" errors
|
||||
if (str_contains($msg, 'Duplicate column') || str_contains($msg, 'Duplicate key name') || str_contains($msg, 'already exists')) {
|
||||
echo " ⏭️ {$name} (already exists)\n";
|
||||
$skipped++;
|
||||
} else {
|
||||
echo " ❌ {$name}: {$msg}\n";
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n───────────────────────────────────────────\n";
|
||||
|
||||
// Seed subscription plans
|
||||
echo "\n📦 Seeding subscription plans...\n";
|
||||
|
||||
$plans = [
|
||||
['free', 'مجانية', 'Free', 1, 15, 1, 0.00, 1, 1, 10],
|
||||
['basic', 'أساسية', 'Basic', 3, 100, 3, 15.00, 1, 1, 20],
|
||||
['office', 'مكتبية', 'Office', 10, 500, 10, 45.00, 1, 1, 30],
|
||||
['pro', 'احترافية', 'Pro', 25, 2000, 25, 99.00, 1, 1, 40],
|
||||
['enterprise', 'مؤسسية', 'Enterprise', 999, 99999, 999, 249.00, 1, 1, 50],
|
||||
];
|
||||
|
||||
$planStmt = $db->prepare("
|
||||
INSERT INTO subscription_plans (id, name_ar, name_en, max_companies, max_invoices_month, max_users, price_jod, ai_features, jofotara_enabled, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name_ar = VALUES(name_ar),
|
||||
name_en = VALUES(name_en),
|
||||
max_companies = VALUES(max_companies),
|
||||
max_invoices_month = VALUES(max_invoices_month),
|
||||
max_users = VALUES(max_users),
|
||||
price_jod = VALUES(price_jod),
|
||||
ai_features = VALUES(ai_features),
|
||||
jofotara_enabled = VALUES(jofotara_enabled),
|
||||
sort_order = VALUES(sort_order)
|
||||
");
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
$planStmt->execute($plan);
|
||||
echo " ✅ Plan: {$plan[0]} ({$plan[1]})\n";
|
||||
}
|
||||
|
||||
// Auto-assign 'free' plan to any tenant without a subscription
|
||||
echo "\n🔗 Auto-assigning free plan to tenants without subscriptions...\n";
|
||||
|
||||
$stmt = $db->query("
|
||||
SELECT t.id FROM tenants t
|
||||
LEFT JOIN subscriptions s ON s.tenant_id = t.id
|
||||
WHERE s.id IS NULL
|
||||
");
|
||||
$orphanTenants = $stmt->fetchAll();
|
||||
|
||||
if (!empty($orphanTenants)) {
|
||||
$insertSub = $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, trial_ends_at)
|
||||
VALUES (?, 'free', 1, 15, 1, 0.00, 'trial', NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), DATE_ADD(NOW(), INTERVAL 14 DAY))
|
||||
");
|
||||
foreach ($orphanTenants as $tenant) {
|
||||
try {
|
||||
$insertSub->execute([$tenant['id']]);
|
||||
echo " ✅ Assigned free plan to tenant: {$tenant['id']}\n";
|
||||
} catch (\Exception $e) {
|
||||
echo " ⚠️ Tenant {$tenant['id']}: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo " ℹ️ All tenants already have subscriptions.\n";
|
||||
}
|
||||
|
||||
echo "\n═══════════════════════════════════════════\n";
|
||||
echo " Migration Complete!\n";
|
||||
echo " ✅ Success: {$success} | ⏭️ Skipped: {$skipped} | ❌ Failed: {$failed}\n";
|
||||
echo "═══════════════════════════════════════════\n";
|
||||
|
||||
```
|
||||
|
||||
## File: `migrate.php`
|
||||
|
||||
```php
|
||||
@@ -371,7 +616,7 @@ try {
|
||||
// 2. Setup Super Admin details
|
||||
$adminEmail = 'admin@musadaq.app';
|
||||
$adminName = 'Hamza';
|
||||
$adminPassword = 'password123'; // Default password
|
||||
$adminPassword = env('SEED_ADMIN_PASSWORD', 'password123'); // Default for dev only
|
||||
|
||||
// Check if user already exists
|
||||
$emailHash = hash('sha256', strtolower($adminEmail));
|
||||
@@ -404,7 +649,7 @@ try {
|
||||
echo "[OK] Super Admin created successfully!\n";
|
||||
echo "----------------------------------------\n";
|
||||
echo "Email: $adminEmail\n";
|
||||
echo "Password: $adminPassword\n";
|
||||
echo "Password: [FROM ENV]\n";
|
||||
echo "Role: super_admin\n";
|
||||
echo "----------------------------------------\n";
|
||||
}
|
||||
@@ -419,6 +664,80 @@ try {
|
||||
|
||||
```
|
||||
|
||||
## File: `backfill_hashes.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Phase 2 Backfill: Generate hashes for existing invoices
|
||||
* Run: php scripts/backfill_hashes.php
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "═══════════════════════════════════════════\n";
|
||||
echo " مُصادَق — Backfill Invoice Hashes\n";
|
||||
echo "═══════════════════════════════════════════\n\n";
|
||||
|
||||
// 1. Fetch all invoices without a hash
|
||||
$stmt = $db->query("SELECT id, company_id, supplier_tin, invoice_number, invoice_date FROM invoices WHERE invoice_hash IS NULL");
|
||||
$invoices = $stmt->fetchAll();
|
||||
|
||||
if (empty($invoices)) {
|
||||
echo "✅ No invoices found needing a hash update.\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "📦 Found " . count($invoices) . " invoices to process...\n";
|
||||
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
// Decrypt supplier_tin if it's encrypted (assuming it is based on upload logic)
|
||||
$rawTin = Encryption::decrypt($inv['supplier_tin'] ?? '') ?: ($inv['supplier_tin'] ?? '');
|
||||
|
||||
$invoiceNum = $inv['invoice_number'] ?? '';
|
||||
$invoiceDate = $inv['invoice_date'] ?? '';
|
||||
|
||||
if (empty($rawTin) || empty($invoiceNum) || empty($invoiceDate)) {
|
||||
echo " ⚠️ Skipping invoice [{$inv['id']}]: Missing critical data for hash calculation.\n";
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate Hash
|
||||
$rawHashString = $inv['company_id'] . '_' . $rawTin . '_' . $invoiceNum . '_' . $invoiceDate;
|
||||
$invoiceHash = hash('sha256', strtolower($rawHashString));
|
||||
|
||||
try {
|
||||
$updateStmt = $db->prepare("UPDATE invoices SET invoice_hash = ? WHERE id = ?");
|
||||
$updateStmt->execute([$invoiceHash, $inv['id']]);
|
||||
$updated++;
|
||||
echo " ✅ Updated invoice [{$inv['id']}] | Hash: " . substr($invoiceHash, 0, 8) . "...\n";
|
||||
} catch (\PDOException $e) {
|
||||
if (str_contains($e->getMessage(), 'Duplicate entry')) {
|
||||
echo " 🚫 Duplicate hash found for invoice [{$inv['id']}]. Skipping update.\n";
|
||||
$skipped++;
|
||||
} else {
|
||||
echo " ❌ Failed to update [{$inv['id']}]: " . $e->getMessage() . "\n";
|
||||
$skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n═══════════════════════════════════════════\n";
|
||||
echo " Backfill Complete!\n";
|
||||
echo " ✅ Updated: {$updated} | ⏭️ Skipped/Failed: {$skipped}\n";
|
||||
echo "═══════════════════════════════════════════\n";
|
||||
|
||||
```
|
||||
|
||||
## File: `debug_data.php`
|
||||
|
||||
```php
|
||||
|
||||
Reference in New Issue
Block a user