# 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: `update_phone.php` ```php prepare("UPDATE users SET phone = ?, phone_hash = ? WHERE id = ?"); $stmt->execute([$encryptedPhone, $phoneHash, $id]); $identifier = "ID $id"; } else { // Note: Searching by encrypted email will likely fail due to IV randomness. Use ID. $stmt = $db->prepare("UPDATE users SET phone = ?, phone_hash = ? WHERE email = ?"); $stmt->execute([$encryptedPhone, $phoneHash, $email]); $identifier = "email $email"; } if ($stmt->rowCount() > 0) { echo "✅ Success! Phone updated for $identifier\n"; echo " Encrypted: $encryptedPhone\n"; echo " Hash: $phoneHash\n"; } else { echo "❌ Failed. User with $identifier not found or no changes made.\n"; } } catch (Exception $e) { echo "❌ Error: " . $e->getMessage() . "\n"; } ``` ## File: `migrate_payments.php` ```php exec(" CREATE TABLE IF NOT EXISTS subscription_plans ( id VARCHAR(50) PRIMARY KEY, name_ar VARCHAR(255) NOT NULL, name_en VARCHAR(255) NOT NULL, max_companies INT NOT NULL, max_invoices_month INT NOT NULL, max_users INT NOT NULL, price_jod DECIMAL(10,3) NOT NULL, ai_features BOOLEAN DEFAULT TRUE, jofotara_enabled BOOLEAN DEFAULT TRUE, sort_order INT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); // 2. Insert initial plans echo "Inserting initial plans...\n"; $plans = require __DIR__ . '/../app/config/plans.php'; $stmt = $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), price_jod = VALUES(price_jod), max_companies = VALUES(max_companies), max_invoices_month = VALUES(max_invoices_month) "); $order = 0; foreach ($plans as $id => $plan) { $stmt->execute([ $id, $plan['name_ar'], $plan['name_en'], $plan['max_companies'], $plan['max_invoices_month'], $plan['max_users'], $plan['price_jod'], $plan['ai_features'] ? 1 : 0, $plan['jofotara_enabled'] ? 1 : 0, $order++ ]); } // 3. Create payment_requests table echo "Creating payment_requests table...\n"; $db->exec(" CREATE TABLE IF NOT EXISTS payment_requests ( id CHAR(36) PRIMARY KEY, tenant_id CHAR(36) NOT NULL, user_id CHAR(36) NOT NULL, plan_id VARCHAR(50) NOT NULL, amount_jod DECIMAL(10,3) NOT NULL, internal_reference VARCHAR(50) UNIQUE NOT NULL, cliq_alias VARCHAR(100) NOT NULL, payer_name VARCHAR(255) DEFAULT NULL, bank_reference VARCHAR(100) DEFAULT NULL, status ENUM('pending','uploaded','verified','approved','rejected') DEFAULT 'pending', admin_notes TEXT DEFAULT NULL, verified_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (tenant_id) REFERENCES tenants(id), FOREIGN KEY (user_id) REFERENCES users(id), INDEX idx_status (status), INDEX idx_bank_ref (bank_reference) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); // 4. Create bank_transactions table echo "Creating bank_transactions table...\n"; $db->exec(" CREATE TABLE IF NOT EXISTS bank_transactions ( id INT AUTO_INCREMENT PRIMARY KEY, bank_reference VARCHAR(100) UNIQUE NOT NULL, amount DECIMAL(10,3) NOT NULL, sender_name VARCHAR(255) DEFAULT NULL, raw_message TEXT NOT NULL, is_claimed BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_ref (bank_reference) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); // 5. Update subscriptions table if needed echo "Updating subscriptions table schema...\n"; // Check if column plan_id exists, if not add it $cols = $db->query("SHOW COLUMNS FROM subscriptions")->fetchAll(PDO::FETCH_COLUMN); if (!in_array('plan_id', $cols)) { $db->exec("ALTER TABLE subscriptions ADD COLUMN plan_id VARCHAR(50) AFTER tenant_id"); $db->exec("ALTER TABLE subscriptions MODIFY COLUMN plan ENUM('free','basic','office','pro','enterprise') DEFAULT 'free'"); } if (!in_array('max_users', $cols)) { $db->exec("ALTER TABLE subscriptions ADD COLUMN max_users INT NOT NULL DEFAULT 1 AFTER max_invoices_per_month"); } echo "Migration completed successfully!\n"; } catch (\Throwable $e) { echo "Migration failed: " . $e->getMessage() . "\n"; exit(1); } ``` ## File: `list_users.php` ```php query("SELECT id, email, name FROM users")->fetchAll(); echo "ID | Email | Name\n"; echo "----------------------\n"; foreach ($users as $user) { $email = App\Core\Encryption::decrypt($user['email']) ?: $user['email']; $name = App\Core\Encryption::decrypt($user['name']) ?: $user['name']; echo "{$user['id']} | $email | $name\n"; } ``` ## File: `reset_queue.sql` ```sql -- Reset failed queue items so the cron worker retries them UPDATE invoice_processing_queue SET status = 'pending', error_message = NULL WHERE status = 'failed'; -- Verify current queue state SELECT id, batch_id, status, error_message, created_at FROM invoice_processing_queue ORDER BY created_at; ``` ## 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, phone VARCHAR(255), 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_phase2.php` ```php "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 "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: `debug_collation.php` ```php query("SHOW FULL COLUMNS FROM $table WHERE Field = 'id'"); $col = $stmt->fetch(PDO::FETCH_ASSOC); print_r($col); echo "--------------------------\n"; } ``` ## File: `create_ai_usage_table.sql` ```sql -- AI Usage Log — Token tracking for cost analysis CREATE TABLE IF NOT EXISTS ai_usage_log ( id CHAR(36) PRIMARY KEY, input_tokens INT UNSIGNED NOT NULL DEFAULT 0, output_tokens INT UNSIGNED NOT NULL DEFAULT 0, total_tokens INT UNSIGNED NOT NULL DEFAULT 0, cost_usd DECIMAL(12, 8) NOT NULL DEFAULT 0, cost_jod DECIMAL(12, 8) NOT NULL DEFAULT 0, model VARCHAR(50) NOT NULL DEFAULT 'gemini-flash-lite', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_created (created_at), INDEX idx_model (model) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## 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 = env('SEED_ADMIN_PASSWORD', 'password123'); // Default for dev only // 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: [FROM ENV]\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: `phase1_migration.sql` ```sql -- ════════════════════════════════════════════════════════════ -- مُصادَق — Phase 1: AI Usage Tracking + Notifications -- ════════════════════════════════════════════════════════════ -- AI Usage Log (tracks every AI request) CREATE TABLE IF NOT EXISTS ai_usage_log ( id INT AUTO_INCREMENT PRIMARY KEY, tenant_id CHAR(36) NOT NULL, user_id CHAR(36) NULL, company_id CHAR(36) NULL, action_type ENUM('invoice_extraction','voice_transcribe','voice_intent','report_generation','chatbot') NOT NULL, model_name VARCHAR(50) NOT NULL, prompt_tokens INT DEFAULT 0, completion_tokens INT DEFAULT 0, total_tokens INT DEFAULT 0, estimated_cost DECIMAL(10,6) DEFAULT 0, request_metadata JSON NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_tenant_date (tenant_id, created_at), INDEX idx_action (action_type), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Notifications CREATE TABLE IF NOT EXISTS notifications ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), tenant_id CHAR(36) NOT NULL, user_id CHAR(36) NULL, type ENUM('invoice_processed','invoice_rejected','quota_warning','month_end','system','achievement') NOT NULL, title VARCHAR(255) NOT NULL, body TEXT NOT NULL, is_read BOOLEAN DEFAULT FALSE, metadata JSON NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_read (user_id, is_read), INDEX idx_tenant (tenant_id), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Referral Codes (Phase 2 prep) CREATE TABLE IF NOT EXISTS referral_codes ( id INT AUTO_INCREMENT PRIMARY KEY, user_id CHAR(36) NOT NULL, code VARCHAR(20) NOT NULL UNIQUE, uses_count INT DEFAULT 0, max_uses INT DEFAULT 50, reward_months INT DEFAULT 1, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Referral Uses (Phase 2 prep) CREATE TABLE IF NOT EXISTS referral_uses ( id INT AUTO_INCREMENT PRIMARY KEY, code_id INT NOT NULL, referred_tenant_id CHAR(36) NOT NULL, reward_applied BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (code_id) REFERENCES referral_codes(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- User Achievements (Phase 2 prep) CREATE TABLE IF NOT EXISTS user_achievements ( id INT AUTO_INCREMENT PRIMARY KEY, user_id CHAR(36) NOT NULL, achievement_code VARCHAR(50) NOT NULL, points INT NOT NULL DEFAULT 0, earned_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY uq_user_achievement (user_id, achievement_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## File: `update_pricing.sql` ```sql -- Restore original Musadaq subscription pricing -- Premium pricing justified by AI extraction + JoFotara + mobile app UPDATE subscription_plans SET name_ar = 'مجانية', name_en = 'Free', max_companies = 1, max_invoices_month = 15, max_users = 1, price_jod = 0.00, jofotara_enabled = 1 WHERE id = 'free'; UPDATE subscription_plans SET name_ar = 'أساسية', name_en = 'Basic', max_companies = 3, max_invoices_month = 100, max_users = 3, price_jod = 15.00, jofotara_enabled = 1 WHERE id = 'basic'; UPDATE subscription_plans SET name_ar = 'مكتبية', name_en = 'Office', max_companies = 10, max_invoices_month = 500, max_users = 10, price_jod = 45.00, jofotara_enabled = 1 WHERE id = 'office'; UPDATE subscription_plans SET name_ar = 'احترافية', name_en = 'Pro', max_companies = 25, max_invoices_month = 2000, max_users = 25, price_jod = 99.00, jofotara_enabled = 1 WHERE id = 'pro'; UPDATE subscription_plans SET name_ar = 'مؤسسية', name_en = 'Enterprise', max_companies = 999, max_invoices_month = 99999, max_users = 999, price_jod = 249.00, jofotara_enabled = 1 WHERE id = 'enterprise'; ``` ## File: `backfill_hashes.php` ```php 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: `migrate_phase3_mobile.php` ```php query("SHOW FULL COLUMNS FROM users WHERE Field = 'id'"); $userCol = $stmt->fetch(PDO::FETCH_ASSOC); $charsetCollation = ""; if ($userCol && !empty($userCol['Collation'])) { $collation = $userCol['Collation']; list($charset) = explode('_', $collation); $charsetCollation = "CHARACTER SET {$charset} COLLATE {$collation}"; } $migrations = [ // ─── 1. User Device Management ───────────────────────── 'create_user_devices' => " CREATE TABLE IF NOT EXISTS user_devices ( id CHAR(36) {$charsetCollation} PRIMARY KEY DEFAULT (UUID()), user_id CHAR(36) {$charsetCollation} NOT NULL, device_fingerprint VARCHAR(64) NOT NULL, device_name VARCHAR(100) NULL, platform ENUM('android','ios','web') NOT NULL DEFAULT 'android', app_version VARCHAR(20) NULL, push_token TEXT NULL, device_secret VARCHAR(128) NULL, is_trusted BOOLEAN DEFAULT FALSE, last_seen_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, UNIQUE KEY uq_user_device (user_id, device_fingerprint), INDEX idx_device_fingerprint (device_fingerprint) ) ", // ─── 2. Users table: Add phone + mobile fields ───────── 'add_users_phone' => "ALTER TABLE users ADD COLUMN phone VARCHAR(255) NULL AFTER email", 'add_users_phone_hash' => "ALTER TABLE users ADD COLUMN phone_hash VARCHAR(64) NULL AFTER phone", 'add_users_pin_hash' => "ALTER TABLE users ADD COLUMN pin_hash VARCHAR(255) NULL AFTER password_hash", 'add_users_biometric' => "ALTER TABLE users ADD COLUMN biometric_enabled BOOLEAN DEFAULT FALSE AFTER pin_hash", 'add_users_phone_index' => "CREATE INDEX idx_phone_hash ON users(phone_hash)", // ─── 3. Invoice Batches (Mobile Scanner) ─────────────── 'create_invoice_batches' => " CREATE TABLE IF NOT EXISTS invoice_batches ( id CHAR(36) {$charsetCollation} PRIMARY KEY, tenant_id CHAR(36) {$charsetCollation} NOT NULL, company_id CHAR(36) {$charsetCollation} NOT NULL, uploaded_by CHAR(36) {$charsetCollation} NULL, total_images INT NOT NULL DEFAULT 0, processed_images INT NOT NULL DEFAULT 0, failed_images INT NOT NULL DEFAULT 0, status ENUM('uploading','processing','done','partial_fail','failed') DEFAULT 'uploading', source ENUM('mobile_scan','web_upload','whatsapp') DEFAULT 'mobile_scan', pdf_path VARCHAR(500) NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, completed_at DATETIME NULL, INDEX idx_tenant_status (tenant_id, status), INDEX idx_company (company_id), INDEX idx_uploaded_by (uploaded_by), 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 ) ", // ─── 4. Invoice Processing Queue ─────────────────────── 'create_processing_queue' => " CREATE TABLE IF NOT EXISTS invoice_processing_queue ( id INT AUTO_INCREMENT PRIMARY KEY, batch_id CHAR(36) {$charsetCollation} NOT NULL, invoice_id CHAR(36) {$charsetCollation} NULL, tenant_id CHAR(36) {$charsetCollation} NOT NULL, company_id CHAR(36) {$charsetCollation} NOT NULL, image_path VARCHAR(500) NOT NULL, image_order INT NOT NULL DEFAULT 0, status ENUM('pending','processing','done','failed') DEFAULT 'pending', attempts INT DEFAULT 0, max_attempts INT DEFAULT 3, error_message TEXT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, processed_at DATETIME NULL, INDEX idx_status_tenant (status, tenant_id), INDEX idx_batch (batch_id), INDEX idx_pending (status, attempts) ) ", // ─── 5. Add batch_id to invoices table ───────────────── 'add_invoices_batch_id' => "ALTER TABLE invoices ADD COLUMN batch_id CHAR(36) NULL AFTER company_id", 'add_invoices_batch_index' => "CREATE INDEX idx_batch_id ON invoices(batch_id)", // ─── 6. Notifications Table ──────────────────────────── 'create_notifications' => " CREATE TABLE IF NOT EXISTS notifications ( id CHAR(36) {$charsetCollation} PRIMARY KEY DEFAULT (UUID()), tenant_id CHAR(36) {$charsetCollation} NOT NULL, user_id CHAR(36) {$charsetCollation} NULL, type VARCHAR(50) NOT NULL, title VARCHAR(255) NOT NULL, body TEXT NULL, data JSON NULL, is_read BOOLEAN DEFAULT FALSE, read_at DATETIME NULL, push_sent BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_unread (user_id, is_read), INDEX idx_tenant (tenant_id), INDEX idx_type (type), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ", ]; $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(); 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: `deploy_production.sh` ```sh #!/bin/bash # ───────────────────────────────────────────────────── # Musadaq Production Deployment Script # Run this on the production server after syncing files # ───────────────────────────────────────────────────── set -e echo "═══════════════════════════════════════════════" echo " مُصادَق — Production Deployment Script" echo "═══════════════════════════════════════════════" # 1. Install PHP dependencies echo "" echo "▶ Step 1: Installing Composer dependencies..." cd /home/musadaq/htdocs/musadaq.intaleqapp.com composer install --no-dev --optimize-autoloader # 2. Ensure storage directories exist echo "" echo "▶ Step 2: Creating storage directories..." mkdir -p storage/invoices mkdir -p storage/logs mkdir -p storage/exports mkdir -p storage/temp chmod -R 775 storage/ # 3. Set up the Cron Job for AI Queue Worker echo "" echo "▶ Step 3: Setting up Cron Job for AI Worker..." echo "" echo " Run: crontab -e" echo " Add this line:" echo "" echo " * * * * * /usr/bin/php /home/musadaq/htdocs/musadaq.intaleqapp.com/app/cron/process_batches.php >> /home/musadaq/htdocs/musadaq.intaleqapp.com/storage/logs/cron.log 2>&1" echo "" echo " This runs the AI Queue Worker every minute." echo " The worker has its own lock file to prevent duplicates." echo "" # 4. Verify environment variables echo "▶ Step 4: Checking .env configuration..." if [ -f .env ]; then echo " ✅ .env file found" # Check critical keys grep -q "GEMINI_API_KEY" .env && echo " ✅ GEMINI_API_KEY set" || echo " ❌ GEMINI_API_KEY missing!" grep -q "DB_HOST" .env && echo " ✅ DB_HOST set" || echo " ❌ DB_HOST missing!" grep -q "ENCRYPTION_KEY" .env && echo " ✅ ENCRYPTION_KEY set" || echo " ❌ ENCRYPTION_KEY missing!" grep -q "JWT_SECRET" .env && echo " ✅ JWT_SECRET set" || echo " ❌ JWT_SECRET missing!" grep -q "FCM_SERVER_KEY\|FIREBASE" .env && echo " ✅ Firebase key set" || echo " ⚠️ Firebase key missing (push notifications won't work)" else echo " ❌ .env file not found! Copy .env.example and configure it." fi echo "" echo "═══════════════════════════════════════════════" echo " ✅ Deployment Complete!" echo "" echo " Next steps:" echo " 1. Add the Cron Job (shown above)" echo " 2. Test the API: curl https://musadaq.intaleqapp.com/api/v1/auth/login" echo " 3. Monitor logs: tail -f storage/logs/cron.log" echo "═══════════════════════════════════════════════" ``` ## File: `create_referral_tables.sql` ```sql -- Referral System Tables CREATE TABLE IF NOT EXISTS referral_codes ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, tenant_id CHAR(36) NOT NULL, code VARCHAR(20) NOT NULL UNIQUE, is_active TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_code (code), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS referrals ( id CHAR(36) PRIMARY KEY, referrer_id CHAR(36) NOT NULL, referred_id CHAR(36) NULL, referral_code VARCHAR(20) NOT NULL, status ENUM('clicked', 'registered', 'subscribed') DEFAULT 'clicked', reward_claimed TINYINT(1) DEFAULT 0, reward_type VARCHAR(50) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, converted_at TIMESTAMP NULL, INDEX idx_referrer (referrer_id), INDEX idx_code (referral_code), FOREIGN KEY (referrer_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## File: `complete_migration.sql` ```sql -- ════════════════════════════════════════════════════════════ -- مُصادَق — Complete Phase 1 Migration (MySQL 8.0 Compatible) -- ════════════════════════════════════════════════════════════ -- 1. Invoice Line Items (AI extracted data) CREATE TABLE IF NOT EXISTS invoice_lines ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), invoice_id CHAR(36) NOT NULL, line_number INT NOT NULL, description VARCHAR(255) NOT NULL, quantity DECIMAL(10,3) DEFAULT 1, unit_price DECIMAL(15,4) NOT NULL, tax_rate DECIMAL(5,2) DEFAULT 16.00, tax_amount DECIMAL(15,4) DEFAULT 0, discount DECIMAL(15,4) DEFAULT 0, total_amount DECIMAL(15,4) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_invoice (invoice_id), FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. JoFotara Submissions Log CREATE TABLE IF NOT EXISTS jofotara_submissions ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), invoice_id CHAR(36) NOT NULL, tenant_id CHAR(36) NOT NULL, company_id CHAR(36) NOT NULL, jofotara_uuid VARCHAR(100) NULL, xml_content LONGTEXT NULL, status ENUM('accepted', 'rejected', 'pending') DEFAULT 'pending', qr_code_raw TEXT NULL, response_body JSON NULL, submitted_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_invoice (invoice_id), INDEX idx_tenant (tenant_id), FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. AI Usage Log CREATE TABLE IF NOT EXISTS ai_usage_log ( id INT AUTO_INCREMENT PRIMARY KEY, tenant_id CHAR(36) NOT NULL, user_id CHAR(36) NULL, company_id CHAR(36) NULL, action_type ENUM('invoice_extraction','voice_transcribe','voice_intent','report_generation','chatbot') NOT NULL, model_name VARCHAR(50) NOT NULL, prompt_tokens INT DEFAULT 0, completion_tokens INT DEFAULT 0, total_tokens INT DEFAULT 0, estimated_cost DECIMAL(10,6) DEFAULT 0, request_metadata JSON NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_tenant_date (tenant_id, created_at), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 4. Notifications CREATE TABLE IF NOT EXISTS notifications ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), tenant_id CHAR(36) NOT NULL, user_id CHAR(36) NULL, type ENUM('invoice_processed','invoice_rejected','quota_warning','month_end','system','achievement') NOT NULL, title VARCHAR(255) NOT NULL, body TEXT NOT NULL, is_read BOOLEAN DEFAULT FALSE, metadata JSON NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_read (user_id, is_read), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ════════════════════════════════════════════════════════════ -- 5. Safe ALTER TABLE (MySQL 8 compatible — no IF NOT EXISTS) -- Run each block separately. If column already exists, -- MySQL will show "Duplicate column" error — just skip it. -- ════════════════════════════════════════════════════════════ -- 5a. Companies: JoFotara credentials -- Run these ONE BY ONE. Skip any that say "Duplicate column name" ALTER TABLE companies ADD COLUMN jofotara_client_id VARCHAR(255) NULL; ALTER TABLE companies ADD COLUMN jofotara_secret_key VARCHAR(255) NULL; ALTER TABLE companies ADD COLUMN jofotara_status ENUM('active', 'inactive', 'pending') DEFAULT 'inactive'; -- 5b. Invoices: AI + JoFotara metadata -- Run these ONE BY ONE. Skip any that say "Duplicate column name" ALTER TABLE invoices ADD COLUMN invoice_category ENUM('simplified', 'standard') DEFAULT 'simplified'; ALTER TABLE invoices ADD COLUMN ubl_type_code VARCHAR(10) DEFAULT '388'; ALTER TABLE invoices ADD COLUMN payment_method_code VARCHAR(10) DEFAULT '013'; ALTER TABLE invoices ADD COLUMN validation_warnings JSON NULL; ALTER TABLE invoices ADD COLUMN ai_confidence DECIMAL(5,2) DEFAULT 0; ALTER TABLE invoices ADD COLUMN jofotara_uuid VARCHAR(100) NULL; ``` ## 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)); ``` ## File: `create_test_account.php` ```php beginTransaction(); // 1. Generate UUIDs $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) ); $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) ); // 2. Test Account Data $tenantName = "مكتب المراجعة التجريبي"; $tenantEmail = "reviewer@musadaq.jo"; $userName = "App Reviewer"; $userEmail = "reviewer@musadaq.jo"; $userPassword = "Reviewer2026!"; // 3. Encrypt data $encryptedTenantName = Encryption::encrypt($tenantName); $encryptedTenantEmail = Encryption::encrypt($tenantEmail); $encryptedUserName = Encryption::encrypt($userName); $encryptedUserEmail = Encryption::encrypt($userEmail); $emailHash = hash('sha256', strtolower($userEmail)); $passwordHash = password_hash($userPassword, PASSWORD_DEFAULT); // 4. Delete existing if any (prevent duplicates on re-run) $stmt = $db->prepare("DELETE FROM users WHERE email_hash = ?"); $stmt->execute([$emailHash]); // 5. Insert Tenant $stmt = $db->prepare("INSERT INTO tenants (id, name, email, status, created_at) VALUES (?, ?, ?, 'active', NOW())"); $stmt->execute([ $tenantId, $encryptedTenantName, $encryptedTenantEmail ]); // 6. Insert User (Manager) $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, $encryptedUserName, $encryptedUserEmail, $emailHash, $passwordHash ]); // 7. Insert Gamification Profile (Optional but good for testing dashboard) $stmtProfile = $db->prepare("INSERT INTO user_profiles (user_id, points, current_level, rank_title) VALUES (?, 1500, 2, 'مُحاسب مبتدئ') ON DUPLICATE KEY UPDATE points=1500"); $stmtProfile->execute([$userId]); $db->commit(); echo "✅ Test Account Created Successfully!\n"; echo "=====================================\n"; echo "Email: $userEmail\n"; echo "Password: $userPassword\n"; echo "=====================================\n"; } catch (\Exception $e) { $db->rollBack(); echo "❌ Error: " . $e->getMessage() . "\n"; } ``` ## File: `create_notifications_table.sql` ```sql -- Notifications Table CREATE TABLE IF NOT EXISTS notifications ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, tenant_id CHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, body TEXT, type ENUM('info', 'success', 'warning', 'error') DEFAULT 'info', category VARCHAR(50) DEFAULT 'general', entity_type VARCHAR(50) NULL, entity_id CHAR(36) NULL, is_read TINYINT(1) DEFAULT 0, read_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_read (user_id, is_read), INDEX idx_tenant (tenant_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ```