'parents', 'firstparent_id' => 'firstparent_id', // users.id of primary parent 'second_user_id' => 'secondparent_id', // users.id of 2nd parent (may be 0) 'second_email' => 'secondparent_email', 'second_firstname' => 'secondparent_firstname', 'second_lastname' => 'secondparent_lastname', ]; /** Defaults for prefs rows per category (your schema) */ protected array $prefsDefaults = [ // category => [via_email, via_sms, cc_all_guardians] 'attendance' => [1, 0, 1], 'grade' => [1, 0, 1], 'behavior' => [1, 0, 1], 'general' => [1, 0, 1], ]; public function run() { $this->db = \Config\Database::connect(); $this->db->transStrict(false); // don't auto-rollback the entire txn on one failure $this->db->transException(false); // don't throw (so we can print errors ourselves) // Start a transaction so the DB stays consistent; we'll print status after. $this->db->transStart(); [$familiesCreated, $studentLinks] = $this->ensureFamiliesAndLinks(); $primaryLinked = $this->linkPrimaryGuardians(); [$stubUsers, $secondaryLinked] = $this->importSecondaryGuardiansFromParents(); $prefsUpserts = $this->seedFamilyCommPrefsPerCategory(); $this->db->transComplete(); echo PHP_EOL; $status = $this->db->transStatus(); echo $status ? "[TXN] Transaction COMMITTED.\n" : "[TXN] Transaction FAILED.\n"; if (! $status) { $err = $this->db->error(); echo " DB error: " . ($err['message'] ?? 'unknown') . " (code " . ($err['code'] ?? 'n/a') . ")\n"; } echo "Families created: {$familiesCreated}\n"; echo "family_students links inserted: {$studentLinks}\n"; echo "Primary guardians linked: {$primaryLinked}\n"; echo "Stub users created (secondary): {$stubUsers}\n"; echo "Secondary guardians linked: {$secondaryLinked}\n"; echo "family_comm_prefs upserts: {$prefsUpserts}\n"; echo PHP_EOL; } protected function exec(string $label, string $sql): int { $ok = $this->db->query($sql); if (! $ok) { $err = $this->db->error(); echo "[ERROR] {$label}: " . ($err['message'] ?? 'unknown') . " (code " . ($err['code'] ?? 'n/a') . ")\n"; return 0; } $n = (int) $this->db->affectedRows(); echo "[OK] {$label}: affectedRows={$n}\n"; return $n; } /** * Create families from students where parent_id maps to a real users.id, * then link all students to their family's id. */ protected function ensureFamiliesAndLinks(): array { $familiesCreated = $this->exec( 'families insert', " INSERT IGNORE INTO families (family_code, household_name, is_active, created_at, updated_at) SELECT DISTINCT CONCAT('FAM-', s.parent_id), CONCAT('Family of User ', s.parent_id), 1, NOW(), NOW() FROM students s JOIN users u ON u.id = s.parent_id WHERE s.parent_id IS NOT NULL " ); $studentLinks = $this->exec( 'family_students insert', " INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home) SELECT f.id, s.id, 1 FROM students s JOIN users u ON u.id = s.parent_id JOIN families f ON f.family_code = CONCAT('FAM-', s.parent_id) " ); // Backfill addresses/phone/household_name from users if empty $this->exec( 'families backfill address/phone/name', " UPDATE families f JOIN users u ON u.id = CAST(SUBSTRING(f.family_code, 5) AS UNSIGNED) SET f.address_line1 = COALESCE(NULLIF(f.address_line1, ''), NULLIF(u.address_street, '')), f.city = COALESCE(NULLIF(f.city, ''), NULLIF(u.city, '')), f.state = COALESCE(NULLIF(f.state, ''), NULLIF(u.state, '')), f.postal_code = COALESCE(NULLIF(f.postal_code, ''), NULLIF(u.zip, '')), f.primary_phone = COALESCE(NULLIF(f.primary_phone, ''), NULLIF(u.cellphone, '')), f.household_name= CASE WHEN f.household_name IS NULL OR f.household_name = '' THEN TRIM(CONCAT('Family of ', NULLIF(u.firstname, ''), ' ', NULLIF(u.lastname, ''))) ELSE f.household_name END " ); return [$familiesCreated, $studentLinks]; } /** * Link primary guardians using student-anchored joins (no family_code parsing assumptions). * relation uses a safe generic 'guardian' (fits varchar(32) and common enums). */ protected function linkPrimaryGuardians(): int { // Log candidate count (already printed earlier for you) $row = $this->db->query(" SELECT COUNT(*) AS c FROM ( SELECT DISTINCT fs.family_id, u.id AS user_id FROM family_students fs JOIN students s ON s.id = fs.student_id JOIN users u ON u.id = s.parent_id WHERE s.parent_id IS NOT NULL ) X ")->getRowArray(); echo "[DEBUG] Primary guardian candidates (via students): " . (int)($row['c'] ?? 0) . "\n"; // Path A: student-anchored (ignore duplicates/violations) $linkedA = $this->exec( 'primary guardians insert A (student-anchored, IGNORE)', " INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) SELECT DISTINCT fs.family_id, u.id, 'guardian', 1, 1, 0 FROM family_students fs JOIN students s ON s.id = fs.student_id JOIN users u ON u.id = s.parent_id WHERE s.parent_id IS NOT NULL " ); // Path B: via family_code (belt & suspenders) $linkedB = $this->exec( 'primary guardians insert B (family_code, IGNORE)', " INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) SELECT f.id, u.id, 'guardian', 1, 1, 0 FROM families f JOIN users u ON u.id = CAST(SUBSTRING(f.family_code, 5) AS UNSIGNED) " ); return $linkedA + $linkedB; } /** * Create/link secondary guardians from legacy parents: * - Create stub users for secondparent_email not present in users. * - Link those (or explicit secondparent_id) to all families of the first parent’s students. */ protected function importSecondaryGuardiansFromParents(): array { $L = $this->legacy; if (! $this->db->tableExists($L['table'])) { echo "[LEGACY] Table '{$L['table']}' not found; skipping secondary import.\n"; return [0, 0]; } $fields = array_map('strtolower', array_column($this->db->getFieldData($L['table']), 'name')); foreach (['firstparent_id','second_user_id','second_email','second_firstname','second_lastname'] as $k) { $col = strtolower($L[$k]); if (! in_array($col, $fields, true)) { echo "[LEGACY] Missing column {$L[$k]} in {$L['table']}; skipping secondary import.\n"; return [0, 0]; } } $stubUsers = $this->exec( 'secondary guardian stubs (users)', " INSERT INTO users (firstname, lastname, email, cellphone, address_street, apt, city, state, zip, accept_school_policy, is_verified, status, is_suspended, failed_attempts, password, created_at, updated_at, token, account_id, user_type, semester, school_year, rfid_tag, last_failed_at) SELECT NULLIF(p.{$L['second_firstname']},''), NULLIF(p.{$L['second_lastname']},''), p.{$L['second_email']}, '', '', NULL, '', '', '', 0, 0, 'Active', 0, 0, '$2y$10abcdefghijklmnopqrstuv', NOW(), NOW(), NULL, NULL, 'secondary', '', NULL, NULL, NULL FROM {$L['table']} p LEFT JOIN users u ON u.email = p.{$L['second_email']} WHERE p.{$L['second_email']} IS NOT NULL AND p.{$L['second_email']} <> '' AND u.id IS NULL " ); $secondaryLinked = $this->exec( 'secondary guardians insert (IGNORE)', " INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) SELECT DISTINCT fs.family_id, COALESCE(u2.id, u_email.id) AS user_id, 'guardian', 0, 1, 0 FROM {$L['table']} p JOIN students s ON s.parent_id = p.{$L['firstparent_id']} OR s.parent_id = p.id JOIN family_students fs ON fs.student_id = s.id LEFT JOIN users u2 ON u2.id = NULLIF(p.{$L['second_user_id']}, 0) LEFT JOIN users u_email ON u_email.email = p.{$L['second_email']} WHERE p.{$L['second_email']} IS NOT NULL AND p.{$L['second_email']} <> '' AND COALESCE(u2.id, u_email.id) IS NOT NULL " ); return [$stubUsers, $secondaryLinked]; } /** * Seed one prefs row per (family_id, category) with your defaults. * Counts how many rows were *actually inserted* across all categories. */ protected function seedFamilyCommPrefsPerCategory(): int { if (! $this->db->tableExists('family_comm_prefs')) { echo "[INFO] family_comm_prefs not found; skipping prefs seed.\n"; return 0; } $insertedTotal = 0; foreach ($this->prefsDefaults as $category => [$viaEmail, $viaSms, $ccAll]) { // Count how many families are missing this category BEFORE $row = $this->db->query(" SELECT COUNT(*) AS c FROM families f LEFT JOIN family_comm_prefs p ON p.family_id = f.id AND p.category = ? WHERE p.family_id IS NULL ", [$category])->getRowArray(); $missing = (int)($row['c'] ?? 0); if ($missing > 0) { $this->db->query(" INSERT INTO family_comm_prefs (family_id, category, via_email, via_sms, cc_all_guardians) SELECT f.id, ?, ?, ?, ? FROM families f LEFT JOIN family_comm_prefs p ON p.family_id = f.id AND p.category = ? WHERE p.family_id IS NULL ", [$category, (int)$viaEmail, (int)$viaSms, (int)$ccAll, $category]); $insertedTotal += $missing; } } return $insertedTotal; } /** Utility: check if a column exists (case-insensitive) */ protected function columnExists(string $table, string $column): bool { if (! $this->db->tableExists($table)) return false; $cols = array_map('strtolower', array_column($this->db->getFieldData($table), 'name')); return in_array(strtolower($column), $cols, true); } }