Skip to content

Commit 6767e06

Browse files
authored
Merge pull request #3542 from codeeu/dev
Release: support profile updates and docs
2 parents 146cb60 + 1a7fb89 commit 6767e06

15 files changed

Lines changed: 1149 additions & 12 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace App\Console\Commands\Support;
4+
5+
use App\Models\Support\SupportCase;
6+
use App\Services\Support\SupportJson;
7+
use App\Services\Support\UserProfileUpdateService;
8+
use Illuminate\Console\Command;
9+
10+
class UserUpdateProfileCommand extends Command
11+
{
12+
protected $signature = 'support:user-update-profile
13+
{email : User email to update}
14+
{--firstname= : New first name (firstname field)}
15+
{--lastname= : New last name (lastname field)}
16+
{--dry-run : Plan only; do not write}
17+
{--json : Output JSON only}';
18+
19+
protected $description = 'Support tool: update a user profile first/last name (dry-run supported)';
20+
21+
public function handle(UserProfileUpdateService $service): int
22+
{
23+
$email = (string) $this->argument('email');
24+
$firstname = $this->option('firstname');
25+
$lastname = $this->option('lastname');
26+
$dryRun = (bool) $this->option('dry-run');
27+
28+
$firstname = is_string($firstname) && trim($firstname) !== '' ? trim($firstname) : null;
29+
$lastname = is_string($lastname) && trim($lastname) !== '' ? trim($lastname) : null;
30+
31+
$case = SupportCase::create([
32+
'source_channel' => 'manual',
33+
'processing_mode' => 'manual',
34+
'subject' => 'CLI: support:user-update-profile',
35+
'raw_message' => json_encode(['email' => $email, 'firstname' => $firstname, 'lastname' => $lastname]),
36+
'status' => 'investigating',
37+
'risk_level' => 'low',
38+
'target_email' => $email,
39+
'correlation_id' => SupportJson::correlationId(),
40+
]);
41+
42+
$payload = $service->updateProfile(
43+
case: $case,
44+
email: $email,
45+
firstname: $firstname,
46+
lastname: $lastname,
47+
dryRun: $dryRun,
48+
viaEmailApproval: !$dryRun,
49+
);
50+
51+
$json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
52+
$this->output->writeln($json);
53+
54+
return ($payload['ok'] ?? false) ? self::SUCCESS : self::FAILURE;
55+
}
56+
}

app/Http/Controllers/Internal/Support/ToolController.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Services\Support\SupportActionLogger;
88
use App\Services\Support\SupportJson;
99
use App\Services\Support\UserAuditService;
10+
use App\Services\Support\UserProfileUpdateService;
1011
use App\Services\Support\UserRestoreService;
1112
use Illuminate\Http\Request;
1213

@@ -16,6 +17,7 @@ public function __construct(
1617
private readonly SupportActionLogger $logger,
1718
private readonly UserAuditService $userAudit,
1819
private readonly UserRestoreService $userRestore,
20+
private readonly UserProfileUpdateService $userProfileUpdate,
1921
private readonly EventAuditService $eventAudit,
2022
) {
2123
}
@@ -78,6 +80,40 @@ public function userRestore(Request $request)
7880
return SupportJson::json($payload, ($payload['ok'] ?? false) ? 200 : 422);
7981
}
8082

83+
public function userProfileUpdate(Request $request)
84+
{
85+
$data = $request->validate([
86+
'support_case_id' => ['required', 'integer'],
87+
'email' => ['required', 'string'],
88+
'firstname' => ['nullable', 'string', 'max:255'],
89+
'lastname' => ['nullable', 'string', 'max:255'],
90+
'dry_run' => ['required', 'boolean'],
91+
]);
92+
93+
$case = SupportCase::findOrFail((int) $data['support_case_id']);
94+
$payload = $this->userProfileUpdate->updateProfile(
95+
case: $case,
96+
email: $data['email'],
97+
firstname: $data['firstname'] ?? null,
98+
lastname: $data['lastname'] ?? null,
99+
dryRun: (bool) $data['dry_run'],
100+
);
101+
102+
$this->logger->log(
103+
case: $case,
104+
actionName: 'user_profile_update',
105+
actionType: 'write',
106+
input: $data,
107+
output: $payload,
108+
succeeded: (bool) ($payload['ok'] ?? false),
109+
executedBy: 'system',
110+
correlationId: $case->correlation_id,
111+
errorMessage: ($payload['ok'] ?? false) ? null : implode(';', (array) ($payload['errors'] ?? [])),
112+
);
113+
114+
return SupportJson::json($payload, ($payload['ok'] ?? false) ? 200 : 422);
115+
}
116+
81117
public function eventAudit(Request $request)
82118
{
83119
$data = $request->validate([

app/Jobs/Support/ExecuteApprovedSupportActionJob.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Models\Support\SupportApproval;
66
use App\Models\Support\SupportCase;
77
use App\Services\Support\SupportActionLogger;
8+
use App\Services\Support\UserProfileUpdateService;
89
use App\Services\Support\UserRestoreService;
910
use Illuminate\Bus\Queueable;
1011
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -20,7 +21,11 @@ public function __construct(public int $supportApprovalId)
2021
{
2122
}
2223

23-
public function handle(UserRestoreService $userRestore, SupportActionLogger $logger): void
24+
public function handle(
25+
UserRestoreService $userRestore,
26+
UserProfileUpdateService $userProfileUpdate,
27+
SupportActionLogger $logger,
28+
): void
2429
{
2530
$approval = SupportApproval::findOrFail($this->supportApprovalId);
2631
$case = SupportCase::findOrFail($approval->support_case_id);
@@ -67,6 +72,16 @@ public function handle(UserRestoreService $userRestore, SupportActionLogger $log
6772
email: (string) ($payload['email'] ?? ''),
6873
dryRun: false,
6974
confidence: isset($payload['confidence']) ? (float) $payload['confidence'] : null,
75+
viaEmailApproval: true,
76+
);
77+
} elseif ($action === 'user_profile_update') {
78+
$result = $userProfileUpdate->updateProfile(
79+
case: $case,
80+
email: (string) ($payload['email'] ?? ''),
81+
firstname: isset($payload['firstname']) ? (string) $payload['firstname'] : null,
82+
lastname: isset($payload['lastname']) ? (string) $payload['lastname'] : null,
83+
dryRun: false,
84+
viaEmailApproval: true,
7085
);
7186
} else {
7287
$result = [

app/Jobs/Support/ProcessSupportCaseDiagnosticsJob.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Models\Support\SupportCaseMessage;
77
use App\Services\Support\Agents\DiagnosticsAgentService;
88
use App\Services\Support\SupportActionLogger;
9+
use App\Services\Support\UserProfileUpdateService;
910
use App\Services\Support\UserRestoreService;
1011
use Illuminate\Bus\Queueable;
1112
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -25,6 +26,7 @@ public function handle(
2526
DiagnosticsAgentService $diagnostics,
2627
SupportActionLogger $logger,
2728
UserRestoreService $userRestore,
29+
UserProfileUpdateService $userProfileUpdate,
2830
): void {
2931
$case = SupportCase::findOrFail($this->supportCaseId);
3032
$case->update(['status' => 'investigating']);
@@ -56,6 +58,20 @@ public function handle(
5658
);
5759
}
5860

61+
if ($case->case_type === 'profile_update' && $case->target_email) {
62+
$dryRunResult = $userProfileUpdate->updateFromCase($case, dryRun: true);
63+
$logger->log(
64+
case: $case,
65+
actionName: 'user_profile_update',
66+
actionType: 'write',
67+
input: ['email' => $case->target_email, 'dry_run' => true],
68+
output: $dryRunResult,
69+
succeeded: (bool) ($dryRunResult['ok'] ?? false),
70+
executedBy: 'agent',
71+
correlationId: $case->correlation_id,
72+
);
73+
}
74+
5975
// Persist diagnostics snapshot as a message for UI/debugging (stable storage for later external orchestrator).
6076
SupportCaseMessage::create([
6177
'support_case_id' => $case->id,

app/Services/Support/Agents/TriageAgentService.php

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,39 @@
33
namespace App\Services\Support\Agents;
44

55
use App\Models\Support\SupportCase;
6+
use App\Services\Support\SupportProfileRequestParser;
67
use Illuminate\Support\Str;
78

89
class TriageAgentService
910
{
11+
public function __construct(
12+
private readonly SupportProfileRequestParser $profileParser,
13+
) {
14+
}
15+
1016
public function triage(SupportCase $case): array
1117
{
12-
$text = Str::lower((string) ($case->normalized_message ?? $case->raw_message ?? ''));
18+
$rawText = (string) ($case->normalized_message ?? $case->raw_message ?? '');
19+
$text = Str::lower($rawText);
20+
$profile = $this->profileParser->parse($rawText);
1321

1422
// V1 heuristic placeholder (replace with LLM later, keep output schema stable).
1523
$caseType = 'unknown';
1624
$runbook = 'unknown';
1725
if (Str::contains($text, ['soft-deleted', 'deleted', 'restore account', 'account missing'])) {
1826
$caseType = 'account_restore';
1927
$runbook = 'restore_deleted_account';
28+
} elseif (Str::contains($text, [
29+
'update profile',
30+
'profile name',
31+
'your details',
32+
'first name',
33+
'last name',
34+
'change name',
35+
'rename profile',
36+
]) || ($profile['firstname'] !== null || $profile['lastname'] !== null)) {
37+
$caseType = 'profile_update';
38+
$runbook = 'update_user_profile';
2039
} elseif (Str::contains($text, ['duplicate', 'two accounts', 'split across'])) {
2140
$caseType = 'duplicate_account';
2241
$runbook = 'duplicate_account_investigation';
@@ -31,22 +50,30 @@ public function triage(SupportCase $case): array
3150
$runbook = 'role_problem';
3251
}
3352

34-
$targetEmail = $this->extractFirstEmail($text);
53+
$targetEmail = $profile['email'] ?? $this->extractFirstEmail($text);
3554
$secondary = $this->extractAllEmails($text);
3655
$secondary = array_values(array_filter($secondary, fn ($e) => $targetEmail ? $e !== $targetEmail : true));
3756

3857
$risk = Str::contains($text, ['password reset', 'merge', 'ownership', 'privileged']) ? 'high' : 'low';
58+
if ($caseType === 'profile_update') {
59+
$risk = 'low';
60+
}
61+
62+
$needsHuman = $targetEmail === null
63+
|| ($caseType === 'profile_update' && $profile['firstname'] === null && $profile['lastname'] === null);
3964

4065
return [
4166
'case_type' => $caseType,
4267
'confidence' => 0.50,
4368
'target_email' => $targetEmail,
4469
'secondary_emails' => $secondary,
4570
'target_user_id' => null,
46-
'requested_action' => null,
71+
'requested_action' => $caseType === 'profile_update' ? 'user_profile_update' : null,
72+
'profile_firstname' => $profile['firstname'],
73+
'profile_lastname' => $profile['lastname'],
4774
'risk_level' => $risk,
4875
'recommended_runbook' => $runbook,
49-
'needs_human_review' => $targetEmail === null,
76+
'needs_human_review' => $needsHuman,
5077
'reasoning_summary' => 'V1 heuristic triage (LLM integration pending).',
5178
];
5279
}

app/Services/Support/SupportApprovalEmailService.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class SupportApprovalEmailService
1414
public function __construct(
1515
private readonly GmailOutboundService $gmail,
1616
private readonly SupportSenderAllowlist $allowlist,
17+
private readonly SupportProfileRequestParser $profileParser,
1718
) {
1819
}
1920

@@ -175,6 +176,20 @@ private function proposedActionForCase(SupportCase $case): array
175176
];
176177
}
177178

179+
if ($case->case_type === 'profile_update' && $case->target_email) {
180+
$profile = $this->profileParser->parse((string) ($case->normalized_message ?? $case->raw_message ?? ''));
181+
if ($profile['firstname'] !== null || $profile['lastname'] !== null) {
182+
return [
183+
'action' => 'user_profile_update',
184+
'payload' => [
185+
'email' => $case->target_email,
186+
'firstname' => $profile['firstname'],
187+
'lastname' => $profile['lastname'],
188+
],
189+
];
190+
}
191+
}
192+
178193
return ['action' => 'none', 'payload' => []];
179194
}
180195

@@ -200,11 +215,22 @@ private function buildDryRunBody(SupportCase $case, array $proposedAction): stri
200215
$lines[] = '';
201216
}
202217

203-
$dryRun = $case->actions()->where('action_name', 'user_restore')->where('action_type', 'write')->latest()->first()?->output_json;
204-
if (is_array($dryRun)) {
205-
$lines[] = 'Planned changes (dry run):';
206-
$lines[] = json_encode($dryRun['changes_planned'] ?? $dryRun, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
207-
$lines[] = '';
218+
foreach (['user_restore', 'user_profile_update'] as $writeAction) {
219+
$dryRun = $case->actions()->where('action_name', $writeAction)->where('action_type', 'write')->latest()->first()?->output_json;
220+
if (!is_array($dryRun)) {
221+
continue;
222+
}
223+
$result = $dryRun['result'] ?? $dryRun;
224+
if (is_array($result) && isset($result['before'], $result['after'])) {
225+
$lines[] = 'Planned profile/account changes (dry run):';
226+
$lines[] = 'Before: '.json_encode($result['before'], JSON_UNESCAPED_SLASHES);
227+
$lines[] = 'After: '.json_encode($result['after'], JSON_UNESCAPED_SLASHES);
228+
$lines[] = '';
229+
} elseif (is_array($result)) {
230+
$lines[] = 'Planned changes (dry run):';
231+
$lines[] = json_encode($result['changes_planned'] ?? $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
232+
$lines[] = '';
233+
}
208234
}
209235

210236
$action = $proposedAction['action'] ?? 'none';

0 commit comments

Comments
 (0)