package LIMS::Validate;

# returns hashref for Data::FormValidator profile, or arrayref for FormValidator::Simple profile
# uses LIMS::Validate::Roles::ConstraintMethods for constraint_method helpers

use Data::Dumper;

use Moose; # all constraint_method helpers located in:
with 'LIMS::Validate::Roles::ConstraintMethods';

has models   => ( is => 'ro', isa => 'HashRef' ); # LIMS::Base::validation_models()
has messages => ( is => 'ro', isa => 'HashRef' ); # $self->cfg('msg')->{dfv_msgs}
has settings => ( is => 'ro', isa => 'HashRef' ); # $self->cfg('settings')

# has lab_tests_count  => ( is => 'ro', isa => 'Int', lazy_build => 1 );

__PACKAGE__->meta->make_immutable;

#-------------------------------------------------------------------------------
sub new_patient { # profile validated OK
	my $self = shift;

	my %profile = (
		field_filters => {
			last_name   => 'lc',
			first_name  => 'lc',
			middle_name => 'lc',
            unit_number => 'uc',
            nhs_number  => 'digit', # remove spaces & non-digits
            month => \&remove_jquery_watermark,
            year  => \&remove_jquery_watermark,
            day   => \&remove_jquery_watermark,
		},
        constraint_methods => {
			nhs_number => $self->join_these(
				$self->check_nhsno_is_valid(), # default msg OK
				$self->check_nhsno_is_unique(), # name = 'is_unique'
			),
			year => $self->get_date_constraints(),
        },
		msgs => {
			constraints => {
				is_unique   => $self->messages->{not_unique},
				yyyy_format => $self->messages->{year_digits},
				future_date => $self->messages->{future_date},
				valid_date  => $self->messages->{invalid_date},
			},
		},
	);

	# get js_new_patient() profile:
	my $js_new_patient = $self->js_new_patient(); # warn Dumper $js_new_patient;

    # if nhs_number optional (determined in js_new_patient):
    if ( grep $_ =~ 'nhs_number', @{ $js_new_patient->{optional} } ) {
        $profile{require_some} = { # require one from this group
            patient_id => [ 1, qw(nhs_number unit_number) ],
        };
    } # warn Dumper $js_new_patient;

	map {
		$profile{$_} = $js_new_patient->{$_};
	} keys %$js_new_patient; # warn Dumper \%profile;

    return \%profile;
}

sub js_new_patient {
	my $self = shift;

    my $settings = $self->settings; # warn Dumper $settings;

    my @required = qw(
        last_name first_name day month year gender referral_source_id
    );
    my @optional = qw( middle_name patient_number );

    $settings->{nhs_number_mandatory}
        ? push @required, 'nhs_number'
        : push @optional, 'nhs_number';

	return {
        required => \@required,
        optional => \@optional,
    }
}

#-------------------------------------------------------------------------------
sub edit_patient { # profile validated OK
	my $self = shift;

	# get js_edit_patient() profile:
	my $js_edit_patient = js_edit_patient(); # warn Dumper $js_edit_patient;

	my %profile = (
		field_filters => {
			last_name   => 'lc',
			first_name  => 'lc',
			middle_name => 'lc',
            nhs_number  => 'digit', # remove spaces & non-digits
		},
        constraint_methods => {
			nhs_number => $self->join_these(
				$self->check_nhsno_is_valid(),
				$self->check_param_ownership(['nhs_number','Patient']), # name = 'ownership'
			),
			year => $self->get_date_constraints(),
        },
		msgs => {
			constraints => {
				ownership   => $self->messages->{not_unique},
				yyyy_format => $self->messages->{year_digits},
				future_date => $self->messages->{future_date},
				valid_date  => $self->messages->{invalid_date},
			},
		},
	);

	# add js_edit_patient profile:
	map {
		$profile{$_} = $js_edit_patient->{$_};
	} keys %$js_edit_patient;

	push @{ $profile{optional} }, '_record_id'; # required if editing patient
	return \%profile;
}

# doesn't support constraint => sub {}
sub js_edit_patient {
	my @required = qw( last_name first_name day month year gender error_code_id );
	my @optional = qw( middle_name nhs_number use_patient_id this_record_only
		all_records request_id );
    return {
        required => \@required,
        optional => \@optional,
		require_some => { # require one from this group:
            confirm_change => [ 1, qw( all_records this_record_only ) ],
        },
    };
}

#-------------------------------------------------------------------------------
sub email_contacts {
	my $self = shift;

	# email_contacts uses a 2-col unique key, so need to supply both:
	my @fields = qw(type contact_address);

	return {
		required => [
			qw( display_name scope referral_source_id type contact_address
				status is_active )
		],
		optional => ['department_id','_record_id'], # _record_id for validation of edit submission
        constraint_methods => {
            contact_address => $self->check_email_is_valid(),
            type => $self->check_param_ownership(
				[ \@fields, 'EmailContact' ]
			),
        },
        msgs => {
            constraints => {
				ownership  => $self->messages->{not_unique},
            },
        },
	};
}

#-------------------------------------------------------------------------------
sub email_report {
	my $self = shift;

	return {
		required => [ qw(mailto sender) ],
        constraint_methods => {
			mailto => $self->check_email_is_valid(),
#            mailto => $self->check_secure_email_domain(), # done in C now
		},
        msgs => {
            constraints => {
#				foo => $self->messages->{invalid_domain}, # checked in C ::Request now
            },
		},
	}
}

#-------------------------------------------------------------------------------
sub new_request { # profile validated OK
	my $self = shift;

    my $settings = $self->settings; # warn Dumper $settings;

    # get request_number messages:
    my $msgs = $self->get_request_number_messages(); # formatted to share with ajax method
    # add block_reference message:
    $msgs->{constraints}->{block_reference} = $self->messages->{need_block_ref};
    # add date constraints:
    $msgs->{constraints}->{yyyy_format}    = $self->messages->{year_digits};
    $msgs->{constraints}->{future_date}    = $self->messages->{future_date};
    $msgs->{constraints}->{valid_date}     = $self->messages->{invalid_date};
    $msgs->{constraints}->{valid_datetime} = $self->messages->{invalid_datetime};

    my @required = qw( referral_source_id referrer_code specimen patient_id );
    my @optional = (
			qw( trial_id trial_number external_reference ), # request details
			qw( urgent private doi copy_to ), # additional options
			qw( storage research treatment monitoring ), # consent
			qw( day month year minute hour ), # datetime specimen taken
			'error_code_id',
    );

    # request_number is required unless auto-incrementing:
    push @required, 'request_number' unless $settings->{autoincrement_requests};

    return {
        required => \@required,
        optional => \@optional,
		field_filters => {
			specimen => 'uc',
            minute 	 => \&remove_jquery_watermark,
            month 	 => \&remove_jquery_watermark,
            hour 	 => \&remove_jquery_watermark,
            year     => \&remove_jquery_watermark,
            day      => \&remove_jquery_watermark,
		},
        constraint_methods => {
			request_number => $self->get_request_number_constraints(),
            specimen       => $self->get_new_request_specimen_constraints(),
			year 		   => $self->get_date_constraints(),
        },
		dependency_groups  => { # if either filled in, both required:
			# trial_group => [ qw( trial_id trial_number) ], # using dependencies
		},
		dependencies => {
			trial_number => 'trial_id', # make trial_id required if trial_no entered
		},
		msgs => $msgs,
    };
}

#-------------------------------------------------------------------------------
sub date_constraints {
	my $self = shift;
	return {
		optional => [ qw/constraint_type days date_from date_to/ ],
		constraint_methods => {
			date_from 	=> $self->check_EU_date_is_valid(),
			date_to   	=> $self->check_EU_date_is_valid(),
			days        => $self->is_integer(),
		},
		msgs => {
			constraints => {
				valid_date => $self->messages->{invalid_date},
				FV_num_int => $self->messages->{need_integer},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub change_password { # profile validated OK
	my $self = shift;
	return $self->get_change_password_profile();
}

#-------------------------------------------------------------------------------
sub js_change_pwd { # only for JavaScript::FormValidator - using FormValidator::Simple for server-side
    return {
        required => [ qw(old_password new_password new_password_confirm) ],
    };
}

#-------------------------------------------------------------------------------
sub change_user_location {
	return {
		required => [ qw(location_id password) ],
	}
}

#-------------------------------------------------------------------------------
sub change_user_email {
    my $self = shift;

	return {
		required => [ qw(email email2 password) ],
        constraint_methods => {
			email => $self->join_these(
                $self->check_email_is_valid(),
                $self->check_param_ownership([ 'email', 'User' ]),
            ),
            email2 => $self->check_emails_match(),
        },
		msgs => {
			constraints => {
                email_mismatch => 'e-mail addresses do not match',
            },
        },
	}
}

#-------------------------------------------------------------------------------
sub config_settings_profile { # profile not tested in t/validation_profiles/
	my $self = shift;

	# NB - new entries require entering in settings.txt as blank line
	# eg 'some_new_entry='
    return [
		admin_contact       	    => [ 'NOT_BLANK', 'EMAIL_LOOSE' ],
		admin_timeout       	    => [ 'NOT_BLANK', 'INT' ],
        application_name    	    => [ 'NOT_BLANK' ],
        autoincrement_requests      => [ 'NOT_BLANK', 'INT' ],
		default_user_timeout 	    => [ 'NOT_BLANK', 'INT' ],
        entries_per_page    	    => [ 'NOT_BLANK', 'INT' ],
        have_request_audit    	    => [ 'NOT_BLANK', 'INT' ],
        lab_name_abbreviation  	    => [ 'NOT_BLANK' ],
        lab_number_prefix   	    => [ 'NOT_BLANK' ],
        local_network_restriction   => [ 'NOT_BLANK', 'INT' ],
        login_display_all_cases     => [ 'NOT_BLANK', 'INT' ],
        local_prefix        	    => [ 'NOT_BLANK' ],
		max_request_number_length   => [ 'NOT_BLANK', 'INT' ],
		min_char_length     	    => [ 'NOT_BLANK', 'INT' ],
#        pas_address         	    => [ 'NOT_BLANK' ], # replaced by PDS
#        pas_pwd             	    => [ 'NOT_BLANK' ],
#        pas_username        	    => [ 'NOT_BLANK' ],
        print_run_max       	    => [ 'NOT_BLANK', 'INT' ],
		require_spell_check         => [ 'NOT_BLANK' ],
		report_display_notes        => [ 'NOT_BLANK' ],
        server_username        	    => [ 'NOT_BLANK' ],
        service_address        	    => [ 'NOT_BLANK' ],
		service_email          	    => [ 'NOT_BLANK', 'EMAIL_LOOSE' ],
        service_name         	    => [ 'NOT_BLANK' ],
        service_telno         	    => [ 'NOT_BLANK' ],
        smtp                	    => [ 'NOT_BLANK' ],
		unreported_duration         => [ 'NOT_BLANK', 'INT' ],
        use_specimen_filter   	    => [ 'NOT_BLANK', 'INT' ],
        use_clinical_trial_filter   => [ 'NOT_BLANK', 'INT' ],
        valid_recipient_domains     => [ 'NOT_BLANK' ],
	];
}

sub js_config_settings {
	my $self = shift;

	my $config_settings = $self->config_settings_profile; # warn Dumper $config_settings;

	# convert array(ref) to hash so we can extract keys:
	my %cfg = @$config_settings; # warn Dumper [ keys %cfg ];

	# required = arrayref of %cfg keys:
	return {
		required => [ keys %cfg ],
	};
}

#-------------------------------------------------------------------------------
sub search { # profile validated OK
	my $self = shift;

	return {
		optional => [ qw(
			last_name first_name nhs_number unit_number trial_id trial_number
			lab_number specimen_code referral_source_id referrer_name urgent
			copy_to external_reference private dob_day dob_month dob_year
            request_day request_month request_year date_extend sql_simple_search
			specific_year previous_days date_from date_to logic kwd patient_id
			id )
		],
        field_filters => { # remove dd/mm/yyyy jQuery watermarks if used:
            specimen_code => 'alphanum', # to fix eg 'xbl,'
            request_month => \&remove_jquery_watermark,
            request_year  => \&remove_jquery_watermark,
            request_day   => \&remove_jquery_watermark,
            dob_month     => \&remove_jquery_watermark,
            dob_year      => \&remove_jquery_watermark,
            dob_day       => \&remove_jquery_watermark,
        },
        dependency_groups  => { # if any one field is filled in, they all become required
            request_dates => [ qw(request_year request_month request_day) ],
            dob_dates     => [ qw(dob_year dob_month dob_day) ],
			sql_simple    => [ qw(sql_simple_search logic kwd) ],
        },
        constraint_methods => {
			request_year => $self->get_date_constraints('request'), # optional prefix
			dob_year     => $self->get_date_constraints('dob'), # optional prefix
			specimen_code => [ # TODO: join_these() doesn't work here
				$self->check_param_length(4), # prohibits multiple specimen search
				$self->get_specimen_constraints,
			],
            nhs_number => $self->check_nhsno_is_valid(),
        },
		msgs => {
			constraints => {
				yyyy_format => $self->messages->{year_digits},
				future_date => $self->messages->{future_date},
				max_length  => $self->messages->{alphanumeric},
				valid_date  => $self->messages->{invalid_date},
			},
		},
	}
}

# remove mi(n), hr, dd, mm & yyyy jQuery watermarks from date fields:
sub remove_jquery_watermark {
    my $v = shift; # warn $v;
    $v =~ s/^(mi(n?)|hr|dd|[Mm][Mm]|yyyy)$//o; # warn $v;
    $v; # returns expression (maybe empty)
}

sub remove_spaces {
    my $v = shift; # warn $v;
	$v =~ s/\s//g;
	return $v
}

#-------------------------------------------------------------------------------
sub error_codes { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( code description  active ) ],
		optional => [
			'_record_id', # for error_code validation
			'is_unique', # only required for new system err code, not report err codes
		],
		field_filters => {
			code => 'lc',
		},
        constraint_methods => {
			code => $self->check_param_ownership( ['code', 'ErrorCode'] ),
        },
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			}
		},
    }
}

#-------------------------------------------------------------------------------
sub error_codes_config {
	my $self = shift;

    return {
        required => 'type',
		optional => 'error_code_id', # collected as arrayref
    }
}

#-------------------------------------------------------------------------------
sub report_notifications {
	my $self = shift;

    my @required = qw( name type contact_address status is_active );
	my @optional = qw( department_id referral_source_id _referral_source_id
		identifier );
	my @require_some = qw( referral_source_id _referral_source_id identifier );
	return {
		required => \@required,
		optional => \@optional,
		constraint_methods => {
            contact_address => $self->check_email_is_valid({nhs_net => 1}), # nhs.net only
		},
		require_some => { # require one from this group
            id_type => [ 1, @require_some ],
        },
	}
}

#-------------------------------------------------------------------------------
sub report {
	my $self = shift;

    my @required = qw( status specimen_quality diagnosis_id );
    # clinical_details optional/required now determined by config
    my @optional = qw(
        comment
        teaching
        option_id
        authorise
        morphology
        biopsy_site
        no_new_report
        general_notes
        final_diagnosis
        gross_description
        followup_option_id
        secondary_diagnosis_id
		maybe_diagnosis_revision
        confirm_outstanding_tests
		delete_secondary_diagnosis
		confirm_outstanding_results

        _diagnosis_id
        _have_outstanding_tests
        _have_outstanding_results
    );
    $self->{settings}->{allow_optional_request_details}
        ? push @optional, 'clinical_details'
        : push @required, 'clinical_details';

	return {
		required => \@required,
		optional => \@optional,
		dependencies => {
			_have_outstanding_results => 'confirm_outstanding_results',
			_have_outstanding_tests   => 'confirm_outstanding_tests',
			_want_gross_description   => 'gross_description',
			_want_biopsy_details      => 'biopsy_site',
		},

		constraint_methods => {
            # require reason for changing diagnosis:
			_diagnosis_id => $self->check_report_params(),
            secondary_diagnosis_id =>
                $self->check_secondary_diagnosis('duplicate_diagnosis'),
		},
		msgs => {
			constraints => {
                duplicate_diagnosis    => $self->messages->{duplicate_diagnosis},
				diagnosis_resubmission => $self->messages->{report_confirm},
			}
		}
	}
}

#-------------------------------------------------------------------------------
sub general_notes { # doesn't need validation
	return {
		optional => [ 'general_notes' ],
	}
}

#-------------------------------------------------------------------------------
sub anatomical_sites {
	my $self = shift;
	return {
		required => [ 'site_name', 'snomed' ],
		field_filters => {
			site_name   => 'lc',
			snomed  	=> 'uc',
		},
        constraint_methods => {
			snomed => $self->check_snomed_code(),
		},
	}
}

#-------------------------------------------------------------------------------
sub report_error_code {
	my $self = shift;

    return {
        required => [ qw( code description active ) ],
		optional => '_record_id', # for error_code validation
    }
}

#-------------------------------------------------------------------------------
sub request_error { # profile validated OK
	return {
		optional => [ qw(error_code_id LIC error_details request_error_code_id) ],
        require_some => { # require one from this group:
            error_group => [ 1, qw(LIC error_code_id request_error_code_id) ],
        },
	}
}

#-------------------------------------------------------------------------------
sub specimens { # profile validated OK
	my $self = shift;

    my $settings = $self->settings;

    my @required = qw( sample_code description active );
    push @required, 'sample_type_id' if $settings->{lab_section_sample_type};

    return {
        required => \@required,
		optional => '_record_id', # for sample_code validation
		field_filters => {
			sample_code => 'uc',
		},
		constraint_methods => {
			sample_code => $self->join_these(
				$self->check_param_length(4),
				$self->check_no_spaces(),
				$self->check_param_ownership( ['sample_code', 'Specimen'] ),
			),
        },
		msgs => {
			constraints => {
				max_length => $self->messages->{too_long},
				no_spaces  => $self->messages->{single_word},
				ownership  => $self->messages->{not_unique},
			},
		},
    }
}

#-------------------------------------------------------------------------------
sub initial_screen { # profile validated OK
    my $self = shift;

    my $settings = $self->settings;

    my @required = qw(screen_id);
    push @required, 'option_id' if $settings->{have_request_audit};

	return {
		required => \@required,
	}
}

#-------------------------------------------------------------------------------
sub referral_sources { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( referral_type organisation_code is_active ) ],
		optional => [ qw( parent_organisation_id display_name practice_name
			practice_address practice_zip _record_id is_branch parent_practice_id
			) ],
		field_filters => {
			organisation_code => 'uc',
		},
        require_some => { # require one from this group
            source_name => [ 1, qw(display_name practice_name) ],
        },
        dependency_groups  => { # if any one field is filled in, they all become required
            practices => [ qw(practice_name	practice_address practice_zip) ],
        },
		dependencies => {
			# if referral_type is hospital, require parent_organisation:
			referral_type => {
				hospital => [ qw( parent_organisation_id ) ],
			},
			is_branch => 'parent_practice_id',
		},
		constraint_methods => {
			organisation_code => $self->join_these(
				$self->check_param_length(6),
				$self->check_no_spaces(),
				$self->check_param_ownership( ['organisation_code', 'ReferralSource'] ),
			),
			display_name =>
				$self->check_param_ownership( ['display_name', 'ReferralSource'] ),
		},
		msgs => {
			constraints => {
				max_length => $self->messages->{too_long},
				no_spaces  => $self->messages->{single_word},
				ownership  => $self->messages->{not_unique},
			},
		},
    }
}

#-------------------------------------------------------------------------------
sub general_practitioners { # edit function only - insufficient to require tests

    return {
        required => [ qw( name active ) ], # can only modify these 2 fields
		field_filters => {
			name => 'uc',
		},
    }
}

#-------------------------------------------------------------------------------
sub new_clinician { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( prefix national_code surname initials_1 referral_source_id
			hospital_department_code ) ],
		optional => [ qw(initials_2 initials_3 initials_4) ],
		field_filters => {
			national_code => 'uc',
		},
		constraint_methods => {
			national_code => $self->join_these(
				$self->check_no_spaces(),
				$self->check_numeric(), # name = 'numeric_format'
				$self->check_param_length(7),
			),
        },
		msgs => {
			constraints => {
				numeric_format => $self->messages->{numbers_only},
				max_length => $self->messages->{too_high},
				# no_spaces => $self->messages->{single_word}, # default OK
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub edit_referrer { # profile validated OK
	my $self = shift;

	return {
		required => [ qw(name national_code referral_type_id active) ],
		optional => '_record_id',
		constraint_methods => {
			national_code => $self->join_these(
				$self->check_referrer_code(), # name = 'referrer_code'
				$self->check_param_ownership( ['national_code', 'Referrer'] ),
			),
        },
		msgs => {
			constraints => {
				referrer_code => $self->messages->{referrer_code},
				ownership     => $self->messages->{not_unique},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub new_location { # profile validated OK

    return {
        required => [ qw( referral_source_id ) ],
        optional => [ qw( unit_number ) ],
    }
}

#-------------------------------------------------------------------------------
sub diagnosis { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( name diagnostic_category_id active new_labtests ) ],
		optional => [ qw( icdo3 _record_id sub_category_id ) ],
		constraint_methods => {
			name => $self->check_param_ownership( ['name', 'Diagnosis'] ),
		},
		dependencies => { # make ICDO3 required if sub_category_id entered
			sub_category_id => 'icdo3',
		},
		msgs => {
			constraints => {
				ownership  => $self->messages->{not_unique},
			},
		},
    }
}

#-------------------------------------------------------------------------------
sub diagnostic_categories { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( description category_type active ) ],
		optional => '_record_id',
		constraint_methods => {
			description	=> $self->check_param_ownership(
				['description', 'DiagnosticCategory']
			),
		},
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			},
		},
    }
}

#-------------------------------------------------------------------------------
sub diagnosis_context { # validation not required
    return {
        required => [ qw( diagnosis_id context_warning_id ) ],
    }
}

#-------------------------------------------------------------------------------
sub diagnosis_change { # validation not required
    return {
        required => [ qw( option_name description is_active ) ],
    }
}

#-------------------------------------------------------------------------------
sub context_warnings { # validation not required
    return {
        required => [ qw( description is_active ) ],
    }
}

#-------------------------------------------------------------------------------
sub patient_case { # validation not tested
    return {
        optional => [ 'unit_number' ],
    }
}

#-------------------------------------------------------------------------------
sub patient_merge { # validation not tested
    return {
        require_some => { # require ONE from this group - need to OMIT leading
			# digit of arrayref (defaults to 1 anyway) as it gets included in
			# $data->valid, which causes fatal error if pat.id = 1 used (eg test suite):
            patient_id => [ undef, qw(last_name unit_number dob_year) ],
        },
        dependency_groups  => { # if any one field is filled in, they all become required
            dates => [ qw(dob_year dob_month dob_day) ],
        },
        optional => [ qw(last_name first_name unit_number dob_year dob_month dob_day) ],
    }
}

#-------------------------------------------------------------------------------
sub trials { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( trial_name active ) ],
		optional => '_record_id',
		constraint_methods => {
			trial_name	=> $self->check_param_ownership(
				['trial_name', 'ClinicalTrial']
			),
		},
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			},
		},
    }
}

#-------------------------------------------------------------------------------
sub screening_terms { # profile validated OK
	my $self = shift;

    my @fields = qw(description category_id); # 2-col unique key

    return {
        required => [ qw(description category_id active) ],
		optional => '_record_id',
		constraint_methods => {
			description => $self->check_param_ownership([ \@fields, 'Screen' ]),
		},
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			},
		},
    };
}

#-------------------------------------------------------------------------------
sub lab_tests { # profile validated OK
	my $self = shift;

	# lab_tests uses a 3-col unique key, so need to supply all 3:
	my @fields = qw(test_name lab_section_id test_type);

    return {
		required => [
			qw(field_label test_name lab_section_id test_type has_results is_active)
		],
		optional => [ '_record_id', 'sample_type_id', 'data_type_id' ],
		dependencies => {
			has_results => {
				yes => [ 'data_type_id' ], # data_type required if has_results = yes
			},
		},
        field_filters => {
			test_name => 'lc',
        },
        constraint_methods => {
			field_label => $self->join_these(
				$self->check_param_length(25),
				$self->check_param_ownership([ \@fields, 'LabTest' ]),
			),
            test_name => $self->check_no_spaces(),
		},
		msgs => {
			constraints => {
				ownership  => $self->messages->{not_unique},
				max_length => $self->messages->{too_long},
                no_spaces  => $self->messages->{single_word},
			},
		},
	};
}

#-------------------------------------------------------------------------------
sub lab_sections { # profile validated OK
	my $self = shift;

    my $settings = $self->settings;

    my @required = qw( section_name has_result_summary has_section_notes
		has_test_sign_out has_results_import has_foreign_id auto_expand is_active
	);
    push @required, 'sample_type_id' if $settings->{lab_section_sample_type};
    push @required, 'has_labels' if $settings->{have_label_printer};

    return {
        required => \@required,
		optional => '_record_id',
		constraint_methods => {
			section_name => $self->check_param_ownership(
				[ 'section_name', 'LabSection' ]
			),
		},
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			},
		},
    };
}

#-------------------------------------------------------------------------------
sub screen_categories {
	my $self = shift;

    return {
        required => [ qw( name is_active ) ],
		optional => '_record_id',
		constraint_methods => {
			category_name => $self->check_param_ownership(
				[ 'name', 'ScreenCategory' ]
			),
		},
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			},
		},
    };
}

#-------------------------------------------------------------------------------
sub result_data_types {
	my $self = shift;

	return {
        required => [ qw( description is_active ) ],
		optional => '_record_id',
		constraint_methods => {
			description => $self->check_param_ownership(
				[ 'description', 'LabTestResultDataType' ]
			),
		},
		msgs => {
			constraints => {
				ownership => $self->messages->{not_unique},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub parent_organisations { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( parent_code description referral_type ) ],
		optional => '_record_id',
		field_filters => {
			description => 'uc', # needs to be UPPERCASE
			parent_code => 'uc',
		},
		constraint_methods => {
			parent_code => $self->join_these(
				$self->check_param_length(6),
				$self->check_no_spaces(),
				$self->check_param_ownership(
					[ 'parent_code', 'ParentOrganisation' ]
				),
			),
			description => $self->check_param_ownership(
				[ 'description', 'ParentOrganisation' ]
			),
		},
		msgs => {
			constraints => {
				max_length => $self->messages->{too_long},
				ownership  => $self->messages->{not_unique},
				no_spaces  => $self->messages->{single_word},
			},
		},
    };
}

=begin  if we need this, move it to Roles::ConstraintMethods
#-------------------------------------------------------------------------------
sub patient_data { # uses lyo.kato DatePlugin (DATE_GREATER_THAN)

    return [
        { date => [ qw(year month day) ] } => [ 'DATE' ],
        { grouped => [qw(year month day)] } => [ ['DATE_GREATER_THAN', DateTime->now ] ],
    ];
}
=cut

#-------------------------------------------------------------------------------
sub patient_search_data { # profile validated OK
	my $self = shift;

    return {
        optional => [ qw(patient_id name year month day patient_no) ], # id from edit_patient rtn
        require_some => {
            data => [ 1, qw(patient_id name year patient_no) ],
        },
        field_filters => { # remove dd/mm/yyyy jQuery watermarks if used:
            patient_no => \&remove_spaces, # in case NHS no in xxx xxxx xxxx format
            month => \&remove_jquery_watermark,
            year  => \&remove_jquery_watermark,
            day   => \&remove_jquery_watermark,
        },
        dependency_groups  => { # if any one field is filled in, they all become required
            dates => [ qw(year month day) ],
        },
        constraint_methods => {
			year => $self->get_date_constraints()
        },
		msgs => {
			constraints => {
				yyyy_format => $self->messages->{year_digits},
				future_date => $self->messages->{future_date},
				valid_date  => $self->messages->{invalid_date},
			},
		},
        # untaint_all_constraints => 1,
    };
}

#-------------------------------------------------------------------------------
sub user_functions { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( function_name function_detail active ) ],
		optional => '_record_id',
        constraint_methods => {
			function_name => $self->join_these(
				$self->check_no_spaces(),
				$self->check_param_ownership(
					[ 'function_name', 'UserFunction' ]
				),
			),
			function_detail => $self->check_param_ownership(
					[ 'function_detail', 'UserFunction' ]
			),
		},
        msgs => {
            constraints => {
                no_spaces => $self->messages->{single_word},
				ownership => $self->messages->{not_unique},
            },
        },
    };
}

#-------------------------------------------------------------------------------
sub user_locations { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( location_name active ) ], # email_contact
		optional => [ qw( _record_id region_code ) ],
		constraint_methods => {
			location_name => $self->check_param_ownership(
				[ 'location_name', 'UserLocation' ]
			),
		},
        msgs => {
            constraints => {
				ownership => $self->messages->{not_unique},
            },
        },
    };
}

#-------------------------------------------------------------------------------
sub user_groups { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( group_name group_label group_detail active ) ],
		optional => '_record_id',
		constraint_methods => {
			group_name => $self->join_these(
				$self->check_no_spaces(),
				$self->check_param_ownership(
					[ 'group_name', 'UserGroup' ]
				),
			),
			group_label => $self->join_these(
				$self->check_param_ownership(
					[ 'group_label', 'UserGroup' ]
				),
			),
		},
        msgs => {
            constraints => {
                no_spaces => $self->messages->{no_spaces},
				ownership => $self->messages->{not_unique},
            },
        },
    };
}

#-------------------------------------------------------------------------------
sub user_details { #  Admin / User administration / User Manager function
	my $self = shift;
	# uses same profile as new_user(), except requires username
    my $profile = $self->new_user;
	push @{ $profile->{required} }, 'username'; # warn Dumper $profile;
	# also requires check of username param ownership:
	$profile->{constraint_methods}->{username} = $self->join_these(
		$self->check_param_ownership([ 'username', 'User' ]),
		$self->check_username(),
	);
	$profile->{msgs}->{constraints}->{username} = $self->messages->{username};
	  # warn Dumper $profile;
	return $profile;
}
#-------------------------------------------------------------------------------
sub new_user { # profile shared by 'Register New User' & 'User Manager' functions
	my $self = shift;

    my @name = qw(first_name last_name);

	my $settings = $self->settings;

    my @optional = qw( _record_id location ); # for validation of edited user
    my @required = qw( first_name last_name full_name password user_location_id
       email designation group_id active service );

    return {
		optional => \@optional,
        required => \@required,
		field_filters => {
			last_name  => 'lc',
			first_name => 'lc',
			username   => 'lc',
            email      => 'lc',
        },
		constraint_methods => {
			last_name  => $self->check_username(), # letters, apostrophies & hyphens only
            full_name  => $self->check_param_ownership([ \@name, 'User' ]),
			service	   => $self->check_lims_service($settings->{_centre}),
			email => $self->join_these(
                $self->check_email_is_valid(),
                $self->check_param_ownership([ 'email', 'User' ]),
            ),
		},
        msgs => {
            constraints => {
				ownership => $self->messages->{not_unique},
				lims_service => $self->messages->{lims_service},
            },
        },
    };
}

#-------------------------------------------------------------------------------
sub user_message {
	return {
		optional => [ 'user_location' ], # not required if using 'PM' link
		required => [ qw( user_id message) ],
	}
}

#-------------------------------------------------------------------------------
sub result_summary_options { # profile validated OK
	my $self = shift;

	# result_summary_options uses a 2-col unique key, so need to supply both:
	my @fields = qw(description lab_section_id);

    return {
        required => [ qw( description lab_section_id is_active ) ],
        optional => [ qw( _record_id ) ],
        constraint_methods => {
            description => $self->check_param_ownership(
				[ \@fields, 'ResultSummaryOption' ]
			),
        },
        msgs => {
            constraints => {
				ownership  => $self->messages->{not_unique},
            },
        },
    }
}

#-------------------------------------------------------------------------------
sub update_results { #
	return {
		required => [ qw( _section_id _section_name) ], # _section_name only used with foreign_id
		optional => [
			'test_id',            # investigation/test id(s)
			'section_notes',      # textarea entry
			'lab_section_id',     # lab_section.id
			'foreign_id',		  # free text entry (maybe move to results_data_entry)
            'retain_req_status',  # override status_option change
		],
	}
}

#-------------------------------------------------------------------------------
sub results_data_entry {
	return {
        required => [ qw( _section_name _section_id ) ],
		optional => [
			'results_summary',    # free text entry
			'_results_summary',   # drop-down entry
			'complete_all_tests', # checkbox
		],
	}
}

#-------------------------------------------------------------------------------
sub update_request { #
	my $self = shift;

	return {
		required => [ qw( error_code_id ) ], # for both data types
		optional => [ qw(
			unit_number referral_source_id _location_name scope
			_referrer referrer_code specimen_code trial_id trial_number urgent
			external_reference private doi copy_to storage treatment research
			monitoring use_patient_case_id day month year hour minute
		) ],
		field_filters => {
            minute 	 => \&remove_jquery_watermark,
            month 	 => \&remove_jquery_watermark,
            hour 	 => \&remove_jquery_watermark,
            year     => \&remove_jquery_watermark,
            day      => \&remove_jquery_watermark,
		},
        dependency_groups  => { # if any one field is filled in, they all become required
			_request_data => [ qw( _request_data specimen_code _referrer ) ] # _request_data is hidden flag
        },
		dependencies => {
			trial_number => 'trial_id', # make trial_id required if trial_no entered
			scope => '_location_name', # referral_source_id only submitted if changed
		},
        constraint_methods => {
            specimen_code => $self->get_specimen_constraints(),
			year 		  => $self->get_date_constraints(),
        },
        msgs => {
            constraints => {
                yyyy_format    => $self->messages->{year_digits},
                future_date    => $self->messages->{future_date},
                valid_date     => $self->messages->{invalid_date},
                valid_datetime => $self->messages->{invalid_datetime},
            },
        }
	}
}

#-------------------------------------------------------------------------------
sub delete_request { # # profile validated OK
	return {
		required => [ qw( confirm_delete reason ) ],
		optional => 'delete_patient_record',
	}
}
=begin # example of message construction which works with JS::DVF
		constraints => {
			reason  => [
				{ name => 'no_spaces', constraint => '/^\\S*$/' },
				{ name => 'word_chars', constraint => '/^\\w+$/' }
			],
		},
		msgs => {
			constraints => {
				no_spaces => 'no spaces allowed',
				word_chars => 'require word chars',
			},
		},
=cut

#-------------------------------------------------------------------------------
sub request_audit_options {
	my $self = shift;

    return {
        required => [ qw( description category_id active ) ],
        optional => [ qw( _record_id ) ],
        constraint_methods => {
            description => $self->check_param_ownership(
				['description', 'AuditRequestOption']
			),
        },
        msgs => {
            constraints => {
				ownership  => $self->messages->{not_unique},
            },
        },
    }
}

#-------------------------------------------------------------------------------
sub consent_options {
	my $self = shift;

    return {
        required => [ qw( consent_name consent_label is_active ) ],
        optional => [ qw( _record_id ) ],
        constraint_methods => {
            consent_name => $self->join_these(
				$self->check_no_spaces(),
				$self->check_param_ownership(['consent_name', 'ConsentOption']),
			),
            consent_label => $self->join_these(
				$self->check_no_spaces(),
				$self->check_param_ownership(['consent_label', 'ConsentOption']),
			),
        },
        msgs => {
            constraints => {
				ownership  => $self->messages->{not_unique},
				no_spaces => $self->messages->{single_word},
            },
        },
    }
}

#-------------------------------------------------------------------------------
sub request_options {
	my $self = shift;

    return {
        required => [ qw( option_name option_label is_active ) ],
        optional => [ qw( _record_id ) ],
        constraint_methods => {
            consent_name => $self->join_these(
				$self->check_no_spaces(),
				$self->check_param_ownership(['option_name', 'RequestOption']),
			),
            consent_label => $self->join_these(
				$self->check_no_spaces(),
				$self->check_param_ownership(['option_label', 'RequestOption']),
			),
        },
        msgs => {
            constraints => {
				ownership  => $self->messages->{not_unique},
				no_spaces => $self->messages->{single_word},
            },
        },
    }
}

#-------------------------------------------------------------------------------
sub request_audit_categories { # profile validated OK
	my $self = shift;

    return {
        required => [ qw( description active ) ],
        optional => [ qw( _record_id ) ],
        constraint_methods => {
            description => $self->check_param_ownership(
				['description', 'AuditRequestCategory']
			),
        },
        msgs => {
            constraints => {
				ownership  => $self->messages->{not_unique},
            },
        },
    }
}

#-------------------------------------------------------------------------------
sub referrer_search { # search profile - doesn't need validating
	my @fields = qw(name national_code);

	return {
        optional => \@fields,
        require_some => {
            data => [ 1, @fields ],
        },
	}
}

#-------------------------------------------------------------------------------
sub referral_source_search { # search profile - doesn't need validating
	my @fields = qw(display_name organisation_code referral_type_id);

	return {
        optional => \@fields,
        require_some => {
            data => [ 1, @fields ],
        },
	}
}

#-------------------------------------------------------------------------------
sub parent_organisation_search { # search profile - doesn't need validating
	my @fields = qw(description parent_code referral_type_id);

	return {
        optional => \@fields,
        require_some => {
            data => [ 1, @fields ],
        },
	}
}

#-------------------------------------------------------------------------------
sub phonelog { # profile validated OK
	my @optionals = qw( summary comments );

	return {
		required => [ qw( user_id status contact ) ],
		optional => \@optionals,
        require_some => {
            text => [ 1, @optionals ],
        },
	}
}

#-------------------------------------------------------------------------------
sub worklist { # profile only used for obtaining form params
	my $self = shift;

	return {
		required => [ qw( lab_section_id ) ],
		optional => [
			qw( lab_test_id test_status status_option_id lab_number_from sort_by
				request_id
			),
		],
	}
}

#-------------------------------------------------------------------------------
sub worklist_update_status {
	return {
		required => [ qw( user_id status_option_id ) ],
		dependencies => {
			status_option_id => {
				3 => [ 'confirm_delete' ], # 3 = DELETE
			},
		}
	}
}

#-------------------------------------------------------------------------------
sub histology_processing { # just used for valid fields:
	return {
		required => [ qw(user_id lab_section_id lab_test_id) ], # drop-down & 2 hidden fields
		optional => [
			qw(frozen_tissue pieces_blocks), # results fields
			# status fields:
			'initial cut-up', 'prepared dabs', 'cells for flow', 'final cut-up',
            'complete',
		],
	}
}

#-------------------------------------------------------------------------------
sub histology_staining { # just used for valid fields:
	return {
		required => [ qw(user_id lab_section_id lab_test_id) ], # drop-down & 2 hidden fields
		optional => [
			'haematoxylin_eosin', # results fields
			# status fields:
			'block out', 'microtomy', 'H & E stain', 'giemsa stain', 'QC',
			'complete',
		],
	}
}

#-------------------------------------------------------------------------------
sub histology_immunochemistry { # just used for valid fields:
	return {
		required => [ qw(user_id lab_section_id lab_test_id) ], # drop-down & 2 hidden fields
		optional => [
			'haematoxylin_eosin', # results fields
			# status fields:
			'microtomy', 'ICC staining', 'QC', 'complete',
		],
	}
}

#-------------------------------------------------------------------------------
sub hmrn_new_diagnoses {
    my @fields = qw(previous_week duration date_from date_to lab_number
		force_tmpl request_id);
	return {
		optional => \@fields,
        require_some => { # require at least one from this group
            duration_args => [ 1, @fields ],
        },
	}
}

#-------------------------------------------------------------------------------
sub hmrn_antecedent {
	return {
        required => [ 'event_id '],
    }
}

#-------------------------------------------------------------------------------
sub hmrn_chronologies {
	my $self = shift;

	return {
		required => [], # none
		optional => [ qw( diagnosis first_appointment palliative_care deceased
			mdt_date ) ],
		constraint_methods => {
			first_appointment 	=> $self->check_EU_date_is_valid(),
			palliative_care 	=> $self->check_EU_date_is_valid(),
			diagnosis 			=> $self->check_EU_date_is_valid(),
			deceased 			=> $self->check_EU_date_is_valid(),
			mdt_date 			=> $self->check_EU_date_is_valid(),
		},
		msgs => {
			constraints => {
				valid_date => $self->messages->{invalid_date},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub hmrn_mdt_date {
	my $self = shift;
	return {
		required => [ 'date' ],
		constraint_methods => {
			date => $self->check_EU_date_is_valid(),
		},
		ajax_methods => { # for jQuery ajax call
			date   => 'check_date_valid',
		},
		msgs => {
			constraints => {
				valid_date => $self->messages->{invalid_date},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub hmrn_imaging {
	my $self = shift;

    my $constraint_methods
        = $self->get_hmrn_param_constraints([ qw/deauville suv_max/ ]);
    $constraint_methods->{scan_date} = $self->check_EU_date_is_valid();

	return {
		required => [ qw(dataset scan_type scan_stage scan_date) ],
		optional => [ qw(option_id details suv_max deauville) ],
		ajax_methods => { # for jQuery ajax call
			scan_date => 'check_date_valid',
		},
		constraint_methods => $constraint_methods,
		msgs => {
			constraints => {
				valid_date => $self->messages->{invalid_date},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub hmrn_referral {
	my $self = shift;

	return {
		required => [
			qw(referral_date date_first_seen referral_type_id from_source_id
				to_source_id)
		],
		optional => [ qw() ],
		constraint_methods => {
			referral_date => $self->join_these(
                $self->check_EU_date_is_valid(),
                $self->check_date_order( [ qw(referral_date date_first_seen) ] ),
            ),
			date_first_seen => => $self->join_these(
                $self->check_EU_date_is_valid(),
                $self->check_date_order( [ qw(referral_date date_first_seen) ] ),
            ),
		},
		ajax_methods => { # for jQuery ajax call
			referral_date => 'check_date_valid',
			date_first_seen => 'check_date_valid',
		},
		msgs => {
			constraints => {
				valid_date => $self->messages->{invalid_date},
                date_order => $self->messages->{date_order},
			},
		},
	};
}

#-------------------------------------------------------------------------------
sub hmrn_treatment {
	my $self = shift;

	my $hmrn_treatment_types = $self->models->{hmrn_treatment_types};

    # force creation of hashref map:
    my $txTypeIdFor = &$hmrn_treatment_types(); # warn Dumper $txTypeIdFor;

    my $clinical_trial_id = $txTypeIdFor->{'clinical trial'}; # warn Dumper $clinical_trial_id;
    my $chemotherapy_id   = $txTypeIdFor->{'chemotherapy'}; # warn Dumper $chemotherapy_id;

    return {
		required => [ qw( location_id tx_type_id ) ], # minimum data set
		optional => [ qw( start_date end_date response_id tx_detail_id ) ],
		constraint_methods => {
            start_date => $self->join_these(
                $self->check_EU_date_is_valid(),
                $self->check_date_order( [ qw(start_date end_date) ] ),
            ),
            end_date => $self->join_these(
                $self->check_EU_date_is_valid(),
                $self->check_date_order( [ qw(start_date end_date) ] ),
            ),
		},
		dependencies => {
			# if tx_type = chemoTx or trial, require tx_detail:
			tx_type_id => {
				$clinical_trial_id => [ qw( tx_detail_id ) ],
				$chemotherapy_id   => [ qw( tx_detail_id ) ],
			},
		},
		ajax_methods => { # for jQuery ajax call
			start_date => 'check_date_valid',
			end_date   => 'check_date_valid',
		},
		msgs => {
			constraints => {
				valid_date => $self->messages->{invalid_date},
                date_order => $self->messages->{date_order},
			},
		},
	}
}

#-------------------------------------------------------------------------------
sub hmrn_demographics {
	my $self = shift;

	my @optional = qw( gp_id  practice_id );

	return {
		required => [ qw( address post_code status ) ],
		# GP can only be selected AFTER practice - default loaded in Model if absent
		# and practice id not re-submitted when GP submitted:
		optional => \@optional,
		require_some => { # require one from this group
            gp_or_practice => [ 1, @optional ],
        },
		constraint_methods => {
			post_code => $self->check_post_code_is_valid(),
		},
	}
}

#-------------------------------------------------------------------------------
sub hmrn_myeloid {
    my $self = shift;

	my @optional = qw( epo transfusion cytopenias karyotype cd34 );    # JD -  removed 'rcm'

	my @required = qw( splenomegaly detection_spleen hepatomegaly detection_liver
					   wbc hb pcv neutrophils monocytes plts
                       ecog lymphs haematocrit_vol
					);

    my $constraint_methods
        = $self->get_hmrn_param_constraints([ @required, @optional ]);

	return {
		required => \@required,
		optional => \@optional,
        require_some => { # require at least one from this group
            mds_or_mpd => [ 1, @optional ],
        },
        dependency_groups => { # if any one field is filled in, they all become required
			mds => [ qw( transfusion cytopenias karyotype cd34 ) ], # MDS fields
			mpd => [ qw( epo ) ], # MPD fields (JD - removed rcm)
        },
        constraint_methods => $constraint_methods,
	}
}

#-------------------------------------------------------------------------------
sub hmrn_lymphoid {
    my $self = shift;

	my @optional = qw( binet plts stage );
    my @required = qw( ecog hb bm wbc sweats lymphs fever albumin wt_loss b2m
					   ct ldh paraprotein pp_level haematocrit_vol neutrophils
                       monocytes igs
					);

    my $constraint_methods
        = $self->get_hmrn_param_constraints([ @required, @optional ]);

	return {
		required => \@required,
		optional => \@optional,
        require_some => { # require at least one from this group
            cll_or_non_cll => [ 1, @optional ],
        },
        dependency_groups => { # if any one field is filled in, they all become required
			cll => [ qw( binet plts ) ],
		},
        constraint_methods => $constraint_methods,
	}
}

#-------------------------------------------------------------------------------
sub hmrn_plasmacell {
    my $self = shift;

    my @required = qw(
        paraprotein pp_level bence_jones serum_kappa serum_lambda igs
		bone_disease bone_lesions b2m mri creatinine skeletal_survey
        hb calcium albumin serum_flc
        wbc plts haematocrit_vol neutrophils lymphs monocytes
        ecog kappa_lambda_ratio
    );
    my @optional = (); # none

    my $constraint_methods
        = $self->get_hmrn_param_constraints([ @required, @optional ]); # JD - added @optional

	return {
		required => \@required,
        optional => \@optional, # added by JD
        constraint_methods => $constraint_methods,
	}
}

#-------------------------------------------------------------------------------
sub hmrn_precursor {
    my $self = shift;

    my @required = qw(
        ecog hb wbc plts haematocrit_vol neutrophils lymphs monocytes
    );
    my @optional = (); # none

    my $constraint_methods
        = $self->get_hmrn_param_constraints([ @required, @optional ]); # JD - added @optional

	return {
		required => \@required,
        optional => \@optional, # added by JD
        constraint_methods => $constraint_methods,
	}
}

#-------------------------------------------------------------------------------
sub outreach_questionnaire {
    my $self = shift;

=begin # refactored - all optional now
    my @required = qw(
            hospital_referral
            new_medication
            serious_infection
            weight_loss
            night_sweats persistent_night_sweats
            rib_pain persistent_rib_pain
            back_pain persistent_back_pain
            arm_pain persistent_arm_pain
            leg_pain persistent_leg_pain
            cervical_swelling axillary_swelling
            abdominal_swelling inguinal_swelling
            location_preference
            transport_arrangements
            transport_difficulty );

    my @optional = qw(
            weight waiting_duration excessive_wait imperial_stones imperial_pounds
            hospital_referral_details new_medication_details monitoring_comments
            serious_infection_details transport_difficulty_details swelling_details
            mobility self_care usual_activity discomfort anxiety health_index );

    my @combined = (@required, @optional); # temporary during switch-over
	return {
        required => [ ],
        optional => \@combined,
		require_some => { # require one from this group:
#            weight => [ 1, qw/weight imperial_stones/ ],
 		},
		dependencies => {
			transport_difficulty => { Y => [ 'transport_difficulty_details' ] },
            serious_infection 	 => { Y => [ 'serious_infection_details'    ] },
            hospital_referral 	 => { Y => [ 'hospital_referral_details'    ] },
            new_medication    	 => { Y => [ 'new_medication_details'       ] },
			waiting_duration  	 => [ 'excessive_wait' ],
        },
    }
=cut
	my @optional = qw(
		pain adenopathy weight_loss night_sweats pain_option_id nodal_option_id
		opinion	mobility self_care usual_activity discomfort anxiety health_index
	);
	return {
        required => [ ],
        optional => \@optional,
	}
}

#-------------------------------------------------------------------------------
sub outreach_demographics {
    my $self = shift;

	return {
		required => [
            qw( address post_code practice_id gp_id dispatch_to status )
        ],
		optional => [ qw(contact_number) ],
		constraint_methods => {
			post_code => $self->check_post_code_is_valid(),
		},
	}
}

#-------------------------------------------------------------------------------
sub outreach_followup {
    return {
        required => [ 'followup_option_id' ],
        optional => [ qw(pack_due return_due appointment_date) ],
    }
}

#-------------------------------------------------------------------------------
sub outreach_lab_results {
    return {
        immunology => {
            required => [ qw( department) ],
            optional => [ qw(IgA IgG IgM electrophoresis globulin paraprotein) ],
        },
        biochemistry => {
            required => [ qw( department ) ],
            optional => [
                qw(sodium potassium urea alk_phos albumin phosphate total_protein
                    creatinine calcium bilirubin magnesium alt)
            ],
        },
        flow_cytometry => {
            required => [
                qw(total_b_cells kappa lambda sIg_neg cd5_pos neoplastic_b_cells
                    total_plasma_cells cd19_neg_pc department)
            ],
            optional => [],
        },
        haematology => {
            required => [ qw( department ) ],
            optional => [ qw(hb wbc plts lymphs) ],
        },
    }
}

#-------------------------------------------------------------------------------
sub pre_screen_data {
	return {
		required => [ qw(surname specimen lab_section_id) ],
		optional => [ qw(
			plasma_cell b_lymphoid t_lymphoid rituximab outreach stem_cell
		) ],
	}
}

#-------------------------------------------------------------------------------
sub pre_screen_new {
	return {
		required => [ qw(specimen lab_section_id) ],
        require_some => { # require one from this group
            lab_tests => [ 1,
				qw(plasma_cell b_lymphoid t_lymphoid rituximab outreach stem_cell)
			],
        },
	}
}

#-------------------------------------------------------------------------------
=begin # passing data from form as lab_test_id_(\d) now:
sub results_data {
	my $self = shift;

	my $lab_tests_count = $self->lab_tests_count;

	return {
		optional => [ ( 1 .. $lab_tests_count ) ],
	}
}
=cut

#-------------------------------------------------------------------------------
sub storage_common {
    my $self = shift;

    my @xna_dependencies = qw( concentration volume method );
    my $msgs = $self->messages;

    return {
        required => [ qw( specimen sample vialId source part_number ) ],
        optional => [ qw( volume concentration method comment specimen_id ) ],
        field_filters => {
            vialId => 'uc', # plate reader returns uc letters
			sample => 'lc', # so dependencies can work with uc & lc entries
        },
        dependencies => {
			# if sample is [rd]na, require concentration, volume & method:
			sample => {
                plasma => [ 'volume' ],
				dna    => \@xna_dependencies,
				rna    => \@xna_dependencies,
			},
		},
        constraint_methods => {
            vialId => $self->check_vial_Id_length(),
            volume => $self->check_numeric(), # integer (not decimal)
            concentration => $self->check_numeric(), # integer (not decimal)
        },
        msgs => {
            constraints => {
                invalid_length => $msgs->{invalid_length},
            },
        }
    };
}

#-------------------------------------------------------------------------------
sub storage_input {
    my $self = shift;

    my $profile = $self->storage_common;

    # replace vialId constraint method with joint:
    my $constraint = $self->join_these(
        $self->check_vial_Id_length(),
		$self->check_vial_Id_unique(),
		$self->check_vial_Id_format(),
	);
    $profile->{constraint_methods}{vialId} = $constraint;

    # add msgs (already have invalid_length in storage_common):
    $profile->{msgs}{constraints}{$_} = $self->messages->{$_}
        for qw( invalid_format not_unique );
    return $profile;
}

#-------------------------------------------------------------------------------
sub storage_update { shift->storage_common } # don't want vialId uniqueness, etc

#-------------------------------------------------------------------------------
sub storage_delete {
    my $self = shift; # warn Dumper $self->messages;
    return {
        required => [ qw( vialId vial_id )],
        optional => [ qw(reason_required reason) ],
		dependencies => {
			reason_required => 'reason',
        },
        constraint_methods => {
            vialId => $self->check_vial_id('vial_confirm'),
        },
		msgs => {
			constraints => {
				vial_confirm => $self->messages->{vial_mismatch},
            },
        },
    };
}

#-------------------------------------------------------------------------------
sub storage_output {
    my $self = shift;

    return {
        required => [ 'vialId' ],
        constraint_methods => {
            vialId => $self->check_param_length(10), # max_length if single int
        },
		msgs => {
			constraints => {
				max_length => $self->messages->{too_long},
            },
        },
    }
}

#-------------------------------------------------------------------------------
# only loaded if storage_locations.yml
sub storage_rack { { required => [ 'storage_location' ] } }

#-------------------------------------------------------------------------------
sub lab_test_data_type {
	return {
		required => [ qw( data_type_id is_active ) ],
	}
}

#-------------------------------------------------------------------------------
sub status_options {
	return {
		required => [ qw(description is_active) ],
	}
}

#-------------------------------------------------------------------------------
sub pre_authorisation_check {
	return {
		required => [ 'diagnosis_id' ],
	}
}

#-------------------------------------------------------------------------------
sub lab_test_result_options {
	return {
		required => [ qw(data_type_id value is_active) ],
		optional => '_record_id',
	}
}

#-------------------------------------------------------------------------------
sub specimen_lab_test { # doesn't need validating
	return {
		optional => [ 'specimen_id' ],
	}
}

#-------------------------------------------------------------------------------
sub section_status_options { # doesn't need validating
	return {
		optional => [ 'option_data' ], # composite of status_option_id & position
	}
}

#-------------------------------------------------------------------------------
# results data validators; called by C::R::FormData::validate_results_data_entry
# need to return validation method & error message:
sub data_entry_integer {
    my $self = shift;
    return {
        method  => $self->is_integer(),
        err_msg => $self->messages->{need_integer},
    }
}

sub data_entry_decimal {
    my $self = shift;
    return {
        method  => $self->is_decimal(),
        err_msg => $self->messages->{need_decimal},
    }
}

sub data_entry_date {
    my $self = shift;
    return {
        method  => $self->check_yyyymmdd_date_is_valid(),
        err_msg => $self->messages->{invalid_date},
    }
}

sub data_entry_datetime {
    my $self = shift;
    return {
        method  => $self->check_yyyymmdd_date_is_valid(),
        err_msg => $self->messages->{invalid_datetime},
    }
}

=begin
sub _build_lab_tests_count {
	my $self = shift;

	my $count = $self->models->{get_lab_tests_count};

	return $count;
}
=cut

1;
