From c223298d9ef860d8847b378e3e4c56a42b467252 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Mon, 18 Jul 2022 13:32:23 +0100 Subject: [PATCH] Bug 30979: Public 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 `/public/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 endpoint at `/public/checkouts` that accepts a POST request with checkout details including item_id and a confirmation token in the body. Future work: We should properly migrate CanBookBeIssued into Koha::* and use that here instead of refering to C4::Circulation. --- C4/Circulation.pm | 3 +- Koha/Item.pm | 1 - Koha/REST/V1/Checkouts.pm | 152 +++++++++++++++++++++++++++- api/v1/swagger/paths/checkouts.yaml | 128 +++++++++++++++++++++++ api/v1/swagger/swagger.yaml | 9 ++ 5 files changed, 290 insertions(+), 3 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 336ce9490a1..eaaa22dd8fe 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -776,7 +776,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 dae9df8f295..b1e119f6c28 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -338,7 +338,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 e4a52b4cadb..e0745668b3e 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 ); +use C4::Circulation qw( CanBookBeIssued AddIssue AddRenewal ); use Koha::Checkouts; use Koha::Old::Checkouts; @@ -99,6 +102,153 @@ 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 ) = + 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 $params = $c->validation->param('body'); + my $item_id = $params->{item_id}; + my $patron_id = $params->{patron_id}; + my $onsite = $params->{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 ) = + 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 - 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); + + # Check return of AddIssue + # Get checkout from AddIssue - render 201 + # Get error from AddIssue - render 400? + + $c->res->headers->location( $c->req->url->to_string . '/' . $checkout->id ); + return $c->render( + status => 201, + openapi => $checkout->to_api + ); + } + 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 82c67bf3247..6f1d0cb760a 100644 --- a/api/v1/swagger/paths/checkouts.yaml +++ b/api/v1/swagger/paths/checkouts.yaml @@ -61,6 +61,53 @@ x-koha-authorization: permissions: circulate: circulate_remaining_permissions + post: + x-mojo-to: Checkouts#add + operationId: addCheckout + tags: + - checkouts + summary: Add a new checkout + parameters: + - name: body + in: body + description: A JSON object containing informations about the new checkout + required: true + schema: + $ref: "../swagger.yaml#/definitions/checkout" + produces: + - application/json + responses: + "201": + description: Created new checkout + schema: + $ref: "../swagger.yaml#/definitions/checkout" + "403": + description: Cannot create checkout + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Checkout not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict in creating resource + 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" "/checkouts/{checkout_id}": get: x-mojo-to: Checkouts#get @@ -275,3 +322,84 @@ x-koha-authorization: permissions: circulate: circulate_remaining_permissions +"/public/checkouts": + post: + x-mojo-to: Checkouts#add + operationId: addCheckoutPublic + tags: + - checkouts + summary: Add a new checkout + parameters: + - name: body + in: body + description: A JSON object containing informations 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 + produces: + - application/json + responses: + "201": + description: Created new checkout + schema: + $ref: "../swagger.yaml#/definitions/checkout" + "403": + description: Cannot create checkout + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Checkout not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "412": + description: Confirmaion required + 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" +"/public/checkouts/availability": + get: + x-mojo-to: Checkouts#get_availability + operationId: availabilityCheckoutsPublic + 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" diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 4869bebe472..a4fd1e0fc35 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -273,6 +273,10 @@ paths: $ref: "./paths/patrons_password.yaml#/~1patrons~1{patron_id}~1password~1expiration_date" "/public/biblios/{biblio_id}": $ref: "./paths/biblios.yaml#/~1public~1biblios~1{biblio_id}" + "/public/checkouts": + $ref: ./paths/checkouts.yaml#/~1public~1checkouts + "/public/checkouts/availability": + $ref: ./paths/checkouts.yaml#/~1public~1checkouts~1availability "/public/items": $ref: "./paths/items.yaml#/~1public~1items" "/public/biblios/{biblio_id}/items": @@ -440,6 +444,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 -- 2.39.0