diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 2036eadd13..07069417db 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -173,10 +173,13 @@ def merge_default_values(submitter) next if value.blank? - acc[field['uuid']] = template_default_value_for_submitter(value, submitter, with_time: true) + value = template_default_value_for_submitter(value, submitter, with_time: true) + + acc[field['uuid']] = Submitters::NormalizeValues.normalize_value(field, value) end - default_values.compact_blank.merge(submitter.values) + # Reject blanks but keep explicit false (unchecked checkbox defaults), which compact_blank would drop. + default_values.reject { |_, v| v.blank? && v != false }.merge(submitter.values) end def build_formula_values(submitter) @@ -303,13 +306,21 @@ def check_field_condition(condition, submitter_values, fields_uuid_index) submitter_values[condition['field_uuid']].present? when 'equal', 'contains' field = fields_uuid_index[condition['field_uuid']] - option = field['options'].find { |o| o['uuid'] == condition['value'] } + option = field['options']&.find { |o| o['uuid'] == condition['value'] } + + # Stale option uuid (template edited after submission): no value can match a deleted option. + return false if option.nil? + values = Array.wrap(submitter_values[condition['field_uuid']]) values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}") when 'not_equal', 'does_not_contain' field = fields_uuid_index[condition['field_uuid']] - option = field['options'].find { |o| o['uuid'] == condition['value'] } + option = field['options']&.find { |o| o['uuid'] == condition['value'] } + + # Stale option uuid (template edited after submission): every value differs from a deleted option. + return true if option.nil? + values = Array.wrap(submitter_values[condition['field_uuid']]) values.exclude?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}") diff --git a/spec/lib/submitters/submit_values_spec.rb b/spec/lib/submitters/submit_values_spec.rb index 652abbf167..36595fee2e 100644 --- a/spec/lib/submitters/submit_values_spec.rb +++ b/spec/lib/submitters/submit_values_spec.rb @@ -84,4 +84,75 @@ end end end + + describe '.merge_default_values' do + let(:checkbox_field) do + { 'uuid' => SecureRandom.uuid, 'name' => 'Agree', 'type' => 'checkbox', + 'submitter_uuid' => submitter.uuid, 'default_value' => default_value } + end + + before do + fields = [checkbox_field] + + template.update!(fields: fields) + submission.update!(template_fields: fields, + template_submitters: [{ 'uuid' => submitter.uuid, 'name' => 'Customer' }]) + submitter.update!(values: {}) + end + + context 'with checkbox default_value "true"' do + let(:default_value) { 'true' } + + it 'persists boolean true' do + expect(described_class.merge_default_values(submitter)[checkbox_field['uuid']]).to be(true) + end + end + + context 'with checkbox default_value "false"' do + let(:default_value) { 'false' } + + it 'persists boolean false' do + expect(described_class.merge_default_values(submitter)[checkbox_field['uuid']]).to be(false) + end + + it 'evaluates checked/unchecked conditions as unchecked' do + values = described_class.merge_default_values(submitter) + + checked = { 'action' => 'checked', 'field_uuid' => checkbox_field['uuid'] } + unchecked = { 'action' => 'unchecked', 'field_uuid' => checkbox_field['uuid'] } + + expect(described_class.check_field_condition(checked, values, {})).to be(false) + expect(described_class.check_field_condition(unchecked, values, {})).to be(true) + end + end + end + + describe '.check_field_condition' do + let(:options_field) do + { 'uuid' => 'field_with_options', 'type' => 'multiple', + 'options' => [{ 'uuid' => 'option_1', 'value' => 'One' }] } + end + let(:fields_uuid_index) { { options_field['uuid'] => options_field } } + let(:values) { { options_field['uuid'] => ['One'] } } + + context 'when the condition points at a deleted option uuid' do + it 'does not raise and evaluates equal/contains as false' do + %w[equal contains].each do |action| + condition = { 'action' => action, 'field_uuid' => options_field['uuid'], 'value' => 'deleted_option_uuid' } + + expect { described_class.check_field_condition(condition, values, fields_uuid_index) }.not_to raise_error + expect(described_class.check_field_condition(condition, values, fields_uuid_index)).to be(false) + end + end + + it 'does not raise and evaluates not_equal/does_not_contain as true' do + %w[not_equal does_not_contain].each do |action| + condition = { 'action' => action, 'field_uuid' => options_field['uuid'], 'value' => 'deleted_option_uuid' } + + expect { described_class.check_field_condition(condition, values, fields_uuid_index) }.not_to raise_error + expect(described_class.check_field_condition(condition, values, fields_uuid_index)).to be(true) + end + end + end + end end