Files
crewli/api/app/Models/FormBuilder/FormSubmission.php
bert.hausmans 96062b9182 feat(form-builder): FormSubmission cast + factory state for failure_response_code
Per RFC-WS-6 §Q3 v1.3 addition 2.

- Added 'failure_response_code' to FormSubmission $fillable + 'string' cast.
  Plain string (not enum) — the exception subclass on
  form_submission_action_failures is the canonical classification source;
  this column is a denormalised mirror for response-shape rendering.
- Factory fluent state method withFailureResponseCode() with documentation
  of the four valid values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 02:00:18 +02:00

166 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models\FormBuilder;
use App\Enums\FormBuilder\ApplyStatus;
use App\Enums\FormBuilder\FormSubmissionReviewStatus;
use App\Enums\FormBuilder\FormSubmissionStatus;
use App\Models\Event;
use App\Models\Organisation;
use App\Models\Scopes\OrganisationScope;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* No direct activity-log hooks on this model: lifecycle events fire from the
* FormSubmissionService (arriving in S2) per ARCH §17.1.
*/
final class FormSubmission extends Model
{
use HasFactory;
use HasUlids;
use SoftDeletes;
protected static function booted(): void
{
self::addGlobalScope(new OrganisationScope);
}
/** @return array{column: string} */
public static function tenantScopeStrategy(): array
{
// Commit 2 added the denormalized organisation_id column
// (addendum Q2 — rapportage-hot exception).
return ['column' => 'organisation_id'];
}
protected $fillable = [
'form_schema_id',
'organisation_id',
'event_id',
'subject_type',
'subject_id',
'submitted_by_user_id',
'public_submitter_name',
'public_submitter_email',
'public_submitter_ip',
'public_submitter_ip_anonymised_at',
'status',
'review_status',
'reviewed_by_user_id',
'reviewed_at',
'review_notes',
'submitted_at',
'schema_version_at_open',
'schema_version_at_submit',
'schema_snapshot',
'submission_duration_seconds',
'auto_save_count',
'anonymised_at',
'is_test',
'submitted_in_locale',
'opened_at',
'first_interacted_at',
'idempotency_key',
'identity_match_status',
'failure_response_code',
];
/** @var array<string, string> */
protected $casts = [
'status' => FormSubmissionStatus::class,
'review_status' => FormSubmissionReviewStatus::class,
'apply_status' => ApplyStatus::class,
'apply_completed_at' => 'datetime',
// Plain string (not enum). The exception subclass on
// form_submission_action_failures is the canonical source of
// classification truth; this column is a denormalised mirror for
// response-shape rendering. Per RFC-WS-6 §Q3 v1.3 addition 2.
'failure_response_code' => 'string',
'schema_snapshot' => 'array',
'is_test' => 'bool',
'submitted_at' => 'datetime',
'reviewed_at' => 'datetime',
'anonymised_at' => 'datetime',
'opened_at' => 'datetime',
'first_interacted_at' => 'datetime',
'public_submitter_ip_anonymised_at' => 'datetime',
'schema_version_at_open' => 'int',
'schema_version_at_submit' => 'int',
'submission_duration_seconds' => 'int',
'auto_save_count' => 'int',
];
public function schema(): BelongsTo
{
return $this->belongsTo(FormSchema::class, 'form_schema_id');
}
public function organisation(): BelongsTo
{
return $this->belongsTo(Organisation::class);
}
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
public function subject(): MorphTo
{
return $this->morphTo();
}
public function submittedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'submitted_by_user_id');
}
public function reviewedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewed_by_user_id');
}
public function values(): HasMany
{
return $this->hasMany(FormValue::class);
}
public function sectionStatuses(): HasMany
{
return $this->hasMany(FormSubmissionSectionStatus::class);
}
public function delegations(): HasMany
{
return $this->hasMany(FormSubmissionDelegation::class);
}
/** @return HasMany<FormSubmissionActionFailure, $this> */
public function actionFailures(): HasMany
{
return $this->hasMany(FormSubmissionActionFailure::class);
}
/**
* RFC-WS-6 §3 (Q4) — submissions awaiting an applicator pass. Excludes
* NULL apply_status legacy rows by design (RFC O1).
*
* @param Builder<FormSubmission> $query
* @return Builder<FormSubmission>
*/
protected function scopePendingApply(Builder $query): Builder
{
return $query->where('apply_status', ApplyStatus::PENDING->value);
}
}