From 6db025bd9dfeecb1f59490578befafbe57a1548e Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 5 May 2023 15:53:50 +0100 Subject: [PATCH] Bug 23336: Add checkout API's This patch adds API's to allow for a checkout flow using the RESTful API. We add an availability endpoint to check an items current availability status. The endpoint can be found at `/checkouts/availability` and is a GET request that requires item_id and patron_id passed as parameters. We return an availability object that includes blockers, confirms, warnings and a confirmation token to be used for checkout. We also add a corresponding checkout method to the `/checkouts` endpoint. The method accepts a POST request with checkout details including item_id , patron_id and the confirmation token in the body. Future work: We should properly migrate CanBookBeIssued into Koha::* and use that here instead of refering to C4::Circulation. Signed-off-by: Silvia Meakins Signed-off-by: Kyle M Hall Signed-off-by: Tomas Cohen Arazi --- C4/Circulation.pm | 4 +- Koha/Item.pm | 1 - Koha/REST/V1/Checkouts.pm | 162 +++++++++++++++++++++++++++- api/v1/swagger/paths/checkouts.yaml | 97 +++++++++++++++++ api/v1/swagger/swagger.yaml | 7 ++ 5 files changed, 268 insertions(+), 3 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 4fdc525330..eb754c8401 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -683,6 +683,7 @@ data is keyed in lower case! Available keys: override_high_holds - Ignore high holds onsite_checkout - Checkout is an onsite checkout that will not leave the library + item - Optionally pass the object for the item we are checking out to save a lookup =back @@ -776,7 +777,8 @@ sub CanBookBeIssued { my $onsite_checkout = $params->{onsite_checkout} || 0; my $override_high_holds = $params->{override_high_holds} || 0; - my $item_object = Koha::Items->find({barcode => $barcode }); + my $item_object = $params->{item} + // Koha::Items->find( { barcode => $barcode } ); # MANDATORY CHECKS - unless item exists, nothing else matters unless ( $item_object ) { diff --git a/Koha/Item.pm b/Koha/Item.pm index 6012846c52..2ad4c3fdcb 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -336,7 +336,6 @@ sub move_to_deleted { return Koha::Database->new->schema->resultset('Deleteditem')->create($item_infos); } - =head3 effective_itemtype Returns the itemtype for the item based on whether item level itemtypes are set or not. diff --git a/Koha/REST/V1/Checkouts.pm b/Koha/REST/V1/Checkouts.pm index 7eea31d03b..72e88ed858 100644 --- a/Koha/REST/V1/Checkouts.pm +++ b/Koha/REST/V1/Checkouts.pm @@ -19,10 +19,13 @@ use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; use Mojo::JSON; +use Mojo::JWT; +use Digest::MD5 qw( md5_base64 ); +use Encode; use C4::Auth qw( haspermission ); use C4::Context; -use C4::Circulation qw( AddRenewal CanBookBeRenewed ); +use C4::Circulation qw( AddIssue AddRenewal CanBookBeRenewed ); use Koha::Checkouts; use Koha::Old::Checkouts; @@ -99,6 +102,163 @@ sub get { }; } +=head3 get_availablity + +Controller function that handles retrieval of Checkout availability + +=cut + +sub get_availability { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->validation->param('patron_id') ); + my $inprocess = 0; # What does this do? + my $ignore_reserves = 0; # Don't ignore reserves + my $item = Koha::Items->find( $c->validation->param('item_id') ); + my $params = { + item => $item + }; + + my ( $impossible, $confirmation, $alerts, $messages ) = + C4::Circulation::CanBookBeIssued( $patron, undef, undef, $inprocess, $ignore_reserves, + $params ); + + my $token; + if (keys %{$confirmation}) { + my $claims = { map { $_ => 1 } keys %{$confirmation} }; + my $secret = + md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) ); + $token = Mojo::JWT->new( claims => $claims, secret => $secret )->encode; + } + + my $response = { + blockers => $impossible, + confirms => $confirmation, + warnings => { %{$alerts}, %{$messages} }, + confirmation_token => $token + }; + + return $c->render( status => 200, openapi => $response ); +} + +=head3 add + +Add a new checkout + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + my $body = $c->validation->param('body'); + my $item_id = $body->{item_id}; + my $patron_id = $body->{patron_id}; + my $onsite = $body->{onsite_checkout}; + + return try { + my $item = Koha::Items->find($item_id); + unless ($item) { + return $c->render( + status => 409, + openapi => { + error => 'Item not found', + error_code => 'ITEM_NOT_FOUND', + } + ); + } + + my $patron = Koha::Patrons->find($patron_id); + unless ($patron) { + return $c->render( + status => 409, + openapi => { + error => 'Patron not found', + error_code => 'PATRON_NOT_FOUND', + } + ); + } + + my $inprocess = 0; # What does this do? + my $ignore_reserves = 0; # Don't ignore reserves + my $params = { item => $item }; + + # Call 'CanBookBeIssued' + my ( $impossible, $confirmation, $alerts, $messages ) = + C4::Circulation::CanBookBeIssued( + $patron, + undef, + undef, + $inprocess, + $ignore_reserves, + $params + ); + + # * Fail for blockers - render 403 + if ( keys %{$impossible} ) { + my @errors = keys %{$impossible}; + return $c->render( + status => 403, + openapi => { error => "Checkout not authorized (@errors)" } + ); + } + + # * If confirmation required, check variable set above + # and render 412 if variable is false + if ( keys %{$confirmation} ) { + my $confirmed = 0; + + # Check for existance of confirmation token + # and if exists check validity + if ( my $token = $c->validation->param('confirmation') ) { + my $secret = + md5_base64( + Encode::encode( 'UTF-8', C4::Context->config('pass') ) ); + my $claims = try { + Mojo::JWT->new( secret => $secret )->decode($token); + } + catch { + return $c->render( + status => 403, + openapi => { error => "Confirmation required" } + ); + }; + + # check claims match + my $token_claims = join( / /, sort keys %{$claims} ); + my $confirm_keys = join( / /, sort keys %{$confirmation} ); + $confirmed = 1 if ( $token_claims eq $confirm_keys ); + } + + unless ($confirmed) { + return $c->render( + status => 412, + openapi => { error => "Confirmation error" } + ); + } + } + + # Call 'AddIssue' + my $checkout = AddIssue( $patron->unblessed, $item->barcode ); + if ($checkout) { + $c->res->headers->location( + $c->req->url->to_string . '/' . $checkout->id ); + return $c->render( + status => 201, + openapi => $checkout->to_api + ); + } + else { + return $c->render( + status => 500, + openapi => { error => 'Unknown error during checkout' } + ); + } + } + catch { + $c->unhandled_exception($_); + }; +} + =head3 get_renewals List Koha::Checkout::Renewals diff --git a/api/v1/swagger/paths/checkouts.yaml b/api/v1/swagger/paths/checkouts.yaml index d20aaa1f7d..afe6e5660c 100644 --- a/api/v1/swagger/paths/checkouts.yaml +++ b/api/v1/swagger/paths/checkouts.yaml @@ -61,6 +61,68 @@ x-koha-authorization: permissions: circulate: circulate_remaining_permissions + post: + x-mojo-to: Checkouts#add + operationId: addCheckout + tags: + - checkouts + - patrons + summary: Add a new checkout + parameters: + - name: body + in: body + description: A JSON object containing information about the new checkout + required: true + schema: + $ref: "../swagger.yaml#/definitions/checkout" + - name: confirmation + in: query + description: A JWT confirmation token + required: false + type: string + consumes: + - application/json + produces: + - application/json + responses: + "201": + description: Created checkout + schema: + $ref: "../swagger.yaml#/definitions/checkout" + "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 checkout + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict in creating checkout + 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 "/checkouts/{checkout_id}": get: x-mojo-to: Checkouts#get @@ -273,3 +335,38 @@ x-koha-authorization: permissions: circulate: circulate_remaining_permissions +"/checkouts/availability": + get: + x-mojo-to: Checkouts#get_availability + operationId: availabilityCheckouts + tags: + - checkouts + summary: Get checkout availability + parameters: + - $ref: "../swagger.yaml#/parameters/patron_id_qp" + - $ref: "../swagger.yaml#/parameters/item_id_qp" + produces: + - application/json + responses: + "200": + description: Availability + schema: + type: "object" + "403": + description: Access forbidden + 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 9e60aa77a2..0eff0139c7 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -197,6 +197,8 @@ paths: $ref: "./paths/checkouts.yaml#/~1checkouts~1{checkout_id}~1renewals" "/checkouts/{checkout_id}/renewal": $ref: "./paths/checkouts.yaml#/~1checkouts~1{checkout_id}~1renewal" + "/checkouts/availability": + $ref: "./paths/checkouts.yaml#/~1checkouts~1availability" /circulation-rules/kinds: $ref: ./paths/circulation-rules.yaml#/~1circulation-rules~1kinds /cities: @@ -521,6 +523,11 @@ parameters: name: item_id required: true type: integer + item_id_qp: + description: Internal item identifier + in: query + name: item_id + type: integer job_id_pp: description: Job internal identifier in: path