# Musadaq Project Documentation This file contains the complete source code of the project (excluding dependencies and sensitive data). ## File: `fix_data.php` ```php 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 TEXT NULL, supplier_name TEXT NULL, supplier_address TEXT NULL, buyer_tin TEXT NULL, buyer_national_id TEXT NULL, buyer_name TEXT 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('extracted', 'approved', 'rejected') DEFAULT 'extracted', jofotara_uuid VARCHAR(255) NULL, qr_code TEXT NULL, original_file_path TEXT NULL, invoice_category VARCHAR(20) DEFAULT 'simplified', validation_errors JSON 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, deleted_at DATETIME NULL, 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, FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- JoFotara Submissions (Audit Trail) CREATE TABLE jofotara_submissions ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), invoice_id CHAR(36) NOT NULL, company_id CHAR(36) NOT NULL, tenant_id CHAR(36) NOT NULL, xml_payload LONGTEXT NULL, xml_hash VARCHAR(64) NULL, jofotara_uuid VARCHAR(255) NULL, qr_code_raw TEXT NULL, response_code VARCHAR(20) NULL, response_body JSON NULL, status ENUM('pending','submitted','accepted','rejected','error') DEFAULT 'pending', error_message TEXT NULL, retry_count TINYINT DEFAULT 0, submitted_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE, FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 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 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"; ``` ## File: `seed_super_admin.php` ```php beginTransaction(); // 1. We must create a "System Tenant" for the Super Admin to satisfy the Foreign Key constraint $systemTenantId = '00000000-0000-0000-0000-000000000000'; // Check if system tenant exists $stmt = $db->prepare("SELECT id FROM tenants WHERE id = ?"); $stmt->execute([$systemTenantId]); if (!$stmt->fetch()) { $stmt = $db->prepare("INSERT INTO tenants (id, name, email, status, created_at) VALUES (?, 'System Administration', 'system@musadaq.com', 'active', NOW())"); $stmt->execute([$systemTenantId]); echo "[OK] System Tenant created.\n"; } // 2. Setup Super Admin details $adminEmail = 'admin@musadaq.app'; $adminName = 'Hamza'; $adminPassword = 'password123'; // Default password // Check if user already exists $emailHash = hash('sha256', strtolower($adminEmail)); $stmt = $db->prepare("SELECT id FROM users WHERE email_hash = ?"); $stmt->execute([$emailHash]); if ($stmt->fetch()) { echo "[INFO] Super Admin already exists with this email.\n"; } else { $adminId = 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) ); $encryptedName = Encryption::encrypt($adminName); $encryptedEmail = Encryption::encrypt($adminEmail); $passwordHash = password_hash($adminPassword, PASSWORD_DEFAULT); $stmt = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, 'super_admin', 1, NOW())"); $stmt->execute([ $adminId, $systemTenantId, $encryptedName, $encryptedEmail, $emailHash, $passwordHash ]); echo "[OK] Super Admin created successfully!\n"; echo "----------------------------------------\n"; echo "Email: $adminEmail\n"; echo "Password: $adminPassword\n"; echo "Role: super_admin\n"; echo "----------------------------------------\n"; } $db->commit(); echo "--- Seeding Complete ---\n"; } catch (\Exception $e) { $db->rollBack(); echo "[ERROR] Seeding failed: " . $e->getMessage() . "\n"; } ``` ## File: `debug_data.php` ```php query("SELECT * FROM tenants"); print_r($stmt->fetchAll(PDO::FETCH_ASSOC)); echo "\n--- USERS ---\n"; $stmt = $db->query("SELECT u.id, u.name, u.role, u.tenant_id, t.name as tenant_name FROM users u LEFT JOIN tenants t ON u.tenant_id = t.id"); print_r($stmt->fetchAll(PDO::FETCH_ASSOC)); echo "\n--- COMPANIES ---\n"; $stmt = $db->query("SELECT * FROM companies"); print_r($stmt->fetchAll(PDO::FETCH_ASSOC)); ```