From 29c44f2dbeb63f9e567395b6e65fcb19af70f27c Mon Sep 17 00:00:00 2001 From: gbanyan Date: Sun, 25 Jan 2026 06:22:57 +0800 Subject: [PATCH] Update roster import to sync existing members --- app/Console/Commands/ImportMembersCommand.php | 208 ++++++++++++++---- 1 file changed, 163 insertions(+), 45 deletions(-) diff --git a/app/Console/Commands/ImportMembersCommand.php b/app/Console/Commands/ImportMembersCommand.php index 15fc2b3..051386d 100644 --- a/app/Console/Commands/ImportMembersCommand.php +++ b/app/Console/Commands/ImportMembersCommand.php @@ -64,7 +64,8 @@ class ImportMembersCommand extends Command 10 => 'email', // e-mail ]; - $imported = 0; + $created = 0; + $updated = 0; $skipped = 0; $errors = []; @@ -91,59 +92,64 @@ class ImportMembersCommand extends Command $email = $this->resolveEmail($name, $row[10] ?? ''); $memberType = Member::TYPE_INDIVIDUAL; - // Validate phone for password generation - if (strlen($phone) < 4) { - $errors[] = "Row {$rowNum}: {$name} - Invalid phone number for password"; - $skipped++; + $existingMember = $this->findExistingMember($email, $memberNumber, $nationalId, $name); + if ($existingMember) { + $updateData = $this->buildUpdateData( + $existingMember, + $memberNumber, + $birthDate, + $gender, + $occupation, + $address, + $phone, + $nationalId, + $identityType, + $identityOtherText, + $applyDate, + ); + + if ($dryRun) { + $this->line("[DRY RUN] Would update: {$name} (member_id {$existingMember->id})"); + $updated++; + } elseif (! empty($updateData)) { + $existingMember->update($updateData); + $this->info("Updated: {$name} (member_id {$existingMember->id})"); + $updated++; + } else { + $skipped++; + } + continue; } - // Generate password from last 4 digits - $password = substr($phone, -4); - - // Check for existing user by email $existingUser = User::where('email', $email)->first(); - if ($existingUser) { - $this->warn("Row {$rowNum}: {$name} - Email already exists: {$email}"); - $skipped++; - continue; - } - - if ($memberNumber) { - $existingMemberNumber = Member::where('member_number', $memberNumber)->first(); - if ($existingMemberNumber) { - $this->warn("Row {$rowNum}: {$name} - Member number already exists: {$memberNumber}"); - $skipped++; - continue; - } - } - - // Check for existing member by national ID - if ($nationalId) { - $existingMember = Member::where('national_id_hash', hash('sha256', $nationalId))->first(); - if ($existingMember) { - $this->warn("Row {$rowNum}: {$name} - National ID already exists"); - $skipped++; - continue; - } - } + $isNewUser = false; if ($dryRun) { - $this->line("[DRY RUN] Would import: {$name}, Phone: {$phone}, Email: {$email}, Password: ****{$password}"); - $imported++; + $this->line("[DRY RUN] Would create: {$name}, Phone: {$phone}, Email: {$email}"); + $created++; continue; } - // Create User - $user = User::create([ - 'name' => $name, - 'email' => $email, - 'password' => Hash::make($password), - ]); + if (! $existingUser) { + if (strlen($phone) < 4) { + $errors[] = "Row {$rowNum}: {$name} - Invalid phone number for password"; + $skipped++; + continue; + } + + $password = substr($phone, -4); + $existingUser = User::create([ + 'name' => $name, + 'email' => $email, + 'password' => Hash::make($password), + ]); + $isNewUser = true; + } // Create Member $member = Member::create([ - 'user_id' => $user->id, + 'user_id' => $existingUser->id, 'member_number' => $memberNumber ?: null, 'full_name' => $name, 'email' => $email, @@ -166,6 +172,10 @@ class ImportMembersCommand extends Command 'membership_expires_at' => now()->endOfYear(), // 2026-12-31 ]); + if ($isNewUser) { + $this->info("Created user: {$name} ({$email})"); + } + // Create fully approved MembershipPayment MembershipPayment::create([ 'member_id' => $member->id, @@ -184,8 +194,8 @@ class ImportMembersCommand extends Command 'notes' => 'Imported from legacy roster - pre-approved', ]); - $this->info("Imported: {$name} ({$email})"); - $imported++; + $this->info("Created: {$name} ({$email})"); + $created++; } if (!$dryRun) { @@ -200,7 +210,8 @@ class ImportMembersCommand extends Command $this->newLine(); $this->info("Import complete!"); - $this->info(" Imported: {$imported}"); + $this->info(" Created: {$created}"); + $this->info(" Updated: {$updated}"); $this->info(" Skipped: {$skipped}"); if (!empty($errors)) { @@ -394,6 +405,113 @@ class ImportMembersCommand extends Command return [Member::IDENTITY_OTHER, $raw]; } + protected function findExistingMember(string $email, ?string $memberNumber, string $nationalId, string $name): ?Member + { + if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) { + $user = User::where('email', $email)->first(); + if ($user && $user->member) { + return $user->member; + } + } + + if ($memberNumber) { + $member = Member::where('member_number', $memberNumber)->first(); + if ($member) { + return $member; + } + } + + if ($nationalId !== '') { + $member = Member::where('national_id_hash', hash('sha256', $nationalId))->first(); + if ($member) { + return $member; + } + } + + $matches = Member::where('full_name', $name)->get(); + if ($matches->count() === 1) { + return $matches->first(); + } + + if ($matches->count() > 1) { + $nonPlaceholder = $matches->filter(function (Member $member) { + return ! $this->isPlaceholderEmail($member->email); + }); + if ($nonPlaceholder->count() === 1) { + return $nonPlaceholder->first(); + } + } + + return null; + } + + protected function isPlaceholderEmail(?string $email): bool + { + if (! $email) { + return true; + } + + return str_ends_with($email, '@member.usher.org.tw'); + } + + protected function buildUpdateData( + Member $member, + ?string $memberNumber, + ?string $birthDate, + ?string $gender, + string $occupation, + string $address, + string $phone, + string $nationalId, + ?string $identityType, + ?string $identityOtherText, + ?string $appliedAt, + ): array { + $update = []; + + if ($memberNumber && ! $member->member_number) { + $update['member_number'] = $memberNumber; + } + + if ($birthDate && ! $member->birth_date) { + $update['birth_date'] = $birthDate; + } + + if ($gender && ! $member->gender) { + $update['gender'] = $gender; + } + + if ($occupation !== '' && ! $member->occupation) { + $update['occupation'] = $occupation; + } + + if ($address !== '' && ! $member->address_line_1) { + $update['address_line_1'] = $address; + } + + if ($phone !== '' && ! $member->phone) { + $update['phone'] = $phone; + } + + if ($nationalId !== '' && empty($member->national_id_encrypted)) { + $update['national_id'] = $nationalId; + } + + if ($identityType && ! $member->identity_type) { + $update['identity_type'] = $identityType; + } + + if ($identityOtherText && ! $member->identity_other_text) { + $update['identity_other_text'] = $identityOtherText; + } + + if ($appliedAt && ! $member->applied_at) { + $update['applied_at'] = $appliedAt; + } + + return $update; + } + protected function toPinyin(string $name): string { // Simple romanization for email generation