" CREATE TABLE IF NOT EXISTS user_devices ( id CHAR(36) PRIMARY KEY DEFAULT (UUID()), user_id CHAR(36) 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ", // ─── 2. Users table: Add phone + mobile fields ───────── 'add_users_phone' => "ALTER TABLE users ADD COLUMN phone VARCHAR(20) 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) PRIMARY KEY, tenant_id CHAR(36) NOT NULL, company_id CHAR(36) NOT NULL, uploaded_by CHAR(36) NOT 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ", // ─── 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) NOT NULL, invoice_id CHAR(36) NULL, tenant_id CHAR(36) NOT NULL, company_id CHAR(36) 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ", // ─── 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) PRIMARY KEY DEFAULT (UUID()), tenant_id CHAR(36) NOT NULL, user_id CHAR(36) 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ", ]; $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";