From 776ad5f3c0f7dfdbfecd6d0917a9d0ada416b0be Mon Sep 17 00:00:00 2001 From: Tomas Cohen Arazi Date: Mon, 9 Jun 2025 13:46:12 -0300 Subject: [PATCH] Bug 40101: Add `Koha::Patron->can_place_holds()` This patch adds a new method to the `Koha::Patron` class. As described in the POD, this method makes checks on the patron's current situation and returns a boolean telling if it should be allowed to place holds. This doesn't check specific item/biblio holdability. It only covers patron specific conditions that could prevent holds to be placed. Both in `opac-reserve.pl` and `reserve/request.pl` this checks are all run, and all blocking situations are presented to the UI user. Feedback in community (so far) expressed this is a desirable situation, so that behavior can be retained with the use of the `no_short_circuit` parameter. If `no_short_circuit` is not passed, then the routine will return on the first blocking condition it finds. An `overrides` parameter is added, allowing to be passed a list of strings that match the possible error messages. This way, this method can be asked not to check for patron expiration, for example, and things like that. This is of course designed with the API overrides use case in mind. To test: 1. Apply this patches 2. Run: $ ktd --shell k$ prove t/db_dependent/Koha/Patron.t => SUCCESS: Tests pass! 3. Sign off :-D Signed-off-by: Tomas Cohen Arazi Signed-off-by: Kristi Krueger --- Koha/Patron.pm | 152 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/Koha/Patron.pm b/Koha/Patron.pm index dcfff7aeb08..14d5838e6be 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -1318,6 +1318,158 @@ sub move_to_deleted { return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos); } +=head3 can_place_holds + + my $result = $patron->can_place_holds(); + my $result = $patron->can_place_holds( + { + overrides => { debt_limit => 1, card_lost => 1 }, + no_short_circuit => 1 + } + ); + + if ( $patron->can_place_holds() ) { + # patron can place holds + } else { + my @messages = $result->messages; + # handle error messages + } + +Checks if a patron is allowed to place holds based on various patron conditions. + +=head4 Parameters + +=over 4 + +=item * C<$options> (optional) - Hashref with the following keys: + +=over 8 + +=item * C - Hashref of checks to skip. Keys should match error message codes. + +=item * C - Boolean. If true, performs all checks and collects all error messages instead of stopping at first failure. Default: false. + +=back + +=back + +=head4 Returns + +Koha::Result::Boolean object - true if patron can place holds, false otherwise. +When false, the result object contains error messages with details about why +holds are blocked. + +=head4 Error Messages + +The following error message codes may be returned: + +=over 4 + +=item * C - Patron account has expired and expired patrons are blocked from placing holds + +=item * C - Patron owes more than the maximum allowed outstanding amount + +=item * C - Patron's address is marked as incorrect + +=item * C - Patron's library card is marked as lost + +=item * C - Patron account is restricted/debarred + +=item * C - Patron has reached the maximum number of allowed holds + +=back + +Error messages may include additional payload data with relevant details +(amounts, limits, counts, etc.). + +=cut + +sub can_place_holds { + my ( $self, $options ) = @_; + $options //= {}; + + my $overrides = $options->{overrides} // {}; + my $no_short_circuit = $options->{no_short_circuit} // 0; + + my $result = Koha::Result::Boolean->new(1); + + # expired patron check + unless ( $overrides->{expired} ) { + if ( $self->is_expired && $self->category->effective_BlockExpiredPatronOpacActions_contains('hold') ) { + $result->set_value(0); + $result->add_message( { message => 'expired', type => 'error' } ); + + return $result unless $no_short_circuit; + } + } + + # debt check + unless ( $overrides->{debt_limit} ) { + my $max_outstanding = C4::Context->preference("maxoutstanding"); + my $outstanding = $self->account->balance; + + if ( $max_outstanding && $outstanding && ( $outstanding > $max_outstanding ) ) { + $result->set_value(0); + $result->add_message( + { + message => 'debt_limit', type => 'error', + payload => { total_outstanding => $outstanding, max_outstanding => $max_outstanding } + } + ); + + return $result unless $no_short_circuit; + } + } + + # check address marked as incorrect + unless ( $overrides->{bad_address} ) { + if ( $self->gonenoaddress ) { + $result->set_value(0); + $result->add_message( { message => 'bad_address', type => 'error' } ); + + return $result unless $no_short_circuit; + } + } + + # check lost card + unless ( $overrides->{card_lost} ) { + if ( $self->lost ) { + $result->set_value(0); + $result->add_message( { message => 'card_lost', type => 'error' } ); + return $result unless $no_short_circuit; + } + } + + # check restrictions + unless ( $overrides->{restricted} ) { + if ( $self->is_debarred ) { + $result->set_value(0); + $result->add_message( { message => 'restricted', type => 'error' } ); + + return $result unless $no_short_circuit; + } + } + + # check max reserves + unless ( $overrides->{hold_limit} ) { + my $max_holds = C4::Context->preference("maxreserves"); + my $holds_count = $self->holds->count; + if ( $max_holds && ( $holds_count >= $max_holds ) ) { + $result->set_value(0); + $result->add_message( + { + message => 'hold_limit', type => 'error', + payload => { total_holds => $holds_count, max_holds => $max_holds } + } + ); + + return $result unless $no_short_circuit; + } + } + + return $result; +} + =head3 can_request_article if ( $patron->can_request_article( $library->id ) ) { ... } -- 2.39.5