option('id') === null && $this->option('submission') === null && $this->option('org') === null ) { $this->error('At least one of --id, --submission, or --org is required.'); return self::FAILURE; } $query = $this->buildQuery(); $failures = $query->get(); if ($failures->isEmpty()) { $this->info('No matching open failures.'); return self::SUCCESS; } $rows = []; foreach ($failures as $failure) { if ($this->option('dry-run')) { $rows[] = ['id' => (string) $failure->id, 'submission' => (string) $failure->form_submission_id, 'result' => 'would-retry']; continue; } $rows[] = $this->retryOne($failure, $applicator); } $this->table(['id', 'submission', 'result'], $rows); return self::SUCCESS; } /** * @return Builder */ private function buildQuery(): Builder { $query = FormSubmissionActionFailure::query()->open(); if (($id = $this->option('id')) !== null) { $query->where('id', $id); } if (($submissionId = $this->option('submission')) !== null) { $query->where('form_submission_id', $submissionId); } if (($listener = $this->option('listener')) !== null) { $query->where('listener_class', $listener); } if (($org = $this->option('org')) !== null) { // FK chain: failure โ†’ submission โ†’ organisation. $query->whereHas('submission', function (Builder $q) use ($org): void { /** @var Builder<\App\Models\FormBuilder\FormSubmission> $q */ $q->where('organisation_id', $org); }); } return $query; } /** * @return array{id:string, submission:string, result:string} */ private function retryOne(FormSubmissionActionFailure $failure, FormBindingApplicator $applicator): array { $submission = FormSubmission::query()->withoutGlobalScopes()->find($failure->form_submission_id); if ($submission === null) { return ['id' => (string) $failure->id, 'submission' => (string) $failure->form_submission_id, 'result' => 'submission-gone']; } try { DB::transaction(function () use ($applicator, $submission): void { $result = $applicator->apply($submission); FormSubmission::query() ->whereKey($submission->id) ->update([ 'apply_status' => $result->applyStatus()->value, 'apply_completed_at' => now(), ]); }); $failure->retry_count = (int) $failure->retry_count + 1; $failure->resolved_at = now(); $failure->save(); return ['id' => (string) $failure->id, 'submission' => (string) $submission->id, 'result' => 'succeeded']; } catch (Throwable $e) { // Append a NEW row preserving history, increment retry_count on original. DB::transaction(function () use ($failure, $submission, $e): void { FormSubmissionActionFailure::query()->create([ 'form_submission_id' => $submission->id, 'listener_class' => $failure->listener_class, 'failed_at' => now(), 'exception_class' => $e::class, 'exception_message' => $e->getMessage(), 'context' => ['retry_of' => (string) $failure->id], ]); FormSubmissionActionFailure::query() ->whereKey($failure->id) ->update(['retry_count' => (int) $failure->retry_count + 1]); FormSubmission::query() ->whereKey($submission->id) ->update(['apply_status' => ApplyStatus::FAILED->value]); }); return ['id' => (string) $failure->id, 'submission' => (string) $submission->id, 'result' => 'failed-again']; } } }