makeEventRegistrationSubmission(); $result = DB::transaction(fn (): BindingPassResult => $this->applicator()->apply($submission)); $this->assertSame(ApplyStatus::COMPLETED, $result->applyStatus()); $this->assertSame('person', $result->provisionedSubjectType); // Person was created with email + first_name + last_name from bindings. $person = Person::query()->withoutGlobalScopes()->where('email', 'jan@example.nl')->first(); $this->assertNotNull($person); $this->assertSame('Jan', $person->first_name); $this->assertSame('Jansen', $person->last_name); // Activity log: one pass-level + N child entries. $passActivity = Activity::query() ->where('description', 'form_submission.bindings_pass_completed') ->where('subject_id', $submission->id) ->first(); $this->assertNotNull($passActivity); $this->assertSame(2, (int) $passActivity->properties->get('binding_count')); $childActivities = Activity::query() ->where('description', 'form_submission.binding_applied') ->where('subject_id', $submission->id) ->get(); $this->assertCount(2, $childActivities); foreach ($childActivities as $child) { $this->assertSame((string) $passActivity->id, $child->properties->get('parent_activity_id')); } } public function test_no_transaction_guard_present(): void { // RefreshDatabase wraps every PHPUnit test in a transaction; the // guard is exercised via the listener path (ApplyBindingsOnFormSubmit) // which opens its own transaction explicitly. Verify the guard // exists in the source by checking for the throw of // FormBindingInfraException (per RFC-WS-6 §Q3 v1.3 addition 2 — the // 'no_transaction' developer-error maps onto temporary_error so the // GlitchTip alert + retry-after workflow fires). $reflection = new \ReflectionClass(FormBindingApplicator::class); $source = file_get_contents($reflection->getFileName()); $this->assertStringContainsString('FormBindingInfraException', $source); $this->assertStringContainsString('DB::transactionLevel()', $source); $this->assertStringContainsString('must be invoked inside DB::transaction', $source); } private function applicator(): FormBindingApplicator { return $this->app->make(FormBindingApplicator::class); } private function makeEventRegistrationSubmission(): FormSubmission { $event = Event::factory()->create(); $crowdType = CrowdType::factory()->create([ 'organisation_id' => $event->organisation_id, 'is_active' => true, ]); $schema = FormSchema::factory()->create([ 'organisation_id' => $event->organisation_id, 'purpose' => FormPurpose::EVENT_REGISTRATION->value, 'default_crowd_type_id' => $crowdType->id, ]); $emailField = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'field_type' => FormFieldType::EMAIL->value, 'slug' => 'email', ]); $emailBinding = FormFieldBinding::factory()->forField($emailField) ->entityOwned('person', 'email') ->create([ 'is_identity_key' => true, 'merge_strategy' => FormFieldBindingMergeStrategy::Overwrite->value, 'trust_level' => 80, ]); $firstNameField = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'slug' => 'first_name', ]); $firstNameBinding = FormFieldBinding::factory()->forField($firstNameField) ->entityOwned('person', 'first_name') ->create([ 'merge_strategy' => FormFieldBindingMergeStrategy::Overwrite->value, 'trust_level' => 70, ]); $lastNameField = FormField::factory()->create([ 'form_schema_id' => $schema->id, 'slug' => 'last_name', ]); $lastNameBinding = FormFieldBinding::factory()->forField($lastNameField) ->entityOwned('person', 'last_name') ->create([ 'merge_strategy' => FormFieldBindingMergeStrategy::Overwrite->value, 'trust_level' => 60, ]); $submission = FormSubmission::factory() ->forEvent($event) ->create([ 'form_schema_id' => $schema->id, 'subject_type' => 'person', ]); $submission->schema_snapshot = [ 'fields' => [ $this->snapshotField($emailField, $emailBinding, 'person', 'email', identityKey: true, trustLevel: 80), $this->snapshotField($firstNameField, $firstNameBinding, 'person', 'first_name', trustLevel: 70), $this->snapshotField($lastNameField, $lastNameBinding, 'person', 'last_name', trustLevel: 60), ], ]; $submission->save(); $this->writeValue($submission->id, $emailField->id, 'jan@example.nl'); $this->writeValue($submission->id, $firstNameField->id, 'Jan'); $this->writeValue($submission->id, $lastNameField->id, 'Jansen'); return $submission->fresh(); } private function writeValue(string $submissionId, string $fieldId, mixed $value): void { $row = new FormValue; $row->form_submission_id = $submissionId; $row->form_field_id = $fieldId; $row->setAttribute('value', $value); $row->value_anonymised = false; $row->save(); } /** * @return array */ private function snapshotField( FormField $field, FormFieldBinding $binding, string $entity, string $column, bool $identityKey = false, int $trustLevel = 50, ): array { return [ 'id' => (string) $field->id, 'slug' => (string) $field->slug, 'sort_order' => (int) $field->sort_order, 'bindings' => [[ 'id' => (string) $binding->id, 'mode' => 'entity_owned', 'entity' => $entity, 'column' => $column, 'merge_strategy' => 'overwrite', 'trust_level' => $trustLevel, 'is_identity_key' => $identityKey, ]], ]; } }