*/ use HasFactory; use HasUlids; protected $table = 'form_submission_action_failures'; protected $fillable = [ 'form_submission_id', 'listener_class', 'binding_id', 'failed_at', 'exception_class', 'exception_message', 'exception_trace', 'context', 'retry_count', 'resolved_at', 'resolved_by_user_id', 'resolved_note', 'dismissed_at', 'dismissed_by_user_id', 'dismissed_reason_type', 'dismissed_reason_note', ]; /** @var array */ protected $casts = [ 'failed_at' => 'datetime', 'resolved_at' => 'datetime', 'dismissed_at' => 'datetime', 'context' => 'array', 'retry_count' => 'int', 'dismissed_reason_type' => DismissalReasonType::class, ]; /** @return BelongsTo */ public function submission(): BelongsTo { return $this->belongsTo(FormSubmission::class, 'form_submission_id'); } /** @return BelongsTo */ public function binding(): BelongsTo { return $this->belongsTo(FormFieldBinding::class, 'binding_id'); } /** @return BelongsTo */ public function resolvedBy(): BelongsTo { return $this->belongsTo(User::class, 'resolved_by_user_id'); } /** @return BelongsTo */ public function dismissedBy(): BelongsTo { return $this->belongsTo(User::class, 'dismissed_by_user_id'); } /** * RFC-WS-6 Q5 addendum (sessie 3c) — per-attempt retry history. * `retry_count` on this model stays as denormalized cache; the * detail UI consumes this relation for per-attempt timeline. * * @return HasMany */ public function retryAttempts(): HasMany { return $this->hasMany(FormSubmissionActionFailureRetryAttempt::class, 'form_submission_action_failure_id') ->latest('attempted_at'); } /** * @param Builder $query * @return Builder */ protected function scopeOpen(Builder $query): Builder { return $query->whereNull('resolved_at')->whereNull('dismissed_at'); } /** * @param Builder $query * @return Builder */ protected function scopeResolved(Builder $query): Builder { return $query->whereNotNull('resolved_at'); } /** * @param Builder $query * @return Builder */ protected function scopeDismissed(Builder $query): Builder { return $query->whereNotNull('dismissed_at'); } public function isOpen(): bool { return $this->resolved_at === null && $this->dismissed_at === null; } /** * Sessie 3c (Q2 closure): a resolved failure also blocks retry — * retrying a closed failure would either no-op or trigger a * spurious state transition. Both are unwanted. Open is the only * retriable state. */ public function canBeRetried(): bool { return $this->resolved_at === null && $this->dismissed_at === null; } }