RSS Git Download  Clone
Raw Blame History
package LIMS::Controller::Patient;

use Moose;
BEGIN { extends 'LIMS::Base'; }
with (
    'LIMS::Controller::Roles::Misc',
	'LIMS::Controller::Roles::SessionStore',
	'LIMS::Controller::Roles::PatientDemographics', # Mini-Spine service
);
__PACKAGE__->meta->make_immutable(inline_constructor => 0);

use LIMS::Local::PAS;
use LIMS::Local::Utils;

use Data::Dumper;

#-------------------------------------------------------------------------------
sub default : Startrunmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift; $self->stash( errs => $errs ); # for debugging in tmpl

	return $self->forbidden() unless $self->user_can('register');

    $self->js_validation_profile('js_new_patient');

    # if redirected from empty registration search, retrieve saved form
    # params from session & load into query:
    $self->_load_saved_form_params;

    return $self->tt_process($errs);
}

#-------------------------------------------------------------------------------
sub add_new : Runmode {
    my $self = shift; my $rm = $self->get_current_runmode; $self->_debug_path($rm);

	return $self->forbidden() unless $self->user_can('register');

    my $dfv = $self->check_rm( 'default', $self->validate('new_patient') )
    || return $self->dfv_error_page;

    my $patient = $dfv->valid; # $self->debug( $patient );

	# create dt object from patient year, month & day vals:
    $patient->{dob} = LIMS::Local::Utils::to_datetime($patient); # $self->debug( $patient );

    # check new patient details for potential duplicates and/or PDS mismatches
    # returns true if OK, otherwise sets appropriate tt_params for default.tt:
    my $check_ok = $self->_check_new_patient($patient); # warn Dumper $check_ok;

    unless ($check_ok) {
      	my $pds_return_codes = $self->get_yaml_file('pds_return_codes');
        $self->tt_params( pds_codes => $pds_return_codes );

        my $html = $self->render_view('patient/default.tt'); # for is_code_ref()
        return $self->fill_form($html);
    }

    # insert data to patients/patient_cases and return patient_cases.id (last_insert_id):
    my $case_id = $self->model('Patient')->create_new_patient($patient)
    || return $self->error('no patient_case id value returned to ' . $rm);

    return $self->redirect( $self->query->url.'/request/add_new/'.$case_id );
}

#-------------------------------------------------------------------------------
sub add_new_location : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my $patient_id = $self->param('id')
        || return $self->error('no patient id passed to '.$self->get_current_runmode);

    my $dfv = $self->check_rm( 'select_patient', $self->validate('new_location') )
        || return $self->dfv_error_page;

    my $data = $dfv->valid; # $self->debug($data);

    my %patient_case_data = (
        patient_id          => $patient_id,
        referral_source_id  => $data->{referral_source_id},
        unit_number         => $data->{unit_number},
    );

    my $patient_case = $self->model('PatientCase')->new_patient_case(\%patient_case_data);

    return $self->redirect( $self->query->url . '/request/add_new/' . $patient_case->id );
}

#-------------------------------------------------------------------------------
sub select_patient : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift;

	return $self->forbidden() unless $self->user_can('register');

    my $patient_id = $self->param('id')
    || return $self->error('no patient id passed to '.$self->get_current_runmode);

	my $patient = $self->model('Patient')->get_patient_demographics($patient_id);

    $self->js_validation_profile('new_location');

    my $patient_cases
		= $self->model('PatientCase')->get_cases_by_patient_id($patient_id);

    if ($self->cfg('settings')->{pas_address}) { # do PAS lookup (if local patient):
        my $pas_query = $self->_pas_query($patient_id); # $self->debug($pas_query);
        $self->tt_params( pas_query => $pas_query ); # switched off, replaced by PDS
    }
	{ # do PDS lookup:
        my $pds_query = $self->_personal_demographic_service($patient);
        $self->tt_params( pds_query => $pds_query );
    }
    { # previous clinical trials:
        my %h = ( patient_id => $patient_id );
		my $trials = $self->model('ClinicalTrial')->get_patient_trials(\%h);
		$self->tt_params( clinical_trials => $trials );
	}

    my $pds_return_codes = $self->get_yaml_file('pds_return_codes');

    $self->tt_params(
        patient_cases => $patient_cases,
        pds_codes     => $pds_return_codes,
        patient       => $patient,
    );

    return $self->render_view($self->tt_template_name, $errs);
}

#-------------------------------------------------------------------------------
sub edit_patient : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $errs = shift; # $self->stash(errs => $errs);

	return $self->forbidden() unless $self->user_can('edit_pid');

    my $patient_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

	# request_id (optionally) passed as 2nd token (or in form if resubmitted after dfv failure)
	my $request_id = $self->param('Id') || $self->query->param('request_id') || '';

	if ($request_id) {
		my $request = $self->model('Request')->get_request($request_id);
		$self->tt_params( request => $request );
	}

    $self->js_validation_profile('js_edit_patient');

    my $patient = $self->model('Patient')->get_patient($patient_id);
    $self->tt_params( patient => $patient );

	if ($self->cfg('settings')->{pas_address}) { # PAS query (replaced by PDS):
		# vars only supplied by PAS search if insufficient details supplied:
		my $vars = $self->query->Vars(); # $self->debug($vars);

		my $pas_query = (%$vars)
            ? [ $vars ] # template expects array of hashrefs
            : $self->_pas_query($patient_id);
		$self->tt_params( pas_query => $pas_query ); # $self->debug($pas_query);
	}
	{ # do PDS lookup:
        my $pds_query = $self->_personal_demographic_service($patient);
		my $pds_return_codes = $self->get_yaml_file('pds_return_codes');
        $self->tt_params(
			pds_query => $pds_query,
	        pds_codes => $pds_return_codes,
		);
    }

	{ # error codes:
		my $error_codes
			= $self->model('ErrorCode')->get_error_code_assignment('patient');
		$self->tt_params(
			error_codes => $error_codes,
		);
	}
	{ # get any similar patient entries:
	 	my $cases
			= $self->model('Patient')->get_similar_patients($patient);

		my $similar_entries = $self->extract_patients_and_locations($cases);
		$self->tt_params( similar_entries => $similar_entries );
	}
	{ # has patient id been used already (for delete function):
		my $count = $self->model('Patient')->patient_request_count($patient_id);
		$self->tt_params( count => $count );
	}

    return $self->render_view($self->tt_template_name, $errs);
}

#-------------------------------------------------------------------------------
sub pds_query : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my $patient_id =
        $self->param('id') || $self->query->param('_patient_id');

    if ($patient_id) { # warn $patient_id;
        my $data = $self->model('Patient')->get_patient($patient_id);
        $self->tt_params( patient => $data ); # warn Dumper $data;
    }
    else { # no match in patients table - re-use submitted form params:
        my $vars = $self->query->Vars; # warn Dumper $vars;

        # freeze vars in session for transfer to registration form if no PAS match:
        unless ($vars->{_is_pds_search}) { # ie return from pas search - don't want these vals
            $self->session->param( patient_search_params => { %$vars } ); # needs to be in assoc. array format 1st
        }

        # get yr, month, day from dob if supplied (otherwise expect separate vars):
        if ( my $dob = $vars->{dob} ) { # convert to ymd format for Utils::to_datetime():
            @{$vars}{qw(year month day)} = split '-', $dob;
        }

        my %patient = map +($_ => $vars->{$_}),
            qw(last_name first_name nhs_number unit_number);
        $patient{dob} = LIMS::Local::Utils::to_datetime($vars); # .tt requires DT

        $self->tt_params( patient => \%patient ); # warn Dumper \%patient;

=begin # PDS doesn't use unit numbers:
        my @unit_numbers;
        if ( my $unit_number = $self->query->param('unit_number') ) {
            push @unit_numbers, $unit_number; # only if exists, or get [ '' ] in tt
        }
        $self->tt_params( unit_numbers => \@unit_numbers );
=cut
        # clear all query params to prevent fill_form() loading into form fields:
        # $self->query->delete_all(); # not required if not using fill_form()
    }

    return $self->render_view($self->tt_template_name);
}

#-------------------------------------------------------------------------------
sub do_pds_search : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    my %data = $self->query->Vars; # warn Dumper \%data; # in hash context, or can't modify dob key

    if ( grep { ! $data{$_} } qw(last_name gender dob) ) { # warn 'insufficient vars';
        $self->flash( error => $self->messages('demographics')->{pds_insufficient_vars} );
        return $self->forward('pds_query');
    }

    # dob -> datetime:
    if ( my $dob = $data{dob} ) {
        my $dt = DateTime::Format::MySQL->parse_date($dob); # warn Dumper $dt;
        $data{dob} = $dt;
    }
    # format post-code (PDS error unless WWd(d) dWW format):
    if ( my $post_code = $data{post_code} ) {
        $data{post_code} = LIMS::Local::Utils::format_postcode($post_code);
    } # warn \%data;

    my $pds_query = $self->_personal_demographic_service(\%data); # warn Dumper $pds_query;

    my $pds_return_codes = $self->get_yaml_file('pds_return_codes');

    $self->tt_params(
        pds_query => $pds_query,
        pds_codes => $pds_return_codes,
    );

    return $self->forward('pds_query');
}

#-------------------------------------------------------------------------------
# request to edit patient during registration process; sets hidden flag to alter
# redirect destination:
sub register_edit_patient : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

    $self->tt_params( registration_edit => 1 ); # flag for tmpl
	return $self->forward('edit_patient'); # use forward() so tt_template_name() works
}

#-------------------------------------------------------------------------------
sub edit_case : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	return $self->forbidden() unless $self->user_can('edit_pid');

    my $case_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

    $self->js_validation_profile('patient_case');

    my $patient_case
        = $self->model('PatientCase')->retrieve_patient_data($case_id);

    # has entry been used already (for delete function):
    my $requests_count
		= $self->model('Request')->get_patient_case_requests_count($case_id);

    # how many patient_case entries for this patient?
    my $patient_id = $patient_case->patient_id;
    my $patient_case_count = do {
        my $o = $self->model('PatientCase')
            ->get_cases_by_patient_id($patient_id);
        scalar @$o;
    };

    $self->tt_params(
        data               => $patient_case,
        requests_count     => $requests_count,
        patient_case_count => $patient_case_count,
    );

    return $self->tt_process;
}

#-------------------------------------------------------------------------------
sub update_case : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	return $self->forbidden() unless $self->user_can('edit_pid');

    my $case_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

    my $dfv = $self->check_rm( 'default', $self->validate('patient_case') )
        || return $self->dfv_error_page;

    # get form params as hashref:
    my $data = $dfv->valid; # warn Dumper $data;

    my %args = (
        data    => $data,
        case_id => $case_id,
    );

    my $rtn = $self->model('PatientCase')->update_patient_case(\%args);

    return $rtn ?
        $self->error($rtn) :
            $self->redirect( $self->query->url . '/request/add_new/' . $case_id );
}

#-------------------------------------------------------------------------------
sub delete_case : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	return $self->forbidden() unless $self->user_can('delete_record');

    my $q = $self->query;

    my $case_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

    my $case = $self->model('PatientCase')->get_patient_case($case_id)
        || return $self->error(sprintf q!case id '%s' not found in %s!,
            $case_id, $self->get_current_runmode);

    # check not already been used (initial check done in edit_case):
    if ( $self->model('Request')->get_patient_case_requests_count($case_id) ) {
        $self->flash( error => $self->messages('registration')->{cannot_delete_case} );
        return $self->redirect( $q->url . '/patient/edit_case/' . $case_id);
    }
    # need confirmation to delete record:
    if ( $q->param('confirm_delete') ) {
        my $redirect_url
            = $q->url.'/patient/select_patient/'.$case->patient_id;

        # successful delete (or no such record) returns true:
        my $successful_delete = $self->model('PatientCase')->delete_patient_case($case_id);

        return $successful_delete ?
            $self->redirect( $redirect_url ) :
                $self->error('Sorry - delete failed, I\'ve no idea why.');
    }
    # just return template with form:
    else {
        $self->tt_params( case => $case );
        return $self->tt_process;
    }
}

#-------------------------------------------------------------------------------
sub delete_patient : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	return $self->forbidden() unless $self->user_can('delete_record');

    my $q = $self->query;

    my $patient_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

    my $patient = $self->model('Patient')->get_patient($patient_id)
        || return $self->error(sprintf q!patient id '%s' not found in %s!,
            $patient_id, $self->get_current_runmode);

    # check patient not attached to other record(s) (initial check done in edit_patient):
    if ( $self->model('Patient')->patient_request_count($patient_id) ) {
        $self->flash( error => $self->messages('registration')->{cannot_delete_patient} );
        return $self->redirect( $q->url . '/patient/edit_patient/' . $patient_id);
    }
    # need confirmation to delete record:
    if ( $q->param('confirm_delete') ) {
        # successful delete (or no such record) returns true:
        my $successful_delete = $self->model('Patient')->delete_patient($patient_id);

        return $successful_delete ?
            $self->redirect( $q->url.'/register' ) :
                $self->error('Sorry - delete failed, I\'ve no idea why.');
    }
    # just return template with form:
    else {
        $self->tt_params( patient => $patient );
        return $self->tt_process;
    }
}

#-------------------------------------------------------------------------------
sub update_patient : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	return $self->forbidden() unless $self->user_can('edit_pid');

	my $q = $self->query;

    my $patient_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

	# hidden field if edited from a request page:
	my $request_id = $q->param('request_id');

	# put $patient_id into params() for $self->validate('edit_patient') profile:
	$self->query->param(_record_id => $patient_id);

    my $dfv = $self->check_rm( 'edit_patient', $self->validate('edit_patient') );

	# need to (re)set template flag(s) first if validation failed:
	if (! $dfv) {
		map { # warn $q->param($_);
			$self->tt_params( $_ => $q->param($_) );
		} grep $q->param($_), qw(registration_edit request_id);

		return $self->dfv_error_page;
	}

    # get form params as hashref:
    my $data = $dfv->valid; # $self->debug( $data );

    # create DoB datetime object if day, month & year passed:
    $data->{dob} = LIMS::Local::Utils::to_datetime($data);

	# add patient.id to data so record updated if it's an edit:
	$data->{id} = $patient_id;

    # now we have the ability to change only 1 record, need to check NHS no if
	# submitted doesn't already exist (can't change single record with an NHS no):
	unless ( $self->_check_nhs_number_usage($data) ) { # returns 1 if OK
		my $str = '/patient/edit_patient/' . join '/', $patient_id, $request_id;
		return $self->redirect( $self->query->url . $str ); # flash msg set in sub
	}

    { # do PDS lookup - returns 0 if PDS verification failed; 1 if it passes or not required:
		my $result = $self->_demographics_verification($data);
		return $self->forward('edit_patient') if not $result; # only necessary on failure
	}

	# validations, etc ok - update record:
    my $rtn = $self->model('Patient')->update_patient($data); # warn Dumper $rtn;

	# $rtn = { error => $db->error (if any); success => number of rows updated }
	if ($rtn->{error}) {
		return $self->error($rtn->{error});
	}
	else { # update succeeded:
		my $messages = $self->messages('request_edit');
		my $i = $rtn->{success}; # number of rows updated

		my @flash = $i # success can be 0, or an INT
			? ( info => sprintf $messages->{edit_success}, $i )
			: ( warning => $messages->{edit_failed} );

		$self->flash( @flash );

		my $redirect = $q->param('registration_edit') ?
			# redirect to registration page for $patient_id:
			"/register/patient_search?patient_id=$patient_id" :
				# redirect to specific request, or to general search page:
				$request_id ? "/search/=/$request_id" : '/search';

		return $self->redirect( $q->url . $redirect );
	}
}

# ------------------------------------------------------------------------------
sub patient_notes : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

    return $self->forbidden() unless $self->user_can(['report','modify_results']);

    my $request_id = $self->param('id')
    || return $self->error('no request_id passed to '.$self->get_current_runmode);

    my $patient_id = $self->param('Id')
    || return $self->error('no patient_id passed to '.$self->get_current_runmode);

    # only one param passed:
    my $str = $self->query->param('patient_notes');

    my %args = (
        patient_id => $patient_id,
        detail     => $str,
    );

    my $rtn = $self->model('Patient')->update_patient_notes(\%args);
    return $self->error($rtn) if $rtn;

    $self->flash( info => $self->messages('action')->{edit_success} );
    return $self->redirect( $self->query->param('fwd_to') );
}

#-------------------------------------------------------------------------------
# check nhs number if submitted AND editing 1 patient only, returns 0 if NHS no
# already exists, otherwise returns 1:
sub _check_nhs_number_usage {
	my ($self, $data) = @_;

    # only applies to single record change with an NHS number:
    return 1 unless ( $data->{this_record_only} && $data->{nhs_number} );

    my $nhs_number = $data->{nhs_number};
	if ( $self->model('Patient')->get_patient_from_nhs_number($nhs_number) ) {
		$self->flash(error => $self->messages('request_edit')->{nhs_conflict});
		return 0; # blocks update
	}
	return 1;
}

#-------------------------------------------------------------------------------
# checks against PDS  (unless already done it, or PDS isn't config'd eg *.t, or
# anon patient)- only returns 0 if verification _fails_ (not if it isn't done):
sub _demographics_verification {
	my ($self, $data) = @_;

	my $can_skip_pds = (
		$data->{use_patient_id} || # selected existing patient entry
		! $self->cfg('settings')->{pds_proxy} ) || # PDS switched off
		grep $self->query->param($_), qw(_pid_confirmed _skip_pds); # already done it

    unless ( $can_skip_pds ) {
        my $pds_query = $self->_personal_demographic_service($data);

		# need to check pds return is hashref (not error str) before deref'ing:
		my $is_verified = ( $pds_query && ref $pds_query eq 'HASH'
			&& $pds_query->{demographics_verified} );

		# return 0 if verification attempted but fails:
        unless ( $is_verified ) {
            # flag to tt to allow override:
            $self->tt_params( is_patient_edit => 1 ); # to allow '_pid_confirmed'
			return 0; # will trigger forward to patient_edit
		};
	}
	return 1; # caller only cares if verification fails in previous block
}

#-------------------------------------------------------------------------------
# if redirected from empty registration search, retrieve saved form params from
# session & load into query:
sub _load_saved_form_params {
    my $self = shift; $self->_debug_path;

    my $patient_search_params = # will be empty if not arrived via empty registration search
        $self->session->param('patient_search_params') || return;

    foreach (keys %$patient_search_params) { # $self->debug($patient_search_params->{$_});
        $self->query->param( $_ => $patient_search_params->{$_} );
    }

    # now clear patient_search_params hahref
    $self->session->clear('patient_search_params');
}

# checks new patient data for potential duplicate or PAS mismatch,
# returns false if potential problem, otherwise true:
#-------------------------------------------------------------------------------
sub _check_new_patient {
    my $self    = shift; $self->_debug_path;
    my $patient = shift; # hashref (with dob = DT)

    # form flags to confirm & override checks:
    my $local_confirmed = $self->query->param('_local_confirmed');
    my $pid_confirmed   = $self->query->param('_pid_confirmed');

    # do PDS query first - if OK then do a local db check for duplicates - can
	# only do local check *after* PDS, so can skip PDS if local_confirmed:
    if ( not $pid_confirmed and not $local_confirmed ) {
        if ( my $pds_query = $self->_personal_demographic_service($patient) ) { # warn Dumper $pds_query;
			$self->tt_params(
				is_new_patient => 1, # flag for tt to enforce confirmation in case of pds diffs
				pds_query => $pds_query,
				patient   => $patient,
			);
			return 0;
        }
    }

    # check for potential duplicate unit_number and/or nhs_number:
    if ( not $local_confirmed ) {
        my $maybe_duplicate =
            $self->model('PatientCase')->validate_patient_case($patient);

        if (@$maybe_duplicate) { # warn 'maybe_duplicate';
            $self->tt_params( maybe_duplicate => $maybe_duplicate );
            return 0;
        }
    }

    # OK, patient validated against PDS and HILIS4 patient table:
    return 1;
}

#-------------------------------------------------------------------------------
# returns true if 1 or more locations = local in patient_cases object:
sub _is_local_patient {
    my $self = shift; $self->_debug_path;

    my $patient_data = shift; # arrayref if PatientCase obj, or hashref of patient data

    # get local prefix from settings or return if empty:
    my $local_prefix = $self->cfg('settings')->{local_prefix}
        || return 0; # warn $local_prefix;

    if (ref $patient_data eq 'HASH') {
        my $ref_src_id = $patient_data->{referral_source_id};
        my $location =
            $self->model('ReferralSource')->get_referral_source($ref_src_id);
        # returns true if organisation_code matches local prefix:
        return $location->organisation_code =~ /\A($local_prefix)/;
    }
    else { # returns true if at least 1 organisation_code matches local prefix:
        return grep {
                $_->referral_source->organisation_code =~ /\A($local_prefix)/
            } @$patient_data;
    }
}

=begin nd
Function: _pas_query()
Called by <select_patient()> to check patient demographic data submitted either via
selection of previous patient match or directly through Validate button.
Uses <LIMS::Local::PAS> to query the PAS interface. Returns a ref to array of hashrefs
(if matching patient data found on PAS) or string containing error status
($c->config->{msg}) if not.
=cut

#-------------------------------------------------------------------------------
sub _pas_query {
	my ($self, $patient_id) = @_; $self->_debug_path;

    my @pas_config_settings = qw(pas_address pas_username pas_pwd);
    my $cfg = $self->cfg('settings');

    # need all required PAS config settings, or return:
    return 0 if grep ! $cfg->{$_}, @pas_config_settings;

	my $patient_cases # arrayref
		= $self->model('PatientCase')->get_cases_by_patient_id($patient_id);

    # needs to be local patient for PAS lookup:
    return 0 unless $self->_is_local_patient($patient_cases);

	# variable: $pas_excluded_names
    # list of last_names excluded from PAS search (eg HIV's)
#	my $pas_excluded_names  = join '|', @{ $self->cfg('pas_excluded_names') };

#	return if # skip PAS lookup if:
        # last_name matches proscribed entry(s) in config->pas_excluded_names,
#		$patient->{last_name} =~ /\A($pas_excluded_names)/ || # eg HIV's
        # form carries 'pas_checked' flag:
#		$params_ref->{pas_checked}; # already been there

    my %patient;

    if (ref $patient_cases eq 'ARRAY') {
        # extract 1st record (all same patient) from $patient_data:
        my $patient_case = $patient_cases->[0];

        # create patient hashref for PAS query:
        %patient = map {
            $_ => $patient_case->patient->$_;
        } $patient_case->patient->meta->column_names;

        # get local unit_number(s) as arrayref from referral(s):
        my $local_unit_numbers = $self->_get_local_unit_numbers($patient_cases);

        # include unit_numbers arrayref:
        $patient{unit_number} = $local_unit_numbers; # $self->debug($local_unit_numbers);

        # dob needs to be in yyyy-mm-dd format:
        if ( my $dob = $patient_case->patient->dob) {
            $patient{dob} = $dob->ymd; # $self->debug(\%patient);
        }

        # PAS considers FORENAME as first_name + middle_name:
        $patient{first_name} = join ' ', map $patient_case->patient->$_,
            grep $patient_case->patient->$_, qw(first_name middle_name);
    }
    elsif (ref $patient_cases eq 'HASH') { # does this ever happen ??
        %patient = %$patient_cases; # $self->debug(%patient);
        # TODO: dob needs to be ymd:
        $patient{dob} = $patient{dob}->ymd if $patient{dob};
    }
    else {
        return $self->error('_pas_query() called with unexpected $patient_data format');
    }

    my %pas_config = map +($_ => $cfg->{$_}), @pas_config_settings; # $self->debug(\%pas_config);

    my %args = (
        patient  => \%patient,
        config   => \%pas_config,
        messages => $self->messages('demographics'),
    ); # warn Dumper \%args;

    my $pas = LIMS::Local::PAS->new(\%args);

	my $q = $pas->query; # warn Dumper $q; # ref to array of hashrefs or string
    return $q;
}

#-------------------------------------------------------------------------------
sub _personal_demographic_service {
	my ($self, $patient) = @_; $self->_debug_path; # hashref (with DoB = DT)

	return 0 if $self->query->param('_skip_pds'); # _skip_pds is tt flag

	my $config = $self->cfg('settings');

    # pds_proxy only used by dev server, but also used as on/off switch;
    # env flag PDS_LOOKUPS allows pds queries even if not in production mode:
    return 0 unless $config->{pds_proxy} &&
        ( $config->{is_in_production_mode} || $ENV{PDS_LOOKUPS} );

    my $data = ref $patient eq 'LIMS::DB::Patient' # get_pds_data() expects hashref with DT object:
        ? $patient->as_tree(deflate => 0) # $patient = select_patient() object
        : $patient; # $patient = add_new() hashref

    # skip anon patients, certain trials, etc:
    if ( my $cfg = $self->get_yaml_file('pds_exempt_names') ) { # warn Dumper $cfg;
        my $first_names = $cfg->{first_name}; # warn Dumper $first_names;
        my $last_names  = $cfg->{last_name};  # warn Dumper $last_names;

		# set flag for tt to skip pds on subsequent submissions & return 0:
		if (
			( grep $data->{first_name} =~ qr(\A$_\Z)i, @$first_names ) ||
			( grep $data->{last_name}  =~ qr(\A$_\Z)i, @$last_names  )
		) {
			$self->tt_params( is_pds_exempt => 1 );
			return 0;
		}
    }

    my $result = $self->get_pds_data($data); # warn Dumper $result;
    return $result;
}

#-------------------------------------------------------------------------------
# parses $patient_cases array(ref) for local unit_numbers, as patient referral
# from multiple locations may include remote location unit_number(s) which match
# a (different) local patient but are not valid for local PAS lookup:
sub _get_local_unit_numbers {
    my $self = shift; $self->_debug_path;

    my $patient_cases = shift;

    my $local_prefix = $self->cfg('settings')->{local_prefix};

    # create hash of local unit_numbers (excluding default val):
    my %local_unit_numbers =
        map { $_->unit_number, 1 }
        grep { # skip null (or default) unit_numbers:
            $_->unit_number ne $_->meta->column('unit_number')->default,
        }
        grep {
            $_->referral_source->organisation_code =~ /\A($local_prefix)/
        } @$patient_cases; # $self->debug(\%local_unit_numbers);

    # return arrayref to list of unique unit_numbers:
    return [ sort keys %local_unit_numbers ];
}

1;



__END__
=begin # done in LIMS::Valiadate now
_validate_patient_data {
    my $self = shift;

    my $nhsno = $self->query->param('nhs_number');

    # return profile as mixture of new_patient &
    # patient_search_data validation profiles:
    my $new_patient    = $self->validate('new_patient');
    my $patient_search = $self->validate('patient_search_data');

    # add nhs_number constraint_method to patient_search_data:
    $patient_search->{constraint_methods}->{nhs_number} = sub {
        return LIMS::Local::Utils::check_nhsno( $nhsno );
    };

    my %profile = (
        required           => $new_patient->{required},
        optional           => $new_patient->{optional},
        dependency_groups  => $patient_search->{dependency_groups},
        constraint_methods => $patient_search->{constraint_methods},
    );

    $profile{require_some} = {
        patient_id => [ 1, qw(nhs_number unit_number) ],
    };                      #  $self->debug(\%profile);

    return \%profile;
}
=cut

#-------------------------------------------------------------------------------
=begin # direct entry from patient/select_patient.tt now
new_location : Runmode {
    my $self = shift;
    my $errs = shift;

    my $patient_id = $self->param('id')
        || return $self->error('no id passed to '.$self->get_current_runmode);

    $self->js_validation_profile('new_location');

    my $patient = $self->model('Patient')->get_patient($patient_id);

    $self->tt_params(
        patient => $patient,
    );

    return $self->tt_process($errs);
}
=cut