@@ -16,6 +16,7 @@ public function __construct(
1616 private readonly SupportSenderAllowlist $ allowlist ,
1717 private readonly SupportProfileRequestParser $ profileParser ,
1818 private readonly SupportRoleRequestResolver $ roleResolver ,
19+ private readonly SupportEmailChangeRequestResolver $ emailChangeResolver ,
1920 ) {
2021 }
2122
@@ -24,6 +25,7 @@ public function approvalSubject(SupportCase $case): string
2425 $ prefix = (string ) config ('support_gmail.approval_subject_prefix ' , '[CW-SUPPORT ' );
2526 $ headline = match ($ case ->case_type ) {
2627 'profile_update ' => 'Please review — name change ' ,
28+ 'email_change ' => 'Please review — account email change ' ,
2729 'account_restore ' => 'Please review — account restore ' ,
2830 'role_add ' => 'Please review — add user role ' ,
2931 'role_remove ' => 'Please review — remove user role ' ,
@@ -269,6 +271,19 @@ private function proposedActionForCase(SupportCase $case): array
269271 ];
270272 }
271273
274+ if ($ case ->case_type === 'email_change ' ) {
275+ $ change = $ this ->emailChangeResolver ->resolve ($ case );
276+ if ($ change ['from_email ' ] !== null && $ change ['to_email ' ] !== null ) {
277+ return [
278+ 'action ' => 'user_email_update ' ,
279+ 'payload ' => [
280+ 'from ' => $ change ['from_email ' ],
281+ 'to ' => $ change ['to_email ' ],
282+ ],
283+ ];
284+ }
285+ }
286+
272287 if ($ case ->case_type === 'profile_update ' && $ case ->target_email ) {
273288 $ profile = $ this ->profileParser ->parse ((string ) ($ case ->normalized_message ?? $ case ->raw_message ?? '' ));
274289 if ($ profile ['firstname ' ] !== null || $ profile ['lastname ' ] !== null ) {
@@ -503,7 +518,11 @@ private function dryRunPlannedChangeLines(SupportCase $case, array $proposedActi
503518 {
504519 $ action = $ proposedAction ['action ' ] ?? 'none ' ;
505520 $ payload = $ proposedAction ['payload ' ] ?? [];
506- $ email = (string ) ($ case ->target_email ?? $ payload ['email ' ] ?? '' );
521+ $ email = (string ) ($ case ->target_email ?? $ payload ['email ' ] ?? $ payload ['from ' ] ?? '' );
522+
523+ if ($ action === 'user_email_update ' ) {
524+ return $ this ->dryRunEmailChangeLines ($ case , $ payload );
525+ }
507526
508527 if ($ action === 'user_profile_update ' ) {
509528 return $ this ->dryRunProfileChangeLines ($ case , $ payload , $ email );
@@ -540,6 +559,48 @@ private function dryRunPlannedChangeLines(SupportCase $case, array $proposedActi
540559 ];
541560 }
542561
562+ /**
563+ * @param array<string, mixed> $payload
564+ * @return list<string>
565+ */
566+ private function dryRunEmailChangeLines (SupportCase $ case , array $ payload ): array
567+ {
568+ $ output = $ case ->actions ()
569+ ->where ('action_name ' , 'user_email_update ' )
570+ ->where ('action_type ' , 'write ' )
571+ ->latest ()
572+ ->first ()?->output_json;
573+
574+ $ result = is_array ($ output ) ? ($ output ['result ' ] ?? []) : [];
575+ $ from = (string ) ($ result ['before ' ]['email ' ] ?? $ payload ['from ' ] ?? $ case ->target_email ?? '' );
576+ $ to = (string ) ($ result ['after ' ]['email ' ] ?? $ payload ['to ' ] ?? '' );
577+ $ note = (string ) ($ result ['note ' ] ?? '' );
578+
579+ if ($ note === 'email_already_matches_requested_value ' ) {
580+ return [
581+ '' ,
582+ 'The account already uses ' .$ to .'. ' ,
583+ 'Approving will make no change. ' ,
584+ ];
585+ }
586+
587+ $ lines = [
588+ '' ,
589+ 'We will update the login email on this CodeWeek account: ' ,
590+ ' • From: ' .($ from !== '' ? $ from : '(not found) ' ),
591+ ' • To: ' .($ to !== '' ? $ to : '(not found) ' ),
592+ ];
593+
594+ if (($ result ['would_update_email_display ' ] ?? false ) === true ) {
595+ $ lines [] = ' • The public display email will be updated to match. ' ;
596+ }
597+
598+ $ lines [] = '' ;
599+ $ lines [] = 'The person will need to verify the new email address before it is fully active. ' ;
600+
601+ return $ lines ;
602+ }
603+
543604 /**
544605 * @param array<string, mixed> $payload
545606 * @return list<string>
@@ -829,6 +890,7 @@ private function completionHeadline(SupportCase $case, string $action, bool $suc
829890
830891 return match ($ action ) {
831892 'user_profile_update ' => 'Done — name updated on CodeWeek account ' ,
893+ 'user_email_update ' => 'Done — account email updated ' ,
832894 'user_restore ' => 'Done — CodeWeek account reactivated ' ,
833895 'user_role_add ' => 'Done — user role added ' ,
834896 'user_role_remove ' => 'Done — user role removed ' ,
@@ -877,7 +939,7 @@ private function buildCompletionBody(
877939 }
878940
879941 // Role add/remove are batch actions; the per-account list is shown above.
880- if ($ email !== '' && ! in_array ($ action , ['user_role_add ' , 'user_role_remove ' ], true )) {
942+ if ($ email !== '' && ! in_array ($ action , ['user_role_add ' , 'user_role_remove ' , ' user_email_update ' ], true )) {
881943 $ lines [] = '' ;
882944 $ lines [] = 'Account email: ' .$ email ;
883945 }
@@ -888,10 +950,10 @@ private function buildCompletionBody(
888950
889951 $ lines [] = '' ;
890952 if ($ succeeded ) {
891- $ lines [] = in_array ($ action , ['user_role_add ' , 'user_role_remove ' ], true )
953+ $ lines [] = in_array ($ action , ['user_role_add ' , 'user_role_remove ' , ' user_email_update ' ], true )
892954 ? 'No further action is needed. You do not need to reply to this email. '
893955 : 'No further action is needed. The supporter can sign in with their usual email and password. ' ;
894- if (! in_array ($ action , ['user_role_add ' , 'user_role_remove ' ], true )) {
956+ if (! in_array ($ action , ['user_role_add ' , 'user_role_remove ' , ' user_email_update ' ], true )) {
895957 $ lines [] = 'You do not need to reply to this email. ' ;
896958 }
897959 } else {
@@ -914,6 +976,33 @@ private function completionSuccessLines(SupportCase $case, string $action, array
914976 $ inner = is_array ($ result ['result ' ] ?? null ) ? $ result ['result ' ] : [];
915977 $ note = is_string ($ inner ['note ' ] ?? null ) ? $ inner ['note ' ] : '' ;
916978
979+ if ($ action === 'user_email_update ' ) {
980+ $ before = (array ) ($ inner ['before ' ] ?? []);
981+ $ after = (array ) ($ inner ['after ' ] ?? []);
982+
983+ if ($ note === 'email_already_matches_requested_value ' ) {
984+ return [
985+ 'The account already uses the requested email address — no change was required. ' ,
986+ ];
987+ }
988+
989+ $ lines = [
990+ 'We updated the login email on this CodeWeek account. ' ,
991+ '' ,
992+ ' • From: ' .(string ) ($ before ['email ' ] ?? '(unknown) ' ),
993+ ' • To: ' .(string ) ($ after ['email ' ] ?? '(unknown) ' ),
994+ ];
995+
996+ if (($ inner ['would_update_email_display ' ] ?? false ) === true ) {
997+ $ lines [] = ' • The public display email was updated to match. ' ;
998+ }
999+
1000+ $ lines [] = '' ;
1001+ $ lines [] = 'The person should sign in with the new email address and complete email verification if prompted. ' ;
1002+
1003+ return $ lines ;
1004+ }
1005+
9171006 if ($ action === 'user_profile_update ' ) {
9181007 $ lines = [
9191008 'We updated the name shown on the CodeWeek account ' .($ email !== '' ? ' for ' .$ email : '' ).'. ' ,
@@ -1128,7 +1217,11 @@ private function humanizeError(string $code, string $action, int $caseId): strin
11281217 return match (true ) {
11291218 str_contains ($ code , 'no_matching_user ' ) => 'We could not find a CodeWeek account with that email address. ' ,
11301219 str_contains ($ code , 'ambiguous_user ' ) => 'More than one account matched this email. A team member must review Case # ' .$ caseId .' manually. ' ,
1131- str_contains ($ code , 'invalid_email ' ) => 'The email address on the request was not valid. ' ,
1220+ str_contains ($ code , 'invalid_from_email ' ) => 'The current email address on the request was not valid. ' ,
1221+ str_contains ($ code , 'invalid_to_email ' ) => 'The new email address on the request was not valid. ' ,
1222+ str_contains ($ code , 'from_and_to_emails_identical ' ) => 'The current and new email addresses are the same. ' ,
1223+ str_contains ($ code , 'to_email_already_in_use ' ) => 'Another CodeWeek account already uses the new email address. ' ,
1224+ str_contains ($ code , 'email_already_matches ' ) => 'The account already uses the requested email address. ' ,
11321225 str_contains ($ code , 'no_profile_fields ' ) => 'The request did not include a first or last name to update. ' ,
11331226 str_contains ($ code , 'dry_run_mode ' ) => 'The system is in preview-only mode and could not apply live changes. ' ,
11341227 str_contains ($ code , 'unsupported_approved_action ' ) => 'This type of request cannot be run automatically yet. ' ,
0 commit comments