seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); $this->schema = FormSchema::factory()->create(['organisation_id' => $this->org->id]); $this->admin = User::factory()->create(); $this->org->users()->attach($this->admin, ['role' => 'org_admin']); } private function toArray(FormSubmission $submission): array { $request = request(); Sanctum::actingAs($this->admin); return (new FormSubmissionResource($submission->fresh()))->toArray($request); } public function test_pending_status_serialises_as_nested_object(): void { $submission = FormSubmission::create([ 'form_schema_id' => $this->schema->id, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now(), 'is_test' => false, 'identity_match_status' => 'pending', ]); $array = $this->toArray($submission); $this->assertIsArray($array['identity_match']); $this->assertSame(['status' => 'pending'], $array['identity_match']); } public function test_null_status_serialises_as_null(): void { $submission = FormSubmission::create([ 'form_schema_id' => $this->schema->id, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now(), 'is_test' => false, ]); $array = $this->toArray($submission); $this->assertNull($array['identity_match']); } public function test_api_endpoint_returns_matching_shape(): void { Sanctum::actingAs($this->admin); $submission = FormSubmission::create([ 'form_schema_id' => $this->schema->id, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now(), 'is_test' => false, 'identity_match_status' => 'matched', ]); $response = $this->getJson("/api/v1/organisations/{$this->org->id}/forms/submissions/{$submission->id}"); $response->assertOk() ->assertJsonPath('data.identity_match.status', 'matched'); } /** * Per RFC-WS-6 §Q2 v1.3 — for non-person purposes the identity_match * block is null. ApplyBindings only writes identity_match_status='pending' * when the subject is a person; non-person purposes leave the column * NULL and the resource emits null. This test pins the contract per * non-person purpose so a future refactor can't silently drift. * * `event_registration` is intentionally excluded — that purpose IS * person-typed and identity_match is non-null after ApplyBindings runs. * * @return array */ public static function nonPersonPurposes(): array { return [ 'signature_contract' => [FormPurpose::SIGNATURE_CONTRACT], 'user_profile' => [FormPurpose::USER_PROFILE], 'incident_report' => [FormPurpose::INCIDENT_REPORT], 'post_event_evaluation' => [FormPurpose::POST_EVENT_EVALUATION], 'supplier_intake' => [FormPurpose::SUPPLIER_INTAKE], 'artist_advance' => [FormPurpose::ARTIST_ADVANCE], ]; } #[DataProvider('nonPersonPurposes')] public function test_identity_match_is_null_for_non_person_purpose(FormPurpose $purpose): void { $schema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'purpose' => $purpose->value, ]); $submission = FormSubmission::create([ 'form_schema_id' => $schema->id, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now(), 'is_test' => false, // identity_match_status intentionally not set; for non-person // purposes the column stays NULL. ]); $array = $this->toArray($submission); $this->assertNull( $array['identity_match'], "FormSubmissionResource.identity_match must be null for non-person purpose '{$purpose->value}' per RFC-WS-6 §Q2 v1.3.", ); } }