create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'subject']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'gate']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'region']); $service = app(FormFieldConditionalLogicService::class); $service->replaceLogic($field, [ 'operator' => FormFieldConditionalLogicGroupOperator::All->value, 'children' => [ ['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'], [ 'operator' => FormFieldConditionalLogicGroupOperator::Any->value, 'children' => [ ['field_slug' => 'region', 'operator' => 'equals', 'value' => 'NL'], ['field_slug' => 'region', 'operator' => 'equals', 'value' => 'BE'], ], ], ], ]); $this->assertSame(2, FormFieldConditionalLogicGroup::query()->count()); $this->assertSame(3, FormFieldConditionalLogicCondition::query()->count()); $this->assertDatabaseHas('activity_log', [ 'description' => 'field.conditional_logic_replaced', 'subject_type' => 'form_field', 'subject_id' => $field->id, ]); } public function test_replace_logic_is_transactional_and_replaces_existing_tree(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'subject']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'gate']); $service = app(FormFieldConditionalLogicService::class); $service->replaceLogic($field, [ 'operator' => 'all', 'children' => [ ['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'], ], ]); $this->assertSame(1, FormFieldConditionalLogicCondition::query()->count()); $service->replaceLogic($field, [ 'operator' => 'any', 'children' => [ ['field_slug' => 'gate', 'operator' => 'not_equals', 'value' => 'no'], ], ]); // Full replacement — old rows gone, new rows in place. $this->assertSame(1, FormFieldConditionalLogicGroup::query()->count()); $this->assertSame(1, FormFieldConditionalLogicCondition::query()->count()); $condition = FormFieldConditionalLogicCondition::query()->first(); $this->assertSame('not_equals', $condition->comparison_operator->value); } public function test_replace_logic_with_null_clears_tree(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'subject']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'gate']); $service = app(FormFieldConditionalLogicService::class); $service->replaceLogic($field, [ 'operator' => 'all', 'children' => [ ['field_slug' => 'gate', 'operator' => 'empty'], ], ]); $service->replaceLogic($field, null); $this->assertSame(0, FormFieldConditionalLogicGroup::query()->count()); $this->assertSame(0, FormFieldConditionalLogicCondition::query()->count()); } public function test_assert_specs_valid_rejects_unknown_operator(): void { $service = app(FormFieldConditionalLogicService::class); $this->expectException(InvalidConditionalLogicSpecException::class); $service->assertSpecsValid([ 'operator' => 'all', 'children' => [ ['field_slug' => 'x', 'operator' => 'nonsense_op', 'value' => 1], ], ]); } public function test_assert_specs_valid_rejects_root_condition(): void { $service = app(FormFieldConditionalLogicService::class); $this->expectException(InvalidConditionalLogicSpecException::class); $service->assertSpecsValid([ 'field_slug' => 'x', 'operator' => 'equals', 'value' => 'y', ]); } public function test_assert_specs_valid_rejects_empty_group(): void { $service = app(FormFieldConditionalLogicService::class); $this->expectException(InvalidConditionalLogicSpecException::class); $service->assertSpecsValid([ 'operator' => 'all', 'children' => [], ]); } public function test_replace_logic_rejects_unknown_field_slug(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'subject']); $service = app(FormFieldConditionalLogicService::class); $this->expectException(InvalidConditionalLogicSpecException::class); $this->expectExceptionMessageMatches('/ghost_slug/'); $service->replaceLogic($field, [ 'operator' => 'all', 'children' => [ ['field_slug' => 'ghost_slug', 'operator' => 'equals', 'value' => 'x'], ], ]); } public function test_to_json_shape_reconstructs_canonical_show_when(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'subject']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'gate']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'region']); $service = app(FormFieldConditionalLogicService::class); $service->replaceLogic($field, [ 'operator' => 'all', 'children' => [ ['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'], [ 'operator' => 'any', 'children' => [ ['field_slug' => 'region', 'operator' => 'equals', 'value' => 'NL'], ['field_slug' => 'region', 'operator' => 'empty'], ], ], ], ]); $shape = $service->toJsonShape($field->fresh()->rootConditionalLogicGroup()); $this->assertNotNull($shape); $this->assertSame([ 'show_when' => [ 'all' => [ ['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'], [ 'any' => [ ['field_slug' => 'region', 'operator' => 'equals', 'value' => 'NL'], ['field_slug' => 'region', 'operator' => 'empty'], ], ], ], ], ], $shape); } public function test_to_json_shape_returns_null_when_no_logic(): void { $service = app(FormFieldConditionalLogicService::class); $this->assertNull($service->toJsonShape(null)); } public function test_valueless_condition_stores_null_and_omits_value_in_json(): void { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $field = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'subject']); FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'gate']); $service = app(FormFieldConditionalLogicService::class); $service->replaceLogic($field, [ 'operator' => 'all', 'children' => [ ['field_slug' => 'gate', 'operator' => 'not_empty', 'value' => 'this_should_be_dropped'], ], ]); $condition = FormFieldConditionalLogicCondition::query()->first(); $this->assertNull($condition->value); $this->assertSame(FormFieldConditionalLogicConditionOperator::NotEmpty, $condition->comparison_operator); $shape = $service->toJsonShape($field->fresh()->rootConditionalLogicGroup()); $this->assertSame([ 'show_when' => [ 'all' => [ ['field_slug' => 'gate', 'operator' => 'not_empty'], ], ], ], $shape); } }