From 3057af973a9fdf5135219c1b2dd1af459a27c48d Mon Sep 17 00:00:00 2001 From: danyonsewell Date: Mon, 1 May 2023 04:54:35 +0000 Subject: [PATCH] Bug 24400: Rest API for checkout Feature to provide API endpoints for checking out items. To test: 1) Apply this patch 2) Run: $ ktd --shell $ prove t/db_dependent/api/v1/items.t => SUCCESS: Tests pass! 3. Play with item checkout using a REST tool like Postman => SUCCESS: All works as expected 4. Sign off :-D Sponsored-by: Auckland University of Technology --- Koha/REST/V1/Items.pm | 69 ++++++++++++++++------- api/v1/swagger/paths/items.yaml | 35 ++++++++++++ api/v1/swagger/swagger.yaml | 2 + t/db_dependent/api/v1/items.t | 97 ++++++++++++++++++++++++++++++++- 4 files changed, 182 insertions(+), 21 deletions(-) diff --git a/Koha/REST/V1/Items.pm b/Koha/REST/V1/Items.pm index 938ffcd659..45e0fa2354 100644 --- a/Koha/REST/V1/Items.pm +++ b/Koha/REST/V1/Items.pm @@ -27,19 +27,13 @@ use List::MoreUtils qw( any ); use Try::Tiny qw( catch try ); =head1 NAME - Koha::REST::V1::Items - Koha REST API for handling items (V1) - =head1 API - =head2 Methods - =cut =head3 list - Controller function that handles listing Koha::Item objects - =cut sub list { @@ -59,9 +53,7 @@ sub list { } =head3 list_public - Controller function that handles listing Koha::Item objects available to the opac - =cut sub list_public { @@ -85,9 +77,7 @@ sub list_public { } =head3 get - Controller function that handles retrieving a single Koha::Item - =cut sub get { @@ -110,9 +100,7 @@ sub get { } =head3 delete - Controller function that handles deleting a single Koha::Item - =cut sub delete { @@ -173,12 +161,59 @@ sub delete { $c->unhandled_exception($_); }; } +=head3 checkout -=head3 pickup_locations +Check out an item + +=cut + +sub checkout { + my $c = shift->openapi->valid_input or return; + my $item_id = $c->validation->param('item_id'); + my $item = Koha::Items->find( $item_id ); + unless ( $item ) { + return $c->render( + status => 404, + openapi => { error => "Item not found" } + ); + } + my $patron_id = $c->validation->param('patron_id'); + my $patron = Koha::Patrons->find( $patron_id ); + unless ( $patron ) { + return $c->render( + status => 400, + openapi => { error => "Patron not found" } + ); + } + try { + my $barcode = $item->barcode; + my ( $issuing_impossible, $needs_confirmation ) = C4::Circulation::CanBookBeIssued( $patron, $barcode ); + if ( keys %$issuing_impossible or keys %$needs_confirmation ) { + return $c->render( + status => 403, + openapi => { error => "Checkout not allowed" } + ); + } + my $borrower = $patron->unblessed; + my $checkout = C4::Circulation::AddIssue( + $borrower, + $barcode + ); + + $c->res->headers->location( $c->req->url->to_string ); + return $c->render( + status => 201, + openapi => $checkout->to_api + ); + } + catch { + $c->unhandled_exception($_); + }; +} +=head3 pickup_locations Method that returns the possible pickup_locations for a given item used for building the dropdown selector - =cut sub pickup_locations { @@ -242,9 +277,7 @@ sub pickup_locations { } =head3 bundled_items - Controller function that handles bundled_items Koha::Item objects - =cut sub bundled_items { @@ -274,9 +307,7 @@ sub bundled_items { } =head3 add_to_bundle - Controller function that handles adding items to this bundle - =cut sub add_to_bundle { @@ -371,9 +402,7 @@ sub add_to_bundle { } =head3 remove_from_bundle - Controller function that handles removing items from this bundle - =cut sub remove_from_bundle { diff --git a/api/v1/swagger/paths/items.yaml b/api/v1/swagger/paths/items.yaml index ac8c313a14..c8029f1dc6 100644 --- a/api/v1/swagger/paths/items.yaml +++ b/api/v1/swagger/paths/items.yaml @@ -396,6 +396,41 @@ x-koha-authorization: permissions: reserveforothers: place_holds +"/items/{item_id}/checkout/{patron_id}": + post: + x-mojo-to: Items#checkout + operationId: addCheckout + tags: + - items + - checkouts + summary: Add a checkout + parameters: + - $ref: "../swagger.yaml#/parameters/item_id_pp" + - $ref: "../swagger.yaml#/parameters/patron_id_pp" + consumes: + - application/json + produces: + - application/json + responses: + "201": + description: Added checkout + schema: + $ref: "../swagger.yaml#/definitions/checkout" + "400": + description: Patron not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Cannot add checkout + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Item not found + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + circulate: circulate_remaining_permissions "/public/items": get: x-mojo-to: Items#list_public diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 23c59b96a1..562a8f9d07 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -273,6 +273,8 @@ paths: $ref: ./paths/items.yaml#/~1items~1{item_id}~1bundled_items~1{bundled_item_id} "/items/{item_id}/pickup_locations": $ref: "./paths/items.yaml#/~1items~1{item_id}~1pickup_locations" + "/items/{item_id}/checkout/{patron_id}": + $ref: "./paths/items.yaml#/~1items~1{item_id}~1checkout~1{patron_id}" /jobs: $ref: ./paths/jobs.yaml#/~1jobs "/jobs/{job_id}": diff --git a/t/db_dependent/api/v1/items.t b/t/db_dependent/api/v1/items.t index a4a6300652..3c3c3b56e5 100755 --- a/t/db_dependent/api/v1/items.t +++ b/t/db_dependent/api/v1/items.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 5; +use Test::More tests => 6; use Test::MockModule; use Test::Mojo; use Test::Warn; @@ -30,6 +30,7 @@ use t::lib::Mocks; use Mojo::JSON qw(encode_json); use C4::Auth; +use C4::Circulation; use Koha::Items; use Koha::Database; @@ -572,3 +573,97 @@ subtest 'pickup_locations() tests' => sub { $schema->storage->txn_rollback; }; + +subtest 'checkout() tests' => sub { + + plan tests => 27; + + $schema->storage->txn_begin; + + my $item = $builder->build_sample_item; + my $item2 = $builder->build_sample_item; + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4 } + } + ); + + # Make sure we have at least 10 items + for ( 1..10 ) { + $builder->build_sample_item; + } + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( + { password => $password, skip_validation => 1 } ); + + my $userid = $nonprivilegedpatron->userid; + my $itemid = $item->id; + my $itemid2 = $item2->id; + my $patronid = $patron->borrowernumber; + my $non_existent_code = $item->itemnumber; + + $t->post_ok( "//$userid:$password@/api/v1/items/$itemid/checkout/$patronid" ) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $t->get_ok( "//$userid:$password@/api/v1/items/" . $non_existent_code ) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/items?_per_page=10" ) + ->status_is( 200, 'SWAGGER3.2.2' ); + + my $response_count = scalar @{ $t->tx->res->json }; + + is( $response_count, 10, 'The API returns 10 items' ); + + $t->get_ok( "//$userid:$password@/api/v1/items?external_id=" . $item->barcode ) + ->status_is(200) + ->json_is( '' => [ $item->to_api ], 'SWAGGER3.3.2'); + + $patron->flags(1)->store; + my $deleted_patron_id = $nonprivilegedpatron->id; + $nonprivilegedpatron->delete; + $t->post_ok( "//$userid:$password@/api/v1/items/$itemid/checkout/$deleted_patron_id" ) + ->status_is(400) + ->json_is( '/error' => 'Patron not found' ); + + my $barcode = $item->barcode; + $item->delete; + + $t->post_ok("//$userid:$password@/api/v1/items/$itemid/checkout/$patronid") + ->status_is(404); + + $t->get_ok( "//$userid:$password@/api/v1/items?external_id=" . $item->barcode ) + ->status_is(200) + ->json_is( '' => [] ); + + $t->get_ok( "//$userid:$password@/api/v1/items/" . $non_existent_code ) + ->status_is(404) + ->json_is( + '/error' => 'Item not found' ); + + is( $patron->checkouts->count, 0); + + $t->post_ok( "//$userid:$password@/api/v1/items/$itemid2/checkout/$patronid") + ->status_is(201); + + is( $patron->checkouts->count, 1); + + $schema->storage->txn_rollback; +}; -- 2.30.2