diff --git a/Koha/Patron.pm b/Koha/Patron.pm index 1f018bee2a..d6763fefe8 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -3117,6 +3117,8 @@ sub to_api { ? Mojo::JSON->true : Mojo::JSON->false; + $json_patron->{self_renewal_available} = $self->is_eligible_for_self_renewal(); + return $json_patron; } @@ -3853,6 +3855,114 @@ sub set_permissions { return $self; } +=head3 is_eligible_for_self_renewal + +my $eligible_for_self_renewal = $patron->is_eligible_for_self_renewal(); + +Returns a boolean value for whether self-renewal is available or not + +=cut + +sub is_eligible_for_self_renewal { + my ($self) = @_; + + my $category = $self->category; + return 0 if !$category->self_renewal_enabled; + + return 0 if $self->debarred; + + my $expiry_window = + defined $category->self_renewal_availability_start + ? $category->self_renewal_availability_start + : C4::Context->preference('NotifyBorrowerDeparture'); + my $post_expiry_window = $category->self_renewal_if_expired || 0; + + my $expiry_date = dt_from_string( $self->dateexpiry, undef, 'floating' ); + my $window_start = dt_from_string( $self->dateexpiry, undef, 'floating' )->subtract( days => $expiry_window ); + my $window_end = dt_from_string( $self->dateexpiry, undef, 'floating' )->add( days => $post_expiry_window ); + my $today = dt_from_string( undef, undef, 'floating' ); + + my $within_expiry_window = + $window_start < $today->truncate( to => 'day' ) && $today < $window_end->truncate( to => 'day' ); + return 0 if !$within_expiry_window; + + my $charges_status = $self->is_patron_inside_charge_limits(); + my $self_renewal_charge_limit = $category->self_renewal_fines_block; + foreach my $key ( keys %$charges_status ) { + my $within_renewal_limit = + ( $self_renewal_charge_limit && $self_renewal_charge_limit > $charges_status->{$key}->{charge} ) ? 1 : 0; + return 0 if $charges_status->{$key}->{overlimit} && !$within_renewal_limit; + } + + return 1; +} + +=head3 request_modification + +$patron->request_modification + +Used in the OPAC and in the patron self-renewal workflow to request a modification to a patron's account +Automatically approves the request based on the AutoApprovePatronProfileSettings syspref + +=cut + +sub request_modification { + my ( $self, $modification ) = @_; + + Koha::Patron::Modifications->search( { borrowernumber => $self->borrowernumber } )->delete; + + $modification->{verification_token} = q{} if !$modification->{verification_token}; + $modification->{borrowernumber} = $self->borrowernumber if !$modification->{borrowernumber}; + + my $patron_modification = Koha::Patron::Modification->new($modification)->store()->discard_changes; + + #Automatically approve patron profile changes if AutoApprovePatronProfileSettings is enabled + $patron_modification->approve() if C4::Context->preference('AutoApprovePatronProfileSettings'); +} + +=head3 create_expiry_notice_parameters + +my $letter_params = $expiring_patron->create_expiry_notice_parameters( + { letter_code => $which_notice, forceprint => $forceprint, is_notice_mandatory => $is_notice_mandatory } ); + +Returns the parameters to send an expiry notice to a patron +Used by both the membership_expiry.pl cron and the self-renewal workflow + +=cut + +sub create_expiry_notice_parameters { + my ( $self, $args ) = @_; + + my $letter_code = $args->{letter_code}; + my $forceprint = $args->{forceprint} || 0; + my $is_notice_mandatory = $args->{is_notice_mandatory}; + + my $letter_params = { + module => 'members', + letter_code => $letter_code, + branchcode => $self->branchcode, + lang => $self->lang, + borrowernumber => $self->borrowernumber, + tables => { + borrowers => $self->borrowernumber, + branches => $self->branchcode, + }, + }; + + my $sending_params = { + letter_params => $letter_params, + message_name => 'Patron_Expiry', + forceprint => $forceprint + }; + + if ($is_notice_mandatory) { + $sending_params->{expiry_notice_mandatory} = 1; + $sending_params->{primary_contact_method} = $forceprint ? 'print' : $self->primary_contact_method; + } + + return $sending_params; +} + =head2 Internal methods =head3 _type diff --git a/Koha/REST/V1/Patrons/SelfRenewal.pm b/Koha/REST/V1/Patrons/SelfRenewal.pm new file mode 100644 index 0000000000..00f3a66cf4 --- /dev/null +++ b/Koha/REST/V1/Patrons/SelfRenewal.pm @@ -0,0 +1,137 @@ +package Koha::REST::V1::Patrons::SelfRenewal; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::Patrons; +use Koha::Patron::Attribute::Types; + +use Try::Tiny qw( catch try ); +use JSON qw( to_json ); + +=head1 NAME + +Koha::REST::V1::Patrons::SelfRenewal + +=head1 API + +=head2 Methods + +=head3 start + +Controller function that retrieves the metadata required to begin a patron self renewal + +=cut + +sub start { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->param('patron_id') ); + + return $c->render( status => 403, openapi => { error => "You are not eligible for self-renewal" } ) + if !$patron->is_eligible_for_self_renewal() || !$patron; + + return try { + my $category = $patron->category; + my $self_renewal_settings = { + self_renewal_failure_message => $category->self_renewal_failure_message, + self_renewal_information_message => $category->self_renewal_information_message, + opac_patron_details => C4::Context->preference('OPACPatronDetails') + }; + + return $c->render( + status => 200, + openapi => { self_renewal_settings => $self_renewal_settings } + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 submit + +Controller function that receives the renewal request and process the renewal + +=cut + +sub submit { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->param('patron_id') ); + + return try { + Koha::Database->new->schema->txn_do( + sub { + my $body = $c->req->json; + + return $c->render( status => 403, openapi => { error => "You are not eligible for self-renewal" } ) + if !$patron->is_eligible_for_self_renewal() || !$patron; + + my $OPACPatronDetails = C4::Context->preference("OPACPatronDetails"); + if ($OPACPatronDetails) { + my $patron_details = $body->{patron}; + my $extended_attributes = delete $patron_details->{extended_attributes}; + my $changed_fields = {}; + my $changes_detected; + foreach my $key ( keys %$patron_details ) { + my $submitted_value = $patron_details->{$key}; + my $original_value = $patron->$key; + + if ( $submitted_value ne $original_value ) { + $changed_fields->{$key} = $submitted_value; + $changes_detected++; + } + } + if ($changes_detected) { + $changed_fields->{changed_fields} = join ',', keys %$changed_fields; + $changed_fields->{extended_attributes} = to_json($extended_attributes) if $extended_attributes; + $patron->request_modification($changed_fields); + } + } + + my $new_expiry_date = $patron->renew_account; + my $response = { expiry_date => $new_expiry_date }; + + if ($new_expiry_date) { + my $is_notice_mandatory = $patron->category->enforce_expiry_notice; + my $letter_params = $patron->create_expiry_notice_parameters( + { + letter_code => "MEMBERSHIP_RENEWED", is_notice_mandatory => $is_notice_mandatory, + forceprint => 1 + } + ); + + my $result = $patron->queue_notice($letter_params); + $response->{confirmation_sent} = 1 if $result->{sent}; + } + + return $c->render( + status => 201, + openapi => $response + ); + } + ); + + } catch { + $c->unhandled_exception($_); + }; + +} + +1; diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/patron-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/patron-api-client.js index 4ddaed0cfc..b12135b50b 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/fetch/patron-api-client.js +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/patron-api-client.js @@ -25,6 +25,23 @@ export class PatronAPIClient { }), }; } + + get self_renewal() { + return { + start: (id, query, params, headers) => + this.httpClient.getAll({ + endpoint: `public/patrons/${id}/self_renewal`, + query, + params, + headers, + }), + submit: (id, renewal) => + this.httpClient.post({ + endpoint: `public/patrons/${id}/self_renewal`, + body: renewal, + }), + }; + } } export default PatronAPIClient; diff --git a/misc/cronjobs/membership_expiry.pl b/misc/cronjobs/membership_expiry.pl index 5cecbe8fc3..1ea7cb0313 100755 --- a/misc/cronjobs/membership_expiry.pl +++ b/misc/cronjobs/membership_expiry.pl @@ -285,32 +285,11 @@ while ( my $expiring_patron = $upcoming_mem_expires->next ) { $which_notice = $letter_expiry; } - my $from_address = $expiring_patron->library->from_email_address; - my $letter_params = { - module => 'members', - letter_code => $which_notice, - branchcode => $expiring_patron->branchcode, - lang => $expiring_patron->lang, - borrowernumber => $expiring_patron->borrowernumber, - tables => { - borrowers => $expiring_patron->borrowernumber, - branches => $expiring_patron->branchcode, - }, - }; - - my $sending_params = { - letter_params => $letter_params, - message_name => 'Patron_Expiry', - forceprint => $forceprint - }; - my $is_notice_mandatory = grep( $expiring_patron->categorycode, @mandatory_expiry_notice_categories ); - if ($is_notice_mandatory) { - $sending_params->{expiry_notice_mandatory} = 1; - $sending_params->{primary_contact_method} = $forceprint ? 'print' : $expiring_patron->primary_contact_method; - } + my $letter_params = $expiring_patron->create_expiry_notice_parameters( + { letter_code => $which_notice, forceprint => $forceprint, is_notice_mandatory => $is_notice_mandatory } ); - my $result = $expiring_patron->queue_notice($sending_params); + my $result = $expiring_patron->queue_notice($letter_params); $count_enqueued++ if $result->{sent}; }