feat(form-builder): retry/resolve/dismiss artisan commands (WS-6)
Three CLI commands for ops use, mirroring the API endpoints in Task 9: - form-failures:retry (id|submission|org filter, --dry-run) - form-failures:resolve (single id, optional note) - form-failures:dismiss (single id, DismissalReasonType + note) Cross-tenant isolation enforced via form_submissions.organisation_id FK chain (RFC V3). retry_count incremented on retry; failure history preserved (new row on repeat failure, not in-place mutation). Refs: RFC-WS-6.md §3 (Q5), §4 (V2, V3) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\FormBuilder\DismissalReasonType;
|
||||
use App\Models\FormBuilder\FormSubmissionActionFailure;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* RFC-WS-6 §3 (Q5) + §4 (V2) — typed-reason dismiss action.
|
||||
* Bulk dismiss via API only.
|
||||
*/
|
||||
final class DismissFormSubmissionActionFailures extends Command
|
||||
{
|
||||
protected $signature = 'form-failures:dismiss
|
||||
{id : FormSubmissionActionFailure ULID}
|
||||
{--reason= : DismissalReasonType value (e.g. schema_deleted, other)}
|
||||
{--note= : Required when --reason=other}';
|
||||
|
||||
protected $description = 'Dismiss a single FormSubmissionActionFailure with a typed reason';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$id = (string) $this->argument('id');
|
||||
$reasonInput = $this->option('reason');
|
||||
$note = $this->option('note');
|
||||
|
||||
if ($reasonInput === null) {
|
||||
$this->error('--reason is required. Allowed: ' . implode(', ', array_map(static fn (DismissalReasonType $r): string => $r->value, DismissalReasonType::cases())));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$reason = DismissalReasonType::tryFrom((string) $reasonInput);
|
||||
if ($reason === null) {
|
||||
$this->error("Unknown reason '{$reasonInput}'.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($reason->requiresNote() && empty($note)) {
|
||||
$this->error("--note is required when --reason={$reason->value}.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$failure = FormSubmissionActionFailure::query()->withoutGlobalScopes()->find($id);
|
||||
if ($failure === null) {
|
||||
$this->error("Failure {$id} not found.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($failure->resolved_at !== null) {
|
||||
$this->warn("Failure {$id} already resolved; cannot dismiss.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($failure->dismissed_at !== null) {
|
||||
$this->warn("Failure {$id} already dismissed; no-op.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$failure->dismissed_at = now();
|
||||
$failure->dismissed_reason_type = $reason;
|
||||
$failure->dismissed_reason_note = $note !== null ? (string) $note : null;
|
||||
$failure->save();
|
||||
|
||||
$this->info("Dismissed failure {$id} ({$reason->value}).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user