use v5.34; package Model; use Git; use Moo; use Model::DB; # app db methods use Try::Tiny; use Path::Tiny; use Local::Utils; use Data::Printer; use FindBin qw($RealBin); # warn $RealBin; use Types::Standard qw(assert_Int assert_Str); # validation of user input use Module::Runtime qw(use_module); use Email::Sender::Simple qw(sendmail); # required items for constructor: has app_config => ( is => 'ro', required => 1 ); # appdir, environment, etc has db => ( is => 'lazy', builder => sub { Model::DB->new( app_config => shift->app_config ); }); # BEGIN { $ENV{EMAIL_SENDER_TRANSPORT} ||= 'Test' } # to force test email delivery # warn path($RealBin, '..', '.git')->realpath; my $git = 1; # Git->repository(Directory => path($RealBin, '..', '.git')->realpath); our $VERSION = version(); # warn $VERSION; sub version { return 1; } # $git->command_oneline('rev-list', 'HEAD', '--count') } #------------------------------------------------------------------------------- sub authenticate_user { my ($self, $params) = @_; # p $params; my $userId = assert_Str($params->{userid}); # p $userId; dies unless string $userId =~ s/\s+//g; # remove any spaces (nhs no) # userid is email address, nhs_no or combination of 'OUT' prefix + patient.id: my $key; # for user details db lookup # try valid email address: if ( Local::Utils::is_valid_email($userId) ) { $key = 'email'; } # OUTnn: elsif ( $userId =~ m!^OUT\d{2,6}$! ) { $key = 'patient_id'; $userId =~ s/OUT//; # remove prefix } # NHS number: elsif ( $userId =~ m!^\d{10}$! ) { $key = 'nhs_number'; } return 0 unless $key; # p $userId; # get user from users table or return: my $user = $self->db->get_user_details({ $key => $userId }) || return 0; # p $user; # don't need password authentication for dev env: unless ( $self->app_config->{environment} eq 'development' ) { my $password = assert_Str($params->{password}); # check SHA1 b64digest 'password' param matches patient_access.password: my $digest = _sha1_digest($password); # p $digest; return 0 unless $user->{password} eq $digest; # update last_login date: $self->db->update_last_login($user->{patient_id}); } return $user; } #------------------------------------------------------------------------------- sub get_outreach_data { my ($self, $patient_id) = @_; # p $patient_id; assert_Int($patient_id); # check it's an integer, or die my @data = $self->db->get_pack_dispatches($patient_id); my %h = ( packs => \@data ); # pack dispatches: # report available for 1st one with request_status = complete: for my $req (@data) { # in reverse chronological order so most recent 1st next unless $req->{request_status} eq 'complete'; # encrypt using todays date for tamper protection: my $key = Local::Utils::today->ymd; $h{print_report_token} = Local::Utils::encrypt($req->{id}, $key); $h{date_registered} = $req->{registered}; # use report date if available (only if using HILIS4 & outreach db's): $h{date_reported} = $req->{reported} if $req->{reported}; last; } return \%h; } #------------------------------------------------------------------------------- sub email_patient_demographics { my ($self, $params, $session) = @_; # p $params; p $session; my $app_config = $self->app_config; # p $app_config; my $transport = $self->_get_email_sender_transport($params); # p $transport; # Email::Sender::Transport object my $content = do { # first highlight any changed params: _highlight_changes($params, $session); # p $params; join "\n", map { join ': ', $_, assert_Str($params->{$_}) } qw(first_name last_name dob address email contact_number practice); }; $content .= _email_separator() if lc( ref $transport ) =~ 'mbox'; # p $content; my @header = ( # paired list: Subject => 'Outreach patient demographics update', From => $session->{email}, To => $app_config->{plugins}->{Email}->{headers}->{to}, ); # p \@header; my $email = Email::Simple->create( header => \@header, body => $content ); # p $email->as_string; my %h = ( msg => 'Thanks for your update, we will do something, maybe' ); try { # sleep 5; # to test js output on submit sendmail($email, { transport => $transport } ); } catch { # set errors key, will cause route to return non-success status: $h{errors} = { sendmail => 'email failed: '.$_->message }; }; # p %h; return \%h; } #------------------------------------------------------------------------------- sub validate_password { my ($self, $params, $session) = @_; # p $params; p $session; my $passwd = assert_Str($params->{password}); # p $pwd; my $digest = _sha1_digest($passwd); # p $digest; my %h = (); # return \%h; unless ( $session->{password} eq $digest ) { # p $session->{password}; $h{errors} = { password => "sorry, your password doesn't match!" }; } return \%h; } #------------------------------------------------------------------------------- sub gitlog { my @args = ( '--pretty=format:"%ad :: %s"', '--date=relative' ); my @log = $git->command( 'log', @args ); return \@log; }; # private subs ================================================================= sub _get_email_sender_transport { my ($self, $params) = @_; my $app_config = $self->app_config; # p $app_config; # set Email::Sender::Transport object (Mbox, Test, SMTP, TestFail, etc) # command-line ENV has priority, then web-form 'email' param =~ /fail/, # then app config 'email_sender_transport' setting, finally default smtp # TestFail = local "Email::Sender::Transport::TestFail" package (below) my $t = $ENV{MY_EMAIL_SENDER_TRANSPORT}; $t ||= ( $params->{email} =~ /fail/ ) ? 'TestFail' : $app_config->{email_sender_transport} || 'SMTP'; # p $t; my %args = ( # optional transport args: SMTP => $app_config->{smtp}, Mbox => { filename => path($app_config->{appdir}, 'mbox')->realpath }, ); my $transport_args = $args{$t} || {}; # p $transport_args; my $transport_module = 'Email::Sender::Transport::'.$t; use_module($transport_module)->new($transport_args); # p $transport_module; } sub _highlight_changes { # modifies $params hashref - no return expected my ($params, $session) = @_; my $highlight = sub { '!!! ' . $_[0] . ' !!!' }; no warnings 'uninitialized'; # eg names in demo (depends which db in use) $params->{$_} = &$highlight($params->{$_}) for grep lc $params->{$_} ne lc $session->{$_}, qw(first_name last_name email contact_number); $params->{dob} = &$highlight($params->{dob}) if $params->{dob} ne $session->{dob}->strftime('%d %b %Y'); # address & practice are composited on form: $params->{address} = &$highlight($params->{address}) if $params->{address} ne join ', ', @{$session}{qw/address post_code/}; $params->{practice} = &$highlight($params->{practice}) if $params->{practice} ne join ', ', @{$session}{qw/gp practice/}; # p $params; return undef; } sub _sha1_digest { Local::Utils::sha1_digest(shift) } sub _email_separator { "\n" . '=' x 60 . "\n" } =begin # queries handled by Model::SQL now #------------------------------------------------------------------------------- sub _query { my $self = shift; my ($sql, @bind) = @_; # p $sql; p @bind; my $data = $self->dbix->query($sql, @bind)->hashes; # p $data; _autoinflate_dates($data); # return array, ref to array of hashrefs or single hashref: return wantarray ? @$data : @$data > 1 ? $data # AoH : $data->[0]; # href } #------------------------------------------------------------------------------- # if it looks like a MySQL date or date + time, inflate to DateTime object: sub _autoinflate_dates { my $data = shift; my $re = qr!\d{4}-\d{2}-\d{2}(\s\d{2}:\d{2}:\d{2})?!; my $to_datetime = sub { my $val = shift // return; # p $val; # must use // or lose $val of zero return $val if ref $val eq 'DateTime'; # if already a DateTime object if ( my ($date) = $val =~ m!\A($re)\Z! ) { # p $date; $val = length $date > 10 # ie date + time ? DateTime::Format::MySQL->parse_datetime($date) : DateTime::Format::MySQL->parse_date($date); } return $val; }; for my $ref(@$data) { $ref->{$_} = &$to_datetime($ref->{$_}) for keys %$ref; } } =cut { package Email::Sender::Transport::TestFail; use Data::Printer; use Moo; extends 'Email::Sender::Transport::Test'; sub delivery_failure { my ($self, $email) = @_; # p $email; return Email::Sender::Failure->new('test delivery failure'); } no Moo; 1; } 1;