Update: 2026-05-03 23:57:27
This commit is contained in:
@@ -17,7 +17,10 @@ $data = input();
|
|||||||
|
|
||||||
$errors = Validator::validate($data, [
|
$errors = Validator::validate($data, [
|
||||||
'name' => 'required',
|
'name' => 'required',
|
||||||
'email' => 'required|email'
|
'email' => 'required|email',
|
||||||
|
'manager_name' => 'required',
|
||||||
|
'manager_email' => 'required|email',
|
||||||
|
'manager_password' => 'required'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($errors) {
|
if ($errors) {
|
||||||
@@ -27,14 +30,51 @@ if ($errors) {
|
|||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $db->prepare("INSERT INTO tenants (name, email, phone, status, created_at) VALUES (?, ?, ?, 'active', NOW())");
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Generate Tenant UUID in PHP so we can use it immediately
|
||||||
|
$tenantId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Create Tenant
|
||||||
|
$stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
|
$tenantId,
|
||||||
$data['name'],
|
$data['name'],
|
||||||
$data['email'],
|
$data['email'],
|
||||||
$data['phone'] ?? null
|
$data['phone'] ?? null
|
||||||
]);
|
]);
|
||||||
|
|
||||||
json_success(null, 'تم إنشاء المكتب بنجاح');
|
// Generate User UUID
|
||||||
|
$userId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Encrypt sensitive user data
|
||||||
|
$encryptedName = \App\Core\Encryption::encrypt($data['manager_name']);
|
||||||
|
$encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']);
|
||||||
|
$emailHash = hash('sha256', strtolower($data['manager_email']));
|
||||||
|
|
||||||
|
// 2. Create Initial Manager (Admin) for this Tenant
|
||||||
|
$stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())");
|
||||||
|
$stmtUser->execute([
|
||||||
|
$userId,
|
||||||
|
$tenantId,
|
||||||
|
$encryptedName,
|
||||||
|
$encryptedEmail,
|
||||||
|
$emailHash,
|
||||||
|
password_hash($data['manager_password'], PASSWORD_DEFAULT)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
json_success(null, 'تم إنشاء المكتب ومدير المكتب بنجاح');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
json_error('حدث خطأ أثناء حفظ البيانات', 500);
|
$db->rollBack();
|
||||||
|
json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,22 @@ $encryptedName = Encryption::encrypt($data['name']);
|
|||||||
$encryptedEmail = Encryption::encrypt($data['email']);
|
$encryptedEmail = Encryption::encrypt($data['email']);
|
||||||
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
|
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
|
||||||
|
|
||||||
|
// 3. Determine Tenant ID
|
||||||
|
$tenantId = null;
|
||||||
|
if ($decoded['role'] === 'super_admin') {
|
||||||
|
if (empty($data['tenant_id'])) {
|
||||||
|
json_error('يجب اختيار المكتب المحاسبي', 422);
|
||||||
|
}
|
||||||
|
$tenantId = $data['tenant_id'];
|
||||||
|
} else {
|
||||||
|
$tenantId = $decoded['tenant_id'];
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Save to Database
|
// 4. Save to Database
|
||||||
try {
|
try {
|
||||||
$stmt = $db->prepare("INSERT INTO users (tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO users (tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$decoded['tenant_id'],
|
$tenantId,
|
||||||
$encryptedName,
|
$encryptedName,
|
||||||
$encryptedEmail,
|
$encryptedEmail,
|
||||||
$emailHash,
|
$emailHash,
|
||||||
|
|||||||
@@ -153,6 +153,15 @@
|
|||||||
<option value="admin">مدير نظام</option>
|
<option value="admin">مدير نظام</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div x-show="user?.role === 'super_admin'">
|
||||||
|
<label class="block text-xs text-gray-500 uppercase mb-1">تعيين لمكتب (للسوبر أدمن فقط)</label>
|
||||||
|
<select x-model="newUser.tenant_id" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||||
|
<option value="">-- اختر المكتب --</option>
|
||||||
|
<template x-for="t in tenants" :key="t.id">
|
||||||
|
<option :value="t.id" x-text="t.name"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="pt-4 flex gap-3">
|
<div class="pt-4 flex gap-3">
|
||||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded font-bold transition">حفظ المستخدم</button>
|
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded font-bold transition">حفظ المستخدم</button>
|
||||||
<button type="button" @click="showAddModal = false" class="px-6 py-3 border border-gray-800 rounded hover:bg-gray-800 transition">إلغاء</button>
|
<button type="button" @click="showAddModal = false" class="px-6 py-3 border border-gray-800 rounded hover:bg-gray-800 transition">إلغاء</button>
|
||||||
@@ -178,6 +187,20 @@
|
|||||||
<label class="block text-xs text-gray-500 uppercase mb-1">الهاتف</label>
|
<label class="block text-xs text-gray-500 uppercase mb-1">الهاتف</label>
|
||||||
<input type="text" x-model="newTenant.phone" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
<input type="text" x-model="newTenant.phone" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||||
</div>
|
</div>
|
||||||
|
<hr class="border-gray-800 my-4">
|
||||||
|
<h4 class="text-sm font-bold text-gray-400">معلومات مدير المكتب (Admin)</h4>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 uppercase mb-1">اسم المدير</label>
|
||||||
|
<input type="text" x-model="newTenant.manager_name" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 uppercase mb-1">بريد المدير (لتسجيل الدخول)</label>
|
||||||
|
<input type="email" x-model="newTenant.manager_email" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 uppercase mb-1">كلمة المرور</label>
|
||||||
|
<input type="password" x-model="newTenant.manager_password" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||||
|
</div>
|
||||||
<div class="flex gap-3 pt-4">
|
<div class="flex gap-3 pt-4">
|
||||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 p-3 rounded font-bold transition">حفظ</button>
|
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 p-3 rounded font-bold transition">حفظ</button>
|
||||||
<button type="button" @click="showAddTenantModal = false" class="flex-1 bg-gray-800 hover:bg-gray-700 p-3 rounded transition">إلغاء</button>
|
<button type="button" @click="showAddTenantModal = false" class="flex-1 bg-gray-800 hover:bg-gray-700 p-3 rounded transition">إلغاء</button>
|
||||||
@@ -223,17 +246,22 @@
|
|||||||
page: 'dashboard',
|
page: 'dashboard',
|
||||||
users: [],
|
users: [],
|
||||||
companies: [],
|
companies: [],
|
||||||
|
tenants: [],
|
||||||
stats: { total: 0, pending: 0, approved: 0 },
|
stats: { total: 0, pending: 0, approved: 0 },
|
||||||
showAddModal: false,
|
showAddModal: false,
|
||||||
showAddCompanyModal: false,
|
showAddCompanyModal: false,
|
||||||
newUser: { name: '', email: '', password: '', role: 'employee' },
|
newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
|
||||||
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '' },
|
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '' },
|
||||||
|
newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' },
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (!this.user) window.location.href = '/login.php';
|
if (!this.user) window.location.href = '/login.php';
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
this.loadStats();
|
this.loadStats();
|
||||||
this.loadCompanies();
|
this.loadCompanies();
|
||||||
|
if (this.user.role === 'super_admin') {
|
||||||
|
this.loadTenants();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
@@ -277,7 +305,7 @@
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
this.showAddModal = false;
|
this.showAddModal = false;
|
||||||
this.newUser = { name: '', email: '', password: '', role: 'employee' };
|
this.newUser = { name: '', email: '', password: '', role: 'employee', tenant_id: '' };
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
alert('تم إضافة المستخدم بنجاح');
|
alert('تم إضافة المستخدم بنجاح');
|
||||||
} else {
|
} else {
|
||||||
@@ -305,6 +333,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadTenants() {
|
||||||
|
const res = await fetch('/api/v1/tenants', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.success) this.tenants = json.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTenant() {
|
||||||
|
const res = await fetch('/api/v1/tenants/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.newTenant)
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.success) {
|
||||||
|
this.showAddTenantModal = false;
|
||||||
|
this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' };
|
||||||
|
this.loadTenants();
|
||||||
|
alert('تم إنشاء المكتب وتعيين المدير بنجاح!');
|
||||||
|
} else {
|
||||||
|
alert(json.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
window.location.href = '/login.php';
|
window.location.href = '/login.php';
|
||||||
|
|||||||
311
scripts/PROJECT_DOCUMENTATION.md
Normal file
311
scripts/PROJECT_DOCUMENTATION.md
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# Musadaq Project Documentation
|
||||||
|
|
||||||
|
This file contains the complete source code of the project (excluding dependencies and sensitive data).
|
||||||
|
|
||||||
|
## File: `fix_data.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Data Reset & Fix Script
|
||||||
|
* Clears corrupted company/assignment data but keeps Users and Tenants.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "--- Starting Data Reset ---\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Clear corrupted data tables
|
||||||
|
$db->exec("SET FOREIGN_KEY_CHECKS = 0");
|
||||||
|
$db->exec("TRUNCATE TABLE user_company_assignments");
|
||||||
|
$db->exec("TRUNCATE TABLE invoices");
|
||||||
|
$db->exec("TRUNCATE TABLE companies");
|
||||||
|
$db->exec("SET FOREIGN_KEY_CHECKS = 1");
|
||||||
|
echo "[OK] Cleared companies, invoices, and assignments.\n";
|
||||||
|
|
||||||
|
// 2. Ensure Super Admin does not have a tenant_id (if your schema allows NULL, else set to empty string)
|
||||||
|
// Actually, schema.sql says tenant_id CHAR(36) NOT NULL.
|
||||||
|
// This is a flaw in schema.sql for Super Admins. We will leave users alone for now.
|
||||||
|
|
||||||
|
// 3. Fix the admin's tenant_id to match the first available tenant
|
||||||
|
$stmt = $db->query("SELECT id FROM tenants LIMIT 1");
|
||||||
|
$tenantId = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($tenantId) {
|
||||||
|
$db->exec("UPDATE users SET tenant_id = '$tenantId' WHERE role != 'super_admin'");
|
||||||
|
echo "[OK] Linked all non-super-admin users to Tenant ID: $tenantId\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
echo "--- Reset Complete ---\n";
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
echo "[ERROR] Reset failed: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## File: `schema.sql`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
-- مُصادَق — Database Schema v1.0
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- Tenants (Accounting Offices)
|
||||||
|
CREATE TABLE tenants (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
phone VARCHAR(20),
|
||||||
|
status ENUM('active', 'suspended', 'trial') DEFAULT 'trial',
|
||||||
|
trial_ends_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Users
|
||||||
|
CREATE TABLE users (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('super_admin','admin','accountant','viewer') NOT NULL,
|
||||||
|
company_id CHAR(36) NULL, -- assigned company for accountant
|
||||||
|
refresh_token_hash VARCHAR(255) NULL,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
last_login_at DATETIME NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uq_tenant_email (tenant_id, email),
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- API Keys (for external integrations / mobile scanner)
|
||||||
|
CREATE TABLE api_keys (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) NOT NULL,
|
||||||
|
user_id CHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
public_key VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
secret_hash VARCHAR(255) NOT NULL, -- bcrypt hash of secret
|
||||||
|
last_used_at DATETIME NULL,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Companies
|
||||||
|
CREATE TABLE companies (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
name_en VARCHAR(255) NULL,
|
||||||
|
tax_identification_number VARCHAR(20) NOT NULL,
|
||||||
|
address TEXT NULL,
|
||||||
|
jofotara_client_id_encrypted TEXT NULL,
|
||||||
|
jofotara_secret_key_encrypted TEXT NULL,
|
||||||
|
jofotara_income_source_sequence VARCHAR(50) NULL,
|
||||||
|
certificate_path VARCHAR(255) NULL,
|
||||||
|
certificate_password_encrypted VARCHAR(500) NULL,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_name (name),
|
||||||
|
INDEX idx_tin (tax_identification_number),
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Subscriptions
|
||||||
|
CREATE TABLE subscriptions (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) NOT NULL UNIQUE,
|
||||||
|
plan ENUM('basic','office','pro','enterprise') NOT NULL DEFAULT 'basic',
|
||||||
|
max_companies INT NOT NULL DEFAULT 5,
|
||||||
|
max_invoices_per_month INT NOT NULL DEFAULT 100,
|
||||||
|
price_jod DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
invoices_used_this_month INT NOT NULL DEFAULT 0,
|
||||||
|
status ENUM('active','past_due','cancelled') DEFAULT 'active',
|
||||||
|
current_period_start DATETIME NULL,
|
||||||
|
current_period_end 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
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Invoices
|
||||||
|
CREATE TABLE invoices (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) NOT NULL,
|
||||||
|
company_id CHAR(36) NOT NULL,
|
||||||
|
invoice_number VARCHAR(100) NULL,
|
||||||
|
invoice_date DATE NULL,
|
||||||
|
invoice_type ENUM('cash','credit') DEFAULT 'cash',
|
||||||
|
ubl_type_code CHAR(3) DEFAULT '388',
|
||||||
|
payment_method_code CHAR(3) DEFAULT '013',
|
||||||
|
supplier_tin VARCHAR(20) NULL,
|
||||||
|
supplier_name VARCHAR(255) NULL,
|
||||||
|
supplier_address TEXT NULL,
|
||||||
|
buyer_tin VARCHAR(20) NULL,
|
||||||
|
buyer_national_id VARCHAR(20) NULL,
|
||||||
|
buyer_name VARCHAR(255) NULL,
|
||||||
|
subtotal DECIMAL(15,3) DEFAULT 0,
|
||||||
|
discount_total DECIMAL(15,3) DEFAULT 0,
|
||||||
|
tax_amount DECIMAL(15,3) DEFAULT 0,
|
||||||
|
grand_total DECIMAL(15,3) DEFAULT 0,
|
||||||
|
currency_code CHAR(3) DEFAULT 'JOD',
|
||||||
|
status ENUM('uploaded','extracting','extracted','validated','validation_failed','submitting','approved','rejected') DEFAULT 'uploaded',
|
||||||
|
original_file_path TEXT NULL,
|
||||||
|
invoice_category VARCHAR(20) DEFAULT 'simplified',
|
||||||
|
validation_errors JSON NULL,
|
||||||
|
qr_code TEXT NULL,
|
||||||
|
ai_confidence_score DECIMAL(4,3) NULL,
|
||||||
|
ai_prompt_tokens INT DEFAULT 0,
|
||||||
|
ai_completion_tokens INT DEFAULT 0,
|
||||||
|
ai_total_cost DECIMAL(10,6) DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_tenant (tenant_id),
|
||||||
|
INDEX idx_company (company_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Invoice Lines
|
||||||
|
CREATE TABLE invoice_lines (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
invoice_id CHAR(36) NOT NULL,
|
||||||
|
line_number INT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
quantity DECIMAL(15,3) NOT NULL,
|
||||||
|
unit_price DECIMAL(15,3) NOT NULL,
|
||||||
|
discount DECIMAL(15,3) DEFAULT 0,
|
||||||
|
tax_rate DECIMAL(5,4) NOT NULL,
|
||||||
|
line_total DECIMAL(15,3) NOT NULL,
|
||||||
|
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Audit Logs
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) NULL,
|
||||||
|
user_id CHAR(36) NULL,
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
entity_type VARCHAR(50) NULL,
|
||||||
|
entity_id CHAR(36) NULL,
|
||||||
|
old_data JSON NULL,
|
||||||
|
new_data JSON NULL,
|
||||||
|
ip_address VARCHAR(45) NULL,
|
||||||
|
user_agent VARCHAR(500) NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_tenant (tenant_id),
|
||||||
|
INDEX idx_action (action),
|
||||||
|
INDEX idx_created (created_at)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## File: `migrate.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Advanced Migration Script: Schema Update + Data Encryption
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
use App\Core\Encryption;
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "--- Starting Security Migration ---\n";
|
||||||
|
|
||||||
|
// 1. Add email_hash column if it doesn't exist
|
||||||
|
try {
|
||||||
|
$db->exec("ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) AFTER email, ADD INDEX (email_hash)");
|
||||||
|
echo "[OK] Added email_hash column and index.\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "[SKIP] email_hash column might already exist.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch all users to encrypt their data
|
||||||
|
$stmt = $db->query("SELECT id, name, email FROM users");
|
||||||
|
$users = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo "Found " . count($users) . " users. Starting encryption...\n";
|
||||||
|
|
||||||
|
$updateStmt = $db->prepare("UPDATE users SET name = ?, email = ?, email_hash = ? WHERE id = ?");
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
// Check if data is already encrypted (to avoid double encryption)
|
||||||
|
$isAlreadyEncrypted = Encryption::decrypt($user['email']) !== false;
|
||||||
|
|
||||||
|
if ($isAlreadyEncrypted) {
|
||||||
|
echo "User ID {$user['id']} is already encrypted. Skipping.\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt Name
|
||||||
|
$encryptedName = Encryption::encrypt($user['name']);
|
||||||
|
|
||||||
|
// Encrypt Email
|
||||||
|
$encryptedEmail = Encryption::encrypt($user['email']);
|
||||||
|
|
||||||
|
// Generate Hash for lookup
|
||||||
|
$emailHash = hash('sha256', strtolower($user['email']));
|
||||||
|
|
||||||
|
$updateStmt->execute([
|
||||||
|
$encryptedName,
|
||||||
|
$encryptedEmail,
|
||||||
|
$emailHash,
|
||||||
|
$user['id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "User ID {$user['id']} migrated successfully.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create user_company_assignments table
|
||||||
|
try {
|
||||||
|
$db->exec("CREATE TABLE IF NOT EXISTS user_company_assignments (
|
||||||
|
id INT AUTO_INCREMENT,
|
||||||
|
user_id VARCHAR(100) NOT NULL,
|
||||||
|
company_id VARCHAR(100) NOT NULL,
|
||||||
|
assigned_by VARCHAR(100) NOT NULL,
|
||||||
|
assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_user_company (user_id, company_id),
|
||||||
|
CONSTRAINT fk_uca_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_uca_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_uca_admin FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
echo "[OK] User_company_assignments table created.\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "[SKIP] user_company_assignments table: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Update invoices table to include uploaded_by
|
||||||
|
try {
|
||||||
|
$db->exec("ALTER TABLE invoices ADD COLUMN uploaded_by VARCHAR(100) NULL AFTER status");
|
||||||
|
$db->exec("ALTER TABLE invoices ADD CONSTRAINT fk_inv_uploader FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL");
|
||||||
|
echo "[OK] Updated invoices table with uploaded_by tracker.\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "[SKIP] invoices table update: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "--- Migration Complete ---\n";
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
Reference in New Issue
Block a user