diff --git a/Koha/ILL/ISO18626.pm b/Koha/ILL/ISO18626.pm new file mode 100644 index 0000000000..bf5d194d7a --- /dev/null +++ b/Koha/ILL/ISO18626.pm @@ -0,0 +1,291 @@ +package Koha::ILL::ISO18626; + +# Copyright PTFS Europe 2025 +# +# 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 JSON qw( encode_json decode_json ); +use File::Basename qw( dirname ); + +use Koha::ILL::ISO18626::RequestingAgencies; +use Koha::DateUtils qw( dt_from_string ); + +=head1 NAME + +Koha::ILL::ISO18626 - Koha ILL ISO18626 class + +=cut + +=head2 Class methods + +=head3 is_invalid + +=cut + +sub is_invalid { + my ( $definition, $json ) = @_; + + my $spec_file = dirname(__FILE__) . "/../../api/v1/swagger/swagger_bundle.json"; + if ( !-f $spec_file ) { + $spec_file = dirname(__FILE__) . "/../../api/v1/swagger/swagger.yaml"; + } + + my $schema = JSON::Validator::Schema::OpenAPIv2->new($spec_file); + + $schema->resolve( $schema->data->{definitions}->{$definition} ); + my @errors = $schema->validate($json); + + my @formatted_errors; + for my $error (@errors) { + my @details = @{ $error->{details} }; + my $mapped_details = join( ', ', map { "$_" } @details ); + + push @formatted_errors, + { errorValue => $error->{path} . ': ' . $mapped_details, errorType => 'BadlyFormedMessage' }; + } + + return \@formatted_errors if @formatted_errors; + return 0; +} + +=head2 is_auth_ok + + my $is_auth_ok = is_auth_ok($json); + +Checks the validity of the requestingAgencyAuthentication against the database. +Returns the Koha::ILL::ISO18626::RequestingAgency object if a matching accountId and securityCode are found, 0 otherwise. + +=cut + +sub is_auth_ok { + my ( $messageType, $json ) = @_; + + my $accountId = $json->{$messageType}->{header}->{requestingAgencyAuthentication}->{accountId}; + my $securityCode = $json->{$messageType}->{header}->{requestingAgencyAuthentication}->{securityCode}; + + my $requesting_agency = + Koha::ILL::ISO18626::RequestingAgencies->find( { account_id => $accountId, securityCode => $securityCode } ); + + return $requesting_agency if $requesting_agency; + return 0; +} + +=head3 confirmation_response + +This function returns an openapi ISO18626 message confirmation response based on the ISO18626 messages's type and body. +e.g. If the message_type is request, it returns a requestConfirmation message + +It does not verify if: + - The message type is valid. + - The message is valid. +These things should be done beforehand + +=cut + +sub confirmation_response { + my ( $message_type, $body, $requesting_agency ) = @_; + + my $json = JSON::decode_json($body); + my $confirmation_message_type = $message_type . 'Confirmation'; + my ( $iso18626_request, $response, $action ); + + if ( $message_type eq 'request' ) { + # + # Handle incoming request message + # + my $request = { + supplyingAgencyId => $json->{request}->{header}->{supplyingAgencyId}, + iso18626_requesting_agency_id => $requesting_agency->iso18626_requesting_agency_id, + requestingAgencyRequestId => $json->{request}->{header}->{requestingAgencyRequestId}, + service_type => $json->{request}->{serviceInfo}->{serviceType}, + _json_payload => $json + }; + + $iso18626_request = Koha::ILL::ISO18626::Request->new($request)->store(); + $iso18626_request->add_message( { type => $message_type, message => $body } ); + + # + # Prepare outgoing requestConfirmation response + # + $response = { + $confirmation_message_type => { + confirmationHeader => { + timestamp => _now_utc_str(), + timestampReceived => _now_utc_str(), + messageStatus => 'OK', + } + } + }; + } elsif ( $message_type eq 'requestingAgencyMessage' ) { + # + # Handle incoming requestingAgencyMessage message + # + my $supplyingAgencyRequestId = + $json->{requestingAgencyMessage}->{header}->{supplyingAgencyRequestId}; + $iso18626_request = Koha::ILL::ISO18626::Requests->find($supplyingAgencyRequestId); + + return { + status => 400, + openapi => error_response( + $confirmation_message_type, + [ + { + errorValue => "supplyingAgencyRequestId: " . $supplyingAgencyRequestId, + errorType => "UnrecognizedDataValue" + } + ] + ) + } unless $iso18626_request; + $action = $json->{$message_type}->{action}; + my $requestingAgencyMessage = $iso18626_request->add_message( { type => $message_type, message => $body } ); + + # Verify if the provided action is supported + my $supported_action_types = supported_action_types($iso18626_request); + return { + status => 400, + openapi => error_response( + $confirmation_message_type, + [ + { + errorValue => "action: " . $json->{$message_type}->{action}, + errorType => "UnsupportedActionType" + } + ] + ) + } + unless ( grep { $_ eq $json->{$message_type}->{action} } @$supported_action_types ); + + # + # Do the things that need to be done depending on the action + # + + if ( $action eq 'Cancel' || $action eq 'Renew' ) { + $iso18626_request->pending_requesting_agency_action($action)->store(); + } + + # TODO: Only send out supplyingAgencyMessage if $action == statusRequest (?) + #$iso18626_request->progress_request( 'requestingAgency', { message => $requestingAgencyMessage } ); #TODO: Rethink this but good enough for now + + # + # Prepare outgoing requestingAgencyMessageConfirmation response + # + $response = { + $confirmation_message_type => { + confirmationHeader => { + timestamp => _now_utc_str(), + timestampReceived => _now_utc_str(), + messageStatus => 'OK', + }, + action => $json->{$message_type}->{action}, + } + }; + } + + #TODO: Handle supplyingAgencyMessage ? + # IF a supplyingAgencyMessage is received, it means this Koha is acting as a requesting agency and we need to handle Koha::ILL::Request instead of Koha::ILL::ISO18626::Request (?) + + # Ensure the produced confirmation message is ISO18626 conformant + my $validation_errors = is_invalid( $confirmation_message_type, $response ); + if ($validation_errors) { + return { + status => 400, + openapi => error_response( + $confirmation_message_type, + $validation_errors + ) + }; + } + + # All good, save the message and return the response + sleep 1; + $iso18626_request->add_message( { type => $confirmation_message_type, message => JSON::encode_json($response) } ); + return { status => 201, openapi => $response }; + +} + +=head3 _now_utc_str + +=cut + +sub _now_utc_str { + return dt_from_string( undef, undef, 'UTC' )->strftime('%Y-%m-%dT%H:%M:%SZ'); +} + +=head3 error_response + +=cut + +sub error_response { + my ( $message_type, $error_data ) = @_; + + return { + $message_type => { + confirmationHeader => { + timestamp => _now_utc_str(), + timestampReceived => _now_utc_str(), + messageStatus => 'ERROR', + }, + errorData => $error_data, + } + }; +} + +=head3 message_types + +=cut + +sub message_types { + return [ + 'request', + 'requestConfirmation', + 'requestingAgencyMessage', + 'requestingAgencyMessageConfirmation', + 'supplyingAgencyMessage', + 'supplyingAgencyMessageConfirmation' + ]; +} + +=head3 supported_action_types + + Returns an array of supported action types for a given ISO18626 request. + Returns an empty array if the request has already been completed. + +=cut + +sub supported_action_types { + my ($iso18626_request) = @_; + + return [] + if $iso18626_request->status =~ /^(?:Cancelled|CopyCompleted|CompletedWithoutReturn|LoanCompleted|Unfilled)$/; + + return [ + 'StatusRequest', + 'Received', + 'Cancel', + + # TODO: Uncomment these once implemented + #'Renew', + #'HoldReturn', + #'ShippedReturn', + #'ShippedForward', + #'Notification', + #'Lost', + ]; +} + +1; diff --git a/Koha/ILL/ISO18626/Message.pm b/Koha/ILL/ISO18626/Message.pm new file mode 100644 index 0000000000..2e9bdc1b05 --- /dev/null +++ b/Koha/ILL/ISO18626/Message.pm @@ -0,0 +1,40 @@ +package Koha::ILL::ISO18626::Message; + +# Copyright Open Fifth 2026 +# +# 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 base qw(Koha::Object); + +=head1 NAME + +Koha::ILL::ISO18626::Message - Koha ILL ISO18626 message Object class + +=cut + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'Iso18626Message'; +} + +1; diff --git a/Koha/ILL/ISO18626/Messages.pm b/Koha/ILL/ISO18626/Messages.pm new file mode 100644 index 0000000000..69ca061530 --- /dev/null +++ b/Koha/ILL/ISO18626/Messages.pm @@ -0,0 +1,51 @@ +package Koha::ILL::ISO18626::Messages; + +# Copyright Open Fifth 2026 +# +# 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 Koha::Database; +use Koha::ILL::ISO18626::Message; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::ILL::Messages - Koha ISO18626 messages Object class + +=cut + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'Iso18626Message'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::ILL::ISO18626::Message'; +} + +1; diff --git a/Koha/ILL/ISO18626/Request.pm b/Koha/ILL/ISO18626/Request.pm new file mode 100644 index 0000000000..37639e1028 --- /dev/null +++ b/Koha/ILL/ISO18626/Request.pm @@ -0,0 +1,407 @@ +package Koha::ILL::ISO18626::Request; + +# Copyright Open Fifth 2026 +# +# 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 Koha::ILL::ISO18626::Messages; +use Koha::REST::V1; +use XML::LibXML; +use JSON qw( encode_json decode_json ); +use File::Basename qw( dirname ); + +use Koha::Biblios; +use Koha::ILL::ISO18626::RequestingAgency; +use Koha::DateUtils qw( dt_from_string ); + +use base qw(Koha::Object); + +=head1 NAME + +Koha::ILL::ISO18626::Request - Koha ILL ISO18626 request Object class + +=cut + +=head2 Internal methods + +=head3 new + +Intercept the 'new' I lifecycle method +If a supplierUniqueRecordId exists in the request payload, the +corresponding bib record is linked to this request on creation + +=cut + +sub new { + my ( $class, $params ) = @_; + + my $json = delete $params->{_json_payload}; + if ( $json && $json->{request}->{bibliographicInfo}->{supplierUniqueRecordId} ) { + my $supplierUniqueRecordId = $json->{request}->{bibliographicInfo}->{supplierUniqueRecordId}; + + my $biblio = Koha::Biblios->find($supplierUniqueRecordId); + $params->{biblio_id} = $biblio->biblionumber if $biblio; + } + + my $self = $class->SUPER::new($params); + return $self; +} + +=head3 add_message + +Add the I to this ISO18626 request + +=cut + +sub add_message { + my ( $self, $params ) = @_; + + my $type = $params->{type}; + my $message = $params->{message}; + + if ( ref $message eq 'HASH' ) { + $message = encode_json($message); + } + + return $self->_result->add_to_iso18626_messages( + { + type => $type, + content => $message, + } + ); +} + +=head3 messages + +Return the I for this ISO18626 request + +=cut + +sub messages { + my ($self) = @_; + my $messages = $self->_result->iso18626_messages->search( + {}, + { order_by => { -desc => 'timestamp' } } + ); + return Koha::ILL::ISO18626::Messages->_new_from_dbic($messages); +} + +=head3 send_message + +Send a message to the requesting agency + # $message must be a valid XML string. + +=cut + +sub send_message { + my ( $self, $type, $message ) = @_; + + # my $requesting_agency = $self->_result->requesting_agency; + + # return unless $requesting_agency->callback_endpoint; + + # my $ua = LWP::UserAgent->new( agent => 'Koha ILL' ); + # $ua->agent( 'Koha/' . Koha::version() ); + + # my $response = $ua->post( + # $requesting_agency->callback_endpoint, + # Content_Type => 'application/xml', + # Content => $message, + # ); + + # if ( $response->is_success ) { + #FIXME: Better way to convert this to json other than invoking Koha::REST::V1 ? + # my $parser = XML::LibXML->new(); + # my $doc = $parser->parse_string($message); + # my $root = $doc->documentElement(); + # my $json = Koha::REST::V1::parse_xml($root); + + # $json = JSON::encode_json($json); + $self->add_message( { type => $type, message => $message } ); + return 1; + + # } + # else { + # $self->_result->add_message( 'Request', "Failed to send message: " . $response->status_line ); + # return; + # } +} + +=head3 requesting_agency + +Returns the associated Koha::ILL::ISO18626::RequestingAgency object. + +=cut + +sub requesting_agency { + my ($self) = @_; + my $ra_rs = $self->_result->iso18626_requesting_agency; + return unless $ra_rs; + return Koha::ILL::ISO18626::RequestingAgency->_new_from_dbic($ra_rs); +} + +=head3 hold + + my $hold = $iso18626_request->hold(); + +Method that returns the related I + +=cut + +sub hold { + my ($self) = @_; + my $hold_rs = $self->_result->reserve; + return unless $hold_rs; + return Koha::Hold->_new_from_dbic($hold_rs); +} + +=head3 get_checkout + +Returns the related Koha::Checkout obj for this ISO18626 request + +=cut + +sub get_checkout { + my ($self) = @_; + my $rs = $self->_result->issue; + return unless $rs; + return Koha::Checkout->_new_from_dbic($rs); +} + +=head3 progress_request + +Progress the request by sending a message to the requesting agency with the status of the request. + +Params: + - actor: supplyingAgency or requestingAgency + - params: may contain status, messageInfoNote, message + +=cut + +sub progress_request { + my ( $self, $actor, $params ) = @_; + + return unless $actor; + + my $resulting_status = $self->status; + my $old_status = $self->status; + my $new_status = $params->{status}; + + my $reasonForMessage = 'RequestResponse'; + my $messageInfoNote = $params->{messageInfoNote} // undef; + my $answerYesNo = $params->{answerYesNo} // undef; + my $expectedDeliveryDate = _format_iso_payload_date_param( $params->{expectedDeliveryDate} ); + my $reasonUnfilled = $params->{reasonUnfilled} // undef; + my $reasonRetry = $params->{reasonRetry} // undef; + + my $courierName = + ( $params->{courierName} + && $params->{reasonRetry} eq 'ReqDelMethodNotSupp' + && $params->{deliveryMethod} + && grep { $_ eq 'Courier' } @{ $params->{deliveryMethod} } ) ? $params->{courierName} : undef; + my $deliveryMethod = $params->{deliveryMethod} + && $params->{reasonRetry} eq 'ReqDelMethodNotSupp' ? $params->{deliveryMethod} : undef; + my $edition = + $params->{edition} && $params->{reasonRetry} eq 'ReqEditionNotPossible' ? $params->{edition} : undef; + my $itemFormat = + $params->{itemFormat} && $params->{reasonRetry} eq 'ReqFormatNotPossible' ? $params->{itemFormat} : undef; + my $loanCondition = $params->{loanCondition} + && $params->{reasonRetry} eq 'MustMeetLoanCondition' ? $params->{loanCondition} : undef; + my $offeredCostsCurrencyCode = $params->{offeredCostsCurrencyCode} + && $params->{reasonRetry} eq 'CostExceedsMaxCost' ? $params->{offeredCostsCurrencyCode} : undef; + my $offeredCostsMonetaryValue = $params->{offeredCostsMonetaryValue} + && $params->{reasonRetry} eq 'CostExceedsMaxCost' ? $params->{offeredCostsMonetaryValue} : undef; + my $paymentMethod = + $params->{paymentMethod} && $params->{reasonRetry} eq 'ReqPayMethodNotSupported' + ? $params->{paymentMethod} + : undef; + my $retryBefore = $params->{retryBefore} && $new_status eq 'RetryPossible' ? $params->{retryBefore} : undef; + my $retryAfter = $params->{retryAfter} && $new_status eq 'RetryPossible' ? $params->{retryAfter} : undef; + my $serviceLevel = $params->{serviceLevel} + && $params->{reasonRetry} eq 'ReqServLevelNotSupp' ? $params->{serviceLevel} : undef; + my $serviceType = + $params->{serviceType} && $params->{reasonRetry} eq 'ReqServTypeNotPossible' ? $params->{serviceType} : undef; + my $volume = $params->{volume} && $params->{reasonRetry} eq 'MultiVolAvail' ? $params->{volume} : undef; + + if ( $actor eq 'requestingAgency' ) { + return unless $params->{message}; + + my $message = $params->{message}; + my $json_message = JSON::decode_json( $message->content ); + + my $requesting_agency_action = $json_message->{requestingAgencyMessage}->{action}; + + if ( $requesting_agency_action eq 'StatusRequest' ) { + $reasonForMessage = 'StatusRequestResponse'; + } elsif ( $requesting_agency_action eq 'Received' ) { + return; + } + } elsif ( $actor eq 'supplyingAgency' ) { + return unless $new_status; + + $resulting_status = $new_status if $actor eq 'supplyingAgency'; + + if ( $resulting_status ne $old_status ) { + $reasonForMessage = 'StatusChange'; + } + + if ( $new_status eq 'Cancelled' ) { + $reasonForMessage = 'CancelResponse'; + $resulting_status = $old_status unless $answerYesNo eq 'Y'; + $self->pending_requesting_agency_action(undef)->store; + } + + #TODO: Handle other statuses + # elsif ( $new_status eq 'Renew' ) { #Handle 'renew', it's not a status + # $reasonForMessage = 'RenewResponse'; + # $answerYesNo = $params->{answerYesNo}; + # $resulting_status = ?????; + # } + + } else { + return; + } + + my $check_out = $self->get_checkout; + my $check_out_item = $self->get_checkout ? $self->get_checkout->item : undef; + + my $json = { + supplyingAgencyMessage => { + header => { + supplyingAgencyId => { + agencyIdType => 'ISIL', + agencyIdValue => 'sup_agency_value', + }, + requestingAgencyRequestId => 'XYZ', + supplyingAgencyRequestId => $self->iso18626_request_id, + timestamp => _format_iso_payload_date_param('now'), + requestingAgencyId => { + agencyIdType => 'ISIL', + agencyIdValue => 'req_agency_value', + }, + }, + messageInfo => { + reasonForMessage => $reasonForMessage, + $answerYesNo ? ( answerYesNo => $answerYesNo ) : (), + $messageInfoNote ? ( note => $messageInfoNote ) : (), + $reasonUnfilled && $resulting_status eq 'Unfilled' ? ( reasonUnfilled => $reasonUnfilled ) : (), + $reasonRetry && $resulting_status eq 'RetryPossible' ? ( reasonRetry => $reasonRetry ) : (), + }, + statusInfo => { + status => $resulting_status, + $expectedDeliveryDate ? ( expectedDeliveryDate => $expectedDeliveryDate ) : (), + $check_out ? ( dueDate => $check_out->date_due ) : (), + lastChange => $self->updated_on, + }, + $resulting_status eq 'RetryPossible' + ? ( + retryInfo => { + $loanCondition ? ( loanCondition => $loanCondition ) : (), + $edition ? ( edition => [ split /,/, $edition ] ) : (), + $itemFormat ? ( itemFormat => $itemFormat ) : (), + $volume ? ( volume => [ split /,/, $volume ] ) : (), + $serviceType ? ( serviceType => $serviceType ) : (), + $serviceLevel ? ( serviceLevel => $serviceLevel ) : (), + $deliveryMethod ? ( deliveryMethod => $deliveryMethod ) : (), + $courierName ? ( courierName => $courierName ) : (), + $offeredCostsMonetaryValue && $offeredCostsCurrencyCode + ? ( + offeredCosts => [ + { currencyCode => $offeredCostsCurrencyCode, monetaryValue => $offeredCostsMonetaryValue } + ] + ) + : (), + $paymentMethod ? ( paymentMethod => $paymentMethod ) : (), + $retryBefore ? ( retryBefore => $retryBefore ) : (), + $retryAfter ? ( retryAfter => $retryAfter ) : (), + } + ) + : (), + ( $resulting_status eq 'Loaned' && $check_out_item ) + ? ( + deliveryInfo => { + dateSent => $check_out->issuedate, + itemId => $check_out_item->barcode, + + #itemFormat => ['PaperCopy'], # TODO: Implement: Must come from payload (only needed if specified by RA?) + serviceType => 'Loan', + + #deliveryMethod => 'Email', # TODO: Implement: Must come from payload (only needed if specified by RA?) + #paymentMethod => 'BankTransfer' # TODO: Implement: Must come from payload (only needed if specified by RA?) + } + ) + : (), + + # TODO: Implement shippingInfo + # shippingInfo => { + # courierName => 'DHL', + # trackingId => [ '123', 'abc' ], + # insurance => 'N', + # insuranceThirdParty => 'N', + # thirdPartyName => 'Some name, if insuranceThirdParty', + # insuranceCosts => [ { currencyCode => 'EUR', monetaryValue => '50.00' } ] + # } + }, + }; + + my $spec_file = dirname(__FILE__) . "/../../../api/v1/swagger/swagger_bundle.json"; + if ( !-f $spec_file ) { + $spec_file = dirname(__FILE__) . "/../../../api/v1/swagger/swagger.yaml"; + } + + my $schema = JSON::Validator::Schema::OpenAPIv2->new($spec_file); + $schema->resolve( $schema->data->{definitions}->{supplyingAgencyMessage} ); + my @errors = $schema->validate($json); + + if (@errors) { + + #TODO: Throw exception + use Data::Dumper; + $Data::Dumper::Maxdepth = 4; + warn Dumper( '##### 2 #######################################################line: ' . __LINE__ ); + warn Dumper( \@errors ); + warn Dumper('##### end2 #######################################################'); + } + $self->status($resulting_status)->store; + $self->send_message( 'supplyingAgencyMessage', $json ); + return 1; +} + +=head2 _format_iso_payload_date_param + +Normalizes a date string to C + +=cut + +sub _format_iso_payload_date_param { + my ($date_str) = @_; + return unless $date_str; + + my $dt = eval { $date_str eq 'now' ? dt_from_string() : dt_from_string($date_str) }; + return $dt ? $dt->strftime('%Y-%m-%d %H:%M:%S') : $date_str; +} + +=head3 _type + +=cut + +sub _type { + return 'Iso18626Request'; +} + +1; diff --git a/Koha/ILL/ISO18626/RequestingAgencies.pm b/Koha/ILL/ISO18626/RequestingAgencies.pm new file mode 100644 index 0000000000..414008cb4b --- /dev/null +++ b/Koha/ILL/ISO18626/RequestingAgencies.pm @@ -0,0 +1,51 @@ +package Koha::ILL::ISO18626::RequestingAgencies; + +# Copyright Open Fifth 2026 +# +# 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 Koha::Database; +use Koha::ILL::ISO18626::RequestingAgency; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::ILL::RequestingAgencies - Koha ISO18626 Requesting Agencies Object class + +=cut + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'Iso18626RequestingAgency'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::ILL::ISO18626::RequestingAgency'; +} + +1; diff --git a/Koha/ILL/ISO18626/RequestingAgency.pm b/Koha/ILL/ISO18626/RequestingAgency.pm new file mode 100644 index 0000000000..2e2863c4d9 --- /dev/null +++ b/Koha/ILL/ISO18626/RequestingAgency.pm @@ -0,0 +1,66 @@ +package Koha::ILL::ISO18626::RequestingAgency; + +# Copyright Open Fifth 2026 +# +# 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 base qw(Koha::Object); + +=head1 NAME + +Koha::ILL::ISO18626::RequestingAgency - Koha ILL ISO18626 Requesting Agency Object class + +=cut + +=head2 Internal methods + +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::ILL::ISO18626::RequestingAgency +object on the API. + +=cut + +sub to_api_mapping { + return { + borrowernumber => 'patron_id', + }; +} + +=head3 ill_partner + +Return the ill_partner patron for this requesting agency + +=cut + +sub ill_partner { + my ($self) = @_; + my $patrons_rs = $self->_result->patron; + return unless $patrons_rs; + return Koha::Patron->_new_from_dbic($patrons_rs); +} + +=head3 _type + +=cut + +sub _type { + return 'Iso18626RequestingAgency'; +} + +1; diff --git a/Koha/ILL/ISO18626/Requests.pm b/Koha/ILL/ISO18626/Requests.pm new file mode 100644 index 0000000000..aed52c11ef --- /dev/null +++ b/Koha/ILL/ISO18626/Requests.pm @@ -0,0 +1,67 @@ +package Koha::ILL::ISO18626::Requests; + +# Copyright Open Fifth 2026 +# +# 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 Koha::Database; +use Koha::ILL::ISO18626::Request; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::ILL::Requests - Koha ISO18626 requests Object class + +=cut + +=head2 Internal methods + +=head3 get_request_by_issue_id + + my $iso18626_request = Koha::ILL::ISO18626::Requests->get_request_by_issue_id( $c->{issue_id} ); + +Returns the request provided an issue_id. + +=cut + +sub get_request_by_issue_id { + my ( $self, $issue_id ) = @_; + + return $self->find( { 'issue_id' => $issue_id } ); +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'Iso18626Request'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::ILL::ISO18626::Request'; +} + +1; diff --git a/Koha/REST/V1/ILL/ISO18626/RequestingAgencies.pm b/Koha/REST/V1/ILL/ISO18626/RequestingAgencies.pm new file mode 100644 index 0000000000..6ba222e100 --- /dev/null +++ b/Koha/REST/V1/ILL/ISO18626/RequestingAgencies.pm @@ -0,0 +1,209 @@ +package Koha::REST::V1::ILL::ISO18626::RequestingAgencies; + +# 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::ILL::ISO18626::RequestingAgencies; +use Try::Tiny qw( catch try ); + +=head1 NAME + +Koha::REST::V1::ILL::ISO18626::RequestingAgencies + +=head2 Operations + +=head3 list + +Controller function that handles listing Koha::ILL::ISO18626::RequestingAgency objects + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $reqs = $c->objects->search( Koha::ILL::ISO18626::RequestingAgencies->new ); + return $c->render( + status => 200, + openapi => $reqs, + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +Controller function that handles retrieving a single Koha::ILL::ISO18626::RequestingAgency object + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $iso18626_requesting_agency = $c->objects->find( + Koha::ILL::ISO18626::RequestingAgencies->search, + $c->param('iso18626_requesting_agency_id') + ); + + return $c->render_resource_not_found("ISO18626 requesting agency") + unless $iso18626_requesting_agency; + + return $c->render( + status => 200, + openapi => $iso18626_requesting_agency + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 add + +Controller function that handles adding a new Koha::ILL::ISO18626::RequestingAgency object + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + Koha::Database->new->schema->txn_do( + sub { + + my $body = $c->req->json; + + my $requesting_agency = Koha::ILL::ISO18626::RequestingAgency->new_from_api($body)->store; + $c->res->headers->location( + $c->req->url->to_string . '/' . $requesting_agency->iso18626_requesting_agency_id ); + return $c->render( + status => 201, + openapi => $c->objects->to_api($requesting_agency), + ); + } + ); + } catch { + + my $to_api_mapping = Koha::ILL::ISO18626::RequestingAgency->new->to_api_mapping; + + if ( blessed $_ ) { + if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) { + return $c->render( + status => 409, + openapi => { error => $_->error, conflict => $_->duplicate_id } + ); + } elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) { + return $c->render( + status => 400, + openapi => { error => "Given " . $to_api_mapping->{ $_->broken_fk } . " does not exist" } + ); + } elsif ( $_->isa('Koha::Exceptions::BadParameter') ) { + return $c->render( + status => 400, + openapi => { error => "Given " . $to_api_mapping->{ $_->parameter } . " does not exist" } + ); + } elsif ( $_->isa('Koha::Exceptions::PayloadTooLarge') ) { + return $c->render( + status => 413, + openapi => { error => $_->error } + ); + } + } + + $c->unhandled_exception($_); + }; +} + +=head3 update + +Controller function that handles updating a Koha::ILL::ISO18626::RequestingAgency object + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $requesting_agency = Koha::ILL::ISO18626::RequestingAgencies->find( $c->param('iso18626_requesting_agency_id') ); + + return $c->render_resource_not_found("ISO18626 requesting agency") + unless $requesting_agency; + + return try { + Koha::Database->new->schema->txn_do( + sub { + + my $body = $c->req->json; + + $requesting_agency->set_from_api($body)->store; + $c->res->headers->location( + $c->req->url->to_string . '/' . $requesting_agency->iso18626_requesting_agency_id ); + return $c->render( + status => 200, + openapi => $c->objects->to_api($requesting_agency), + ); + } + ); + } catch { + my $to_api_mapping = Koha::ILL::ISO18626::RequestingAgency->new->to_api_mapping; + + if ( blessed $_ ) { + if ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) { + return $c->render( + status => 400, + openapi => { error => "Given " . $to_api_mapping->{ $_->broken_fk } . " does not exist" } + ); + } elsif ( $_->isa('Koha::Exceptions::BadParameter') ) { + return $c->render( + status => 400, + openapi => { error => "Given " . $to_api_mapping->{ $_->parameter } . " does not exist" } + ); + } elsif ( $_->isa('Koha::Exceptions::PayloadTooLarge') ) { + return $c->render( + status => 413, + openapi => { error => $_->error } + ); + } + } + + $c->unhandled_exception($_); + }; +} + +=head3 delete + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $requesting_agency = Koha::ILL::ISO18626::RequestingAgencies->find( $c->param('iso18626_requesting_agency_id') ); + + return $c->render_resource_not_found("ISO18626 requesting agency") + unless $requesting_agency; + + return try { + $requesting_agency->delete; + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/Koha/REST/V1/ILL/ISO18626/Requests.pm b/Koha/REST/V1/ILL/ISO18626/Requests.pm new file mode 100644 index 0000000000..95122dc304 --- /dev/null +++ b/Koha/REST/V1/ILL/ISO18626/Requests.pm @@ -0,0 +1,103 @@ +package Koha::REST::V1::ILL::ISO18626::Requests; + +# 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::ILL::ISO18626::Requests; +use Try::Tiny qw( catch try ); + +=head1 NAME + +Koha::REST::V1::ILL::ISO18626::Requests + +=head2 Operations + +=head3 list + +Controller function that handles listing Koha::ILL::ISO18626::Request objects + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $reqs = $c->objects->search( Koha::ILL::ISO18626::Requests->new ); + return $c->render( + status => 200, + openapi => $reqs, + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +Controller function that handles retrieving a single Koha::ILL::ISO18626::Request object + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $iso18626_request = + $c->objects->find( Koha::ILL::ISO18626::Requests->search, $c->param('iso18626_request_id') ); + + return $c->render_resource_not_found("Iso18626 request") + unless $iso18626_request; + + return $c->render( + status => 200, + openapi => $iso18626_request + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 edit + +Controller function that handles editing a single Koha::ILL::ISO18626::Request object + +=cut + +sub edit { + my $c = shift or return; + + my $request_id = $c->param('iso18626_request_id'); + my $request = Koha::ILL::ISO18626::Requests->find($request_id); + + return try { + my $body = $c->req->json; + my $status = $body->{status}; + + $request->progress_request( 'supplyingAgency', $body ); #TODO: Rethink this but good enough for now + + return $c->render( + status => 200, + openapi => $request->to_api + ); + } catch { + return $c->unhandled_exception($_); + }; +} + +1; diff --git a/Koha/Schema/Result/Borrower.pm b/Koha/Schema/Result/Borrower.pm index 241f5fbc0b..37e7f7cf75 100644 --- a/Koha/Schema/Result/Borrower.pm +++ b/Koha/Schema/Result/Borrower.pm @@ -1526,6 +1526,21 @@ __PACKAGE__->has_many( { cascade_copy => 0, cascade_delete => 0 }, ); +=head2 iso18626_requesting_agency + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "iso18626_requesting_agency", + "Koha::Schema::Result::Iso18626RequestingAgency", + { "foreign.borrowernumber" => "self.borrowernumber" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + =head2 issues Type: has_many @@ -2227,8 +2242,8 @@ Composing rels: L -> permission __PACKAGE__->many_to_many("permissions", "user_permissions", "permission"); -# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-11-03 14:19:07 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:NHrhTcf4p6JW5fNgfYiqeQ +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-04-14 10:19:11 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Tptr5nUuS5haVck624FS3Q __PACKAGE__->belongs_to( "library", diff --git a/Koha/Schema/Result/Iso18626Message.pm b/Koha/Schema/Result/Iso18626Message.pm new file mode 100644 index 0000000000..cc920a3707 --- /dev/null +++ b/Koha/Schema/Result/Iso18626Message.pm @@ -0,0 +1,150 @@ +use utf8; +package Koha::Schema::Result::Iso18626Message; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::Iso18626Message + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("iso18626_messages"); + +=head1 ACCESSORS + +=head2 iso18626_message_id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +Internal message number + +=head2 iso18626_request_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +Associated ISO18626 request + +=head2 content + + data_type: 'mediumtext' + is_nullable: 0 + +Message content (XML) + +=head2 type + + data_type: 'enum' + extra: {list => ["request","requestConfirmation","supplyingAgencyMessage","supplyingAgencyMessageConfirmation","requestingAgencyMessage","requestingAgencyMessageConfirmation"]} + is_nullable: 0 + +ISO18626 message type + +=head2 timestamp + + data_type: 'timestamp' + datetime_undef_if_invalid: 1 + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "iso18626_message_id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "iso18626_request_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "content", + { data_type => "mediumtext", is_nullable => 0 }, + "type", + { + data_type => "enum", + extra => { + list => [ + "request", + "requestConfirmation", + "supplyingAgencyMessage", + "supplyingAgencyMessageConfirmation", + "requestingAgencyMessage", + "requestingAgencyMessageConfirmation", + ], + }, + is_nullable => 0, + }, + "timestamp", + { + data_type => "timestamp", + datetime_undef_if_invalid => 1, + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("iso18626_message_id"); + +=head1 RELATIONS + +=head2 iso18626_request + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "iso18626_request", + "Koha::Schema::Result::Iso18626Request", + { iso18626_request_id => "iso18626_request_id" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-09-26 14:56:58 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3IbJ4SgKGHU+cXJL4LFqwg + +=head2 koha_object_class + +Missing POD for koha_object_class. + +=cut + +sub koha_object_class { + 'Koha::ILL::ISO18626::Message'; +} + +=head2 koha_objects_class + +Missing POD for koha_objects_class. + +=cut + +sub koha_objects_class { + 'Koha::ILL::ISO18626::Messages'; +} + +1; diff --git a/Koha/Schema/Result/Iso18626Request.pm b/Koha/Schema/Result/Iso18626Request.pm new file mode 100644 index 0000000000..aeb1715c0f --- /dev/null +++ b/Koha/Schema/Result/Iso18626Request.pm @@ -0,0 +1,276 @@ +use utf8; +package Koha::Schema::Result::Iso18626Request; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::Iso18626Request + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("iso18626_requests"); + +=head1 ACCESSORS + +=head2 iso18626_request_id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +Internal request number + +=head2 supplyingAgencyId + + accessor: 'supplying_agency_id' + data_type: 'varchar' + is_nullable: 1 + size: 80 + +Supplying agency ID + +=head2 iso18626_requesting_agency_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +Associated ISO18626 requesting agency + +=head2 created_on + + data_type: 'timestamp' + datetime_undef_if_invalid: 1 + default_value: current_timestamp + is_nullable: 0 + +Date and time the request was created + +=head2 updated_on + + data_type: 'timestamp' + datetime_undef_if_invalid: 1 + default_value: current_timestamp + is_nullable: 0 + +Date and time the request was last updated + +=head2 requestingAgencyRequestId + + accessor: 'requesting_agency_request_id' + data_type: 'varchar' + is_nullable: 1 + size: 80 + +Requesting agency request ID or number + +=head2 status + + data_type: 'enum' + default_value: 'RequestReceived' + extra: {list => ["RequestReceived","ExpectToSupply","WillSupply","Loaned","Overdue","Recalled","RetryPossible","Unfilled","HoldReturn","ReleaseHoldReturn","CopyCompleted","LoanCompleted","CompletedWithoutReturn","Cancelled"]} + is_nullable: 1 + +Current ISO18626 status of request + +=head2 service_type + + data_type: 'enum' + extra: {list => ["Copy","Loan","CopyOrLoan"]} + is_nullable: 0 + +ISO18626 service type + +=head2 pending_requesting_agency_action + + data_type: 'enum' + extra: {list => ["Cancel","Renew"]} + is_nullable: 1 + +ISO18626 Requesting Agency action that requires a manual response (yes or no) + +=cut + +__PACKAGE__->add_columns( + "iso18626_request_id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "supplyingAgencyId", + { + accessor => "supplying_agency_id", + data_type => "varchar", + is_nullable => 1, + size => 80, + }, + "iso18626_requesting_agency_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "created_on", + { + data_type => "timestamp", + datetime_undef_if_invalid => 1, + default_value => \"current_timestamp", + is_nullable => 0, + }, + "updated_on", + { + data_type => "timestamp", + datetime_undef_if_invalid => 1, + default_value => \"current_timestamp", + is_nullable => 0, + }, + "requestingAgencyRequestId", + { + accessor => "requesting_agency_request_id", + data_type => "varchar", + is_nullable => 1, + size => 80, + }, + "status", + { + data_type => "enum", + default_value => "RequestReceived", + extra => { + list => [ + "RequestReceived", + "ExpectToSupply", + "WillSupply", + "Loaned", + "Overdue", + "Recalled", + "RetryPossible", + "Unfilled", + "HoldReturn", + "ReleaseHoldReturn", + "CopyCompleted", + "LoanCompleted", + "CompletedWithoutReturn", + "Cancelled", + ], + }, + is_nullable => 1, + }, + "service_type", + { + data_type => "enum", + extra => { list => ["Copy", "Loan", "CopyOrLoan"] }, + is_nullable => 0, + }, + "pending_requesting_agency_action", + { + data_type => "enum", + extra => { list => ["Cancel", "Renew"] }, + is_nullable => 1, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("iso18626_request_id"); + +=head1 RELATIONS + +=head2 iso18626_messages + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "iso18626_messages", + "Koha::Schema::Result::Iso18626Message", + { "foreign.iso18626_request_id" => "self.iso18626_request_id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 iso18626_requesting_agency + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "iso18626_requesting_agency", + "Koha::Schema::Result::Iso18626RequestingAgency", + { + iso18626_requesting_agency_id => "iso18626_requesting_agency_id", + }, + { is_deferrable => 1, on_delete => "RESTRICT", on_update => "CASCADE" }, +); + +=head2 old_reserve + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "old_reserve", + "Koha::Schema::Result::OldReserve", + { "foreign.iso18626_request_id" => "self.iso18626_request_id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 reserve + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "reserve", + "Koha::Schema::Result::Reserve", + { "foreign.iso18626_request_id" => "self.iso18626_request_id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-03-04 14:57:44 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:T0PCb3b3SH9O7/f3TYDnTw + +=head2 koha_object_class + +Missing POD for koha_object_class. + +=cut + +sub koha_object_class { + 'Koha::ILL::ISO18626::Request'; +} + +=head2 koha_objects_class + +Missing POD for koha_objects_class. + +=cut + +sub koha_objects_class { + 'Koha::ILL::ISO18626::Requests'; +} + +1; diff --git a/Koha/Schema/Result/Iso18626RequestingAgency.pm b/Koha/Schema/Result/Iso18626RequestingAgency.pm new file mode 100644 index 0000000000..d7bee79bee --- /dev/null +++ b/Koha/Schema/Result/Iso18626RequestingAgency.pm @@ -0,0 +1,206 @@ +use utf8; +package Koha::Schema::Result::Iso18626RequestingAgency; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::Iso18626RequestingAgency + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("iso18626_requesting_agencies"); + +=head1 ACCESSORS + +=head2 iso18626_requesting_agency_id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +Internal requesting agency number + +=head2 name + + data_type: 'varchar' + is_nullable: 1 + size: 80 + +Requesting agency name + +=head2 borrowernumber + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +foreign key, linking this to the borrowers table (ILL partner patron) + +=head2 type + + data_type: 'enum' + extra: {list => ["DNUCNI","ICOLC","ISIL"]} + is_nullable: 0 + +ISO18626 agency type + +=head2 account_id + + data_type: 'varchar' + is_nullable: 0 + size: 80 + +Authentication: Requesting agency account ID + +=head2 securityCode + + accessor: 'security_code' + data_type: 'varchar' + is_nullable: 0 + size: 80 + +Authentication: Requesting agency security code + +=head2 callback_endpoint + + data_type: 'mediumtext' + is_nullable: 0 + +Callback endpoint to send messages back to + +=cut + +__PACKAGE__->add_columns( + "iso18626_requesting_agency_id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "name", + { data_type => "varchar", is_nullable => 1, size => 80 }, + "borrowernumber", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "type", + { + data_type => "enum", + extra => { list => ["DNUCNI", "ICOLC", "ISIL"] }, + is_nullable => 0, + }, + "account_id", + { data_type => "varchar", is_nullable => 0, size => 80 }, + "securityCode", + { + accessor => "security_code", + data_type => "varchar", + is_nullable => 0, + size => 80, + }, + "callback_endpoint", + { data_type => "mediumtext", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("iso18626_requesting_agency_id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("uniq_borrowernumber", ["borrowernumber"]); + +=head1 RELATIONS + +=head2 borrowernumber + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "borrowernumber", + "Koha::Schema::Result::Borrower", + { borrowernumber => "borrowernumber" }, + { is_deferrable => 1, on_delete => "RESTRICT", on_update => "CASCADE" }, +); + +=head2 iso18626_requests + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "iso18626_requests", + "Koha::Schema::Result::Iso18626Request", + { + "foreign.iso18626_requesting_agency_id" => "self.iso18626_requesting_agency_id", + }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2026-01-21 12:22:43 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vqPIA624r/PznpUwlK2FBg + +__PACKAGE__->belongs_to( + "patron", + "Koha::Schema::Result::Borrower", + { borrowernumber => "borrowernumber" }, + { + is_deferrable => 1, + join_type => "LEFT", + on_delete => "CASCADE", + on_update => "CASCADE", + }, +); + +=head2 koha_object_class + +Missing POD for koha_object_class. + +=cut + +sub koha_object_class { + 'Koha::ILL::ISO18626::RequestingAgency'; +} + +=head2 koha_objects_class + +Missing POD for koha_objects_class. + +=cut + +sub koha_objects_class { + 'Koha::ILL::ISO18626::RequestingAgencies'; +} + +1; diff --git a/api/v1/swagger/definitions/iso18626_request.yaml b/api/v1/swagger/definitions/iso18626_request.yaml new file mode 100644 index 0000000000..a958b637f7 --- /dev/null +++ b/api/v1/swagger/definitions/iso18626_request.yaml @@ -0,0 +1,58 @@ +--- +type: object +properties: + iso18626_request_id: + type: integer + description: Internal ILL request identifier + supplyingAgencyId: + type: + - string + - "null" + description: Internal supplying agency identifier + status: + type: string + description: ISO18626 request status + service_type: + description: ISO18626 request service type + $ref: "../definitions/ISO18626/types/serviceType.yaml" + pending_requesting_agency_action: + type: + - string + - "null" + enum: + - Cancel + - Renew + - null + description: Requesting Agency action that requires an action from the supplying agency + created_on: + type: + - string + - "null" + format: date-time + description: Date and time the request was created + updated_on: + type: + - string + - "null" + format: date-time + description: Date and time the request was last updated + requestingAgencyRequestId: + type: string + description: Requesting agency request ID or number + iso18626_requesting_agency_id: + type: integer + description: Associated ISO18626 requesting agency internal identifier + requesting_agency: + type: + - object + - "null" + description: Embeded object + messages: + type: + - array + - "null" + description: The linked messages (x-koha-embed) +required: + - service_type + - status +additionalProperties: false diff --git a/api/v1/swagger/definitions/iso18626_requesting_agency.yaml b/api/v1/swagger/definitions/iso18626_requesting_agency.yaml new file mode 100644 index 0000000000..f8bb860654 --- /dev/null +++ b/api/v1/swagger/definitions/iso18626_requesting_agency.yaml @@ -0,0 +1,44 @@ +--- +type: object +properties: + iso18626_requesting_agency_id: + type: integer + description: Internal requesting agency number + readOnly: true + name: + type: string + description: Requesting agency name + patron_id: + type: + - integer + - "null" + description: Internal patron id + type: + type: string + enum: + - DNUCNI # Danish National Union Catalogue Non-ISIL Identifier + - ICOLC # https://icolc.net/consortia + - ISIL # ISO 15511 + description: ISO18626 requesting agency type + account_id: + type: string + description: 'Authentication: Requesting agency account ID' + securityCode: + type: string + description: 'Authentication: Requesting agency security code' + callback_endpoint: + type: string + description: Callback endpoint to send messages back to + ill_partner: + description: ILL partner patron object + type: + - object + - "null" +required: + - iso18626_requesting_agency_id + - name + - type + - account_id + - securityCode + - callback_endpoint +additionalProperties: false diff --git a/api/v1/swagger/paths/ill_iso18626_requesting_agencies.yaml b/api/v1/swagger/paths/ill_iso18626_requesting_agencies.yaml new file mode 100644 index 0000000000..a77047c39e --- /dev/null +++ b/api/v1/swagger/paths/ill_iso18626_requesting_agencies.yaml @@ -0,0 +1,281 @@ +--- +/ill/iso18626_requesting_agencies: + get: + x-mojo-to: ILL::ISO18626::RequestingAgencies#list + operationId: listIso18626IllrequestingAgencies + tags: + - iso18626_requesting_agencies + summary: List ISO18626 ILL requesting agencies + parameters: + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - ill_partner + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + produces: + - application/json + responses: + "200": + description: A list of ISO18626 ILL requesting agencies + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/iso18626_requesting_agency" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: "1" + post: + x-mojo-to: ILL::ISO18626::RequestingAgencies#add + operationId: addIso18626IllrequestingAgencies + tags: + - iso18626_requesting_agencies + summary: Add ISO18626 requesting agency + consumes: + - application/json + produces: + - application/json + parameters: + - description: A JSON object containing information about the new requesting agency + in: body + name: body + required: true + schema: + $ref: "../swagger.yaml#/definitions/iso18626_requesting_agency" + responses: + 201: + description: A successfully created ISO18626 requesting agency + schema: + items: + $ref: "../swagger.yaml#/definitions/iso18626_requesting_agency" + 400: + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + 401: + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + 403: + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + 404: + description: Ressource not found + schema: + $ref: "../swagger.yaml#/definitions/error" + 409: + description: Conflict in creating resource + schema: + $ref: "../swagger.yaml#/definitions/error" + 413: + description: Payload too large + schema: + $ref: "../swagger.yaml#/definitions/error" + 500: + description: |- + Internal server error. Possible `error_code` attribute values: + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + 503: + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: 1 + +/ill/iso18626_requesting_agencies/{iso18626_requesting_agency_id}: + get: + x-mojo-to: ILL::ISO18626::RequestingAgencies#get + operationId: getIso18626IllrequestingAgency + tags: + - iso18626_requesting_agencies + summary: Retrieve a single ISO18626 ILL requesting agency + parameters: + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - ill_partner + - $ref: "../swagger.yaml#/parameters/iso18626_requesting_agency_id_pp" + produces: + - application/json + responses: + "200": + description: A single ISO18626 ILL requesting agency + schema: + $ref: "../swagger.yaml#/definitions/iso18626_requesting_agency" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: "1" + put: + x-mojo-to: ILL::ISO18626::RequestingAgencies#update + operationId: updateIso18626IllrequestingAgency + tags: + - iso18626_requesting_agencies + summary: Update ISO18626 ILL requesting agency + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/iso18626_requesting_agency_id_pp" + - name: body + in: body + description: A JSON object containing new information about existing ISO18626 ILL requesting agency + required: true + schema: + $ref: "../swagger.yaml#/definitions/iso18626_requesting_agency" + responses: + 200: + description: A successfully updated ISO18626 ILL requesting agency + schema: + items: + $ref: "../swagger.yaml#/definitions/iso18626_requesting_agency" + 400: + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + 403: + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + 404: + description: Ressource not found + schema: + $ref: "../swagger.yaml#/definitions/error" + 409: + description: Conflict in updating resource + schema: + $ref: "../swagger.yaml#/definitions/error" + 413: + description: Payload too large + schema: + $ref: "../swagger.yaml#/definitions/error" + 500: + description: |- + Internal server error. Possible `error_code` attribute values: + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + 503: + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + erm: 1 + delete: + x-mojo-to: ILL::ISO18626::RequestingAgencies#delete + operationId: deleteIso18626IllrequestingAgency + tags: + - iso18626_requesting_agencies + summary: Delete ISO18626 ILL requesting agency + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/iso18626_requesting_agency_id_pp" + responses: + 204: + description: ISO18626 ILL requesting agency deleted + 400: + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + 401: + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + 403: + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + 404: + description: Ressource not found + schema: + $ref: "../swagger.yaml#/definitions/error" + 409: + description: Conflict in deleting resource + schema: + $ref: "../swagger.yaml#/definitions/error" + 500: + description: |- + Internal server error. Possible `error_code` attribute values: + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + 503: + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + erm: 1 diff --git a/api/v1/swagger/paths/ill_iso18626_requests.yaml b/api/v1/swagger/paths/ill_iso18626_requests.yaml new file mode 100644 index 0000000000..12a16a8358 --- /dev/null +++ b/api/v1/swagger/paths/ill_iso18626_requests.yaml @@ -0,0 +1,185 @@ +--- +/ill/iso18626_requests: + get: + x-mojo-to: ILL::ISO18626::Requests#list + operationId: listIso18626Illrequests + tags: + - iso18626_requests + summary: List ISO18626 ILL requests + parameters: + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - messages + - requesting_agency + collectionFormat: csv + produces: + - application/json + responses: + "200": + description: A list of ISO18626 ILL requests + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/iso18626_request" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: "1" + +/ill/iso18626_requests/{iso18626_request_id}: + get: + x-mojo-to: ILL::ISO18626::Requests#get + operationId: getIso18626Illrequest + tags: + - iso18626_requests + summary: Retrieve a single ISO18626 ILL request + parameters: + - $ref: "../swagger.yaml#/parameters/iso18626_request_id_pp" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - messages + - requesting_agency + collectionFormat: csv + produces: + - application/json + responses: + "200": + description: A single ISO18626 ILL request + schema: + $ref: "../swagger.yaml#/definitions/iso18626_request" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: "1" + patch: + x-mojo-to: ILL::ISO18626::Requests#edit + operationId: editIso18626Illrequest + tags: + - iso18626_requests + summary: Edit a single ISO18626 ILL request + produces: + - application/json + parameters: + - description: A JSON object containing the new status + in: body + name: status + required: true + schema: + type: object + properties: + status: + description: The status of the ISO18626 request + $ref: "../definitions/ISO18626/types/status.yaml" + answerYesNo: + description: The answerYesNo of the ISO18626 request + $ref: "../definitions/ISO18626/types/yesNo.yaml" + messageInfoNote: + description: The messageInfo note of the ISO18626 request + type: + - string + - "null" + additionalProperties: false + - $ref: "../swagger.yaml#/parameters/iso18626_request_id_pp" + responses: + 200: + description: The ISO18626 ILL request that was updated + schema: + $ref: "../swagger.yaml#/definitions/iso18626_request" + 400: + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + 401: + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + 403: + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + 404: + description: Package not found + schema: + $ref: "../swagger.yaml#/definitions/error" + 500: + description: |- + Internal server error. Possible `error_code` attribute values: + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + 503: + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: 1 diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 0001e0744a..6232cbec7a 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -120,6 +120,10 @@ definitions: $ref: ./definitions/ill_batchstatus.yaml ill_batchstatuses: $ref: ./definitions/ill_batchstatuses.yaml + iso18626_request: + $ref: ./definitions/iso18626_request.yaml + iso18626_requesting_agency: + $ref: ./definitions/iso18626_requesting_agency.yaml ISO18626request: $ref: ./definitions/ISO18626/request.yaml ISO18626requestingAgencyMessage: @@ -471,6 +475,14 @@ paths: $ref: ./paths/ill_backends.yaml#/~1ill~1backends "/ill/backends/{ill_backend_id}": $ref: "./paths/ill_backends.yaml#/~1ill~1backends~1{ill_backend_id}" + /ill/iso18626_requests: + $ref: ./paths/ill_iso18626_requests.yaml#/~1ill~1iso18626_requests + "/ill/iso18626_requests/{iso18626_request_id}": + $ref: ./paths/ill_iso18626_requests.yaml#/~1ill~1iso18626_requests~1{iso18626_request_id} + /ill/iso18626_requesting_agencies: + $ref: ./paths/ill_iso18626_requesting_agencies.yaml#/~1ill~1iso18626_requesting_agencies + "/ill/iso18626_requesting_agencies/{iso18626_requesting_agency_id}": + $ref: ./paths/ill_iso18626_requesting_agencies.yaml#/~1ill~1iso18626_requesting_agencies~1{iso18626_requesting_agency_id} /ill/requests: $ref: ./paths/ill_requests.yaml#/~1ill~1requests /ill/batches: @@ -887,6 +899,12 @@ parameters: name: iso18626_request_id required: true type: integer + iso18626_requesting_agency_id_pp: + description: ISO18626 ILL requesting agency internal identifier + in: path + name: iso18626_requesting_agency_id + required: true + type: integer import_batch_profile_id_pp: description: Internal profile identifier in: path