"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";