From 63696240ec9ac98e5f98367676d41e8ac2a3141a Mon Sep 17 00:00:00 2001 From: Jan Kissig Date: Tue, 2 Jul 2024 10:27:44 +0200 Subject: [PATCH] Bug 24401: Add API endpoint to checkin items This adds a post /checkin endpoint to return checked out items either by its item_id or barcode. To test you can use f.e. ThunderClient extension in codium/vscode. Test plan for ktd on main: a) enable system preference RESTBasicAuth b) check out an item and remember its item id or barcode. You can use the checkouts endpoint for this: Auth: username: koha & password: koha Body JSON: { "item_id" : 578, "library_id": "CPL" } POST http://localhost:8081/api/v1/checkouts c) apply patch and check in item via new checkin endpoint Auth: username: koha & password: koha Body JSON: { "item_id" : 578, "library_id": "CPL" } or { "external_id" : "39999000011418", "library_id": "CPL" } POST http://localhost:8081/api/v1/checkin The endpoint accepts item_id and/or barcode as body params. d) check JSON response for return messages e) run unit tests: prove t/db_dependent/api/v1/checkin.t Signed-off-by: Aleisha Amohia --- Koha/REST/V1/Checkouts.pm | 94 ++++++++++++ api/v1/swagger/definitions/checkin.yaml | 20 +++ api/v1/swagger/paths/checkin.yaml | 63 ++++++++ api/v1/swagger/swagger.yaml | 4 + t/db_dependent/api/v1/checkin.t | 192 ++++++++++++++++++++++++ 5 files changed, 373 insertions(+) create mode 100644 api/v1/swagger/definitions/checkin.yaml create mode 100644 api/v1/swagger/paths/checkin.yaml create mode 100755 t/db_dependent/api/v1/checkin.t diff --git a/Koha/REST/V1/Checkouts.pm b/Koha/REST/V1/Checkouts.pm index 301369a45c..d2a7945168 100644 --- a/Koha/REST/V1/Checkouts.pm +++ b/Koha/REST/V1/Checkouts.pm @@ -281,6 +281,100 @@ sub add { }; } +=head3 checkin + +Check in a checked out item via item_id or barcode + +=cut + +sub checkin { + + my $c = shift->openapi->valid_input or return; + my $body = $c->req->json; + my $item_id = $body->{item_id}; + my $barcode = $body->{external_id}; + my $library_id = $body->{library_id}; + + my $exemptfine = $body->{exemptfine}; + + return try { + + unless ( $item_id or $barcode ) { + return $c->render( + status => 400, + openapi => { + error => 'Missing or wrong parameters', + error_code => 'MISSING_OR_WRONG_PARAMETERS', + } + ); + } + + my $item; + if ($item_id) { + $item = Koha::Items->find($item_id); + } + else { + $item = Koha::Items->find( { barcode => $barcode } ); + } + + unless ($item) { + return $c->render( + status => 404, + openapi => { + error => 'Item not found', + error_code => 'ITEM_NOT_FOUND', + } + ); + } + + # check that item matches when barcode and item_id were given + if ( $item_id + and $barcode + and ( $item->barcode ne $barcode or $item->id != $item_id ) ) + { + return $c->render( + status => 409, + openapi => { + error => + 'Item id and external id belong to different items', + error_code => 'ITEM_ID_NOT_MATCHING_EXTERNAL_ID', + } + ); + } + + my ( $returned, $return_messages ) = + C4::Circulation::AddReturn( $item->barcode, $library_id, + $exemptfine ); + + my $response = { returned => $returned, messages => $return_messages }; + + if ($return_messages->{BadBarcode}){ + $response->{error} = 'Item not found.'; + return $c->render( + status => 409, + openapi => $response + ); + } + + unless ($returned) { + return $c->render( + status => 403, + openapi => $response + ); + } + + $c->res->headers->location( $c->req->url->to_string ); + return $c->render( + status => 201, + openapi => $response + ); + + } + catch { + $c->unhandled_exception($_); + }; +} + =head3 get_renewals List Koha::Checkout::Renewals diff --git a/api/v1/swagger/definitions/checkin.yaml b/api/v1/swagger/definitions/checkin.yaml new file mode 100644 index 0000000000..29bae3b611 --- /dev/null +++ b/api/v1/swagger/definitions/checkin.yaml @@ -0,0 +1,20 @@ +--- +type: object +properties: + returned: + type: integer + description: True if return checkin succeeded + readOnly: true + messages: + description: Messages to the returned item + type: object + item_id: + type: integer + description: Internal item identifier + external_id: + type: string + description: Barcode of the item + exemptfine: + type: boolean + description: If true overdue charges for the item will be removed +additionalProperties: true \ No newline at end of file diff --git a/api/v1/swagger/paths/checkin.yaml b/api/v1/swagger/paths/checkin.yaml new file mode 100644 index 0000000000..0820ca322e --- /dev/null +++ b/api/v1/swagger/paths/checkin.yaml @@ -0,0 +1,63 @@ +--- +/checkin: + post: + x-mojo-to: Checkouts#checkin + operationId: addCheckin + tags: + - checkin + - patrons + summary: Add a checkin + parameters: + - name: body + in: body + description: A JSON object containing information about the checkin + required: true + schema: + $ref: "../swagger.yaml#/definitions/checkin" + consumes: + - application/json + produces: + - application/json + responses: + "201": + description: Created checkin + schema: + $ref: "../swagger.yaml#/definitions/checkin" + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Cannot create checkin + schema: + $ref: "../swagger.yaml#/definitions/checkin" + "404": + description: Item not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict in creating checkin + schema: + $ref: "../swagger.yaml#/definitions/error" + "412": + description: Precondition failed + 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: + circulate: circulate_remaining_permissions diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index dd72361380..bcc80ea793 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -26,6 +26,8 @@ definitions: $ref: ./definitions/cash_register.yaml cashup: $ref: ./definitions/cashup.yaml + checkin: + $ref: ./definitions/checkin.yaml checkout: $ref: ./definitions/checkout.yaml checkouts: @@ -271,6 +273,8 @@ paths: $ref: "./paths/cash_registers.yaml#/~1cash_registers~1{cash_register_id}~1cashups" "/cashups/{cashup_id}": $ref: "./paths/cash_registers.yaml#/~1cashups~1{cashup_id}" + /checkin: + $ref: ./paths/checkin.yaml#/~1checkin /checkouts: $ref: ./paths/checkouts.yaml#/~1checkouts "/checkouts/{checkout_id}": diff --git a/t/db_dependent/api/v1/checkin.t b/t/db_dependent/api/v1/checkin.t new file mode 100755 index 0000000000..5c00ebbec8 --- /dev/null +++ b/t/db_dependent/api/v1/checkin.t @@ -0,0 +1,192 @@ +#!/usr/bin/env perl + +# 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 Test::More tests => 22; +use Test::MockModule; +use Test::Mojo; +use t::lib::Mocks; +use t::lib::TestBuilder; + +use DateTime; + +use C4::Context; +use C4::Circulation qw( AddIssue AddReturn CanBookBeIssued ); + +use Koha::Database; +use Koha::DateUtils qw( dt_from_string output_pref ); +use Koha::Token; + +my $schema = Koha::Database->schema; +my $builder = t::lib::TestBuilder->new; + +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); +my $t = Test::Mojo->new('Koha::REST::V1'); + +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; + +my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; +my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2, branchcode => $branchcode } + } +); +my $password = 'thePassword123'; +$librarian->set_password( { password => $password, skip_validation => 1 } ); +my $userid = $librarian->userid; + +my $other_branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; +my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0, branchcode => $other_branchcode }, + } +); +my $unauth_password = 'thePassword000'; +$patron->set_password( + { password => $unauth_password, skip_validattion => 1 } ); +my $unauth_userid = $patron->userid; +my $patron_id = $patron->borrowernumber; + +my $item1 = $builder->build_sample_item; +my $item1_id = $item1->id; + +my $itemtype = $builder->build( { source => 'Itemtype' } )->{itemtype}; +my $biblio_2 = $builder->build_sample_biblio; +my $item_2 = $builder->build_sample_item( + { biblionumber => $biblio_2->biblionumber, itype => $itemtype } ); +my $item_2_barcode = $item_2->barcode; +my $item_2_id = $item_2->id; + +my $not_issued_item = $builder->build_sample_item; +my $not_issued_item_id = $not_issued_item->id; +my $not_issued_item_barcode = $not_issued_item->barcode; + +my $barcode = '321321321321'; +my $matching_items = Koha::Items->search( { barcode => $barcode } ); +while ( my $item = $matching_items->next ) { + $item->delete; +} + +my $non_existent_item = $builder->build_sample_item; +my $non_existent_item_id = $non_existent_item->id; +my $non_existent_item_barcode = $non_existent_item->barcode; +$non_existent_item->delete; + +my $checkout = $builder->build_object( + { + class => 'Koha::Checkouts', + value => { + itemnumber => $item1_id, + branchcode => $branchcode, + borrowernumber => $patron_id, + note => undef, + notedate => undef, + noteseen => undef + } + } +); +my $checkout_id = $checkout->id; + +my $reserved_checkout = $builder->build_object( + { + class => 'Koha::Checkouts', + value => { + itemnumber => $item_2_id, + branchcode => $branchcode, + borrowernumber => $patron_id, + note => undef, + notedate => undef, + noteseen => undef + } + } +); +my $reserved_checkout_id = $reserved_checkout->id; + +$dbh->do('DELETE FROM reserves'); +Koha::CirculationRules->search()->delete(); +Koha::CirculationRules->set_rules( + { + categorycode => undef, + branchcode => undef, + itemtype => undef, + rules => { + reservesallowed => 1, + holds_per_record => 99 + } + } +); + +my $reserve_id = C4::Reserves::AddReserve( + { + branchcode => $branchcode, + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio_2->biblionumber, + priority => 1, + itemnumber => $item_2->itemnumber, + } +); + +# empty checkin +$t->post_ok( "//$userid:$password@/api/v1/checkin" => json => {} ) + ->status_is(400); + +# checkin on unknow item id +$t->post_ok( "//$userid:$password@/api/v1/checkin" => json => + { item_id => $non_existent_item_id } )->status_is(404) + ->json_is( { error => 'Item not found', error_code => 'ITEM_NOT_FOUND' } ); + +# checkin with unknow barcode +$t->post_ok( "//$userid:$password@/api/v1/checkin" => json => + { external_id => $non_existent_item_barcode } )->status_is(404) + ->json_is( { error => 'Item not found', error_code => 'ITEM_NOT_FOUND' } ); + +# not isued +$t->post_ok( "//$userid:$password@/api/v1/checkin" => json => + { item_id => $not_issued_item_id, library_id => $branchcode } ) + ->status_is(403)->json_is( '/returned' => 0 ) + ->json_has( '/messages' => { NotIssued => $not_issued_item_barcode } ); + +# checkin okay +$t->post_ok( "//$userid:$password@/api/v1/checkin" => json => + { item_id => $item1_id, library_id => $branchcode } )->status_is(201) + ->json_is( '/returned' => 1 ) + ->json_has( + '/messages' => { TransferTrigger => 'ReturnToHome', WasReturned => '1' } ); + +#mismatch of item_id and barcode when both given +$t->post_ok( + "//$userid:$password@/api/v1/checkin" => json => { + item_id => $not_issued_item_id, + external_id => $item_2_barcode, + library_id => $branchcode + } +)->status_is(409); + +# reserved item +$t->post_ok( "//$userid:$password@/api/v1/checkin" => json => + { item_id => $item_2_id, library_id => $branchcode } )->status_is(201) + ->json_is( '/returned' => 1 )->json_has( + '/messages' => { WasReturned => '1' }, + '/ResFound' => { "ResFound" => "Reserved" } + ); + +$schema->storage->txn_rollback; -- 2.39.5