From a44ffa0926d648b4b1509c56e3227784f5404942 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Tue, 29 Oct 2019 12:29:37 -0300 Subject: [PATCH] Bug 14697: Add routes to handle return claims This patch adds the /return_claims route to add new return claims, and then routes to updates notes and the resolution code. To test: 1. Apply this patches 2. Run: $ kshell k$ prove t/db_dependent/api/v1/return_claims.t => SUCCESS: Tests pass! 3. Sign off :-D Signed-off-by: Tomas Cohen Arazi Signed-off-by: Andrew Fuerste-Henry Signed-off-by: Lisette Scheer Signed-off-by: Martin Renvoize --- Koha/REST/V1/ReturnClaims.pm | 257 ++++++++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/return_claim.json | 93 +++++ api/v1/swagger/paths.json | 12 + api/v1/swagger/paths/return_claims.json | 355 +++++++++++++++++++ t/db_dependent/api/v1/return_claims.t | 162 +++++++++ 6 files changed, 882 insertions(+) create mode 100644 Koha/REST/V1/ReturnClaims.pm create mode 100644 api/v1/swagger/definitions/return_claim.json create mode 100644 api/v1/swagger/paths/return_claims.json create mode 100644 t/db_dependent/api/v1/return_claims.t diff --git a/Koha/REST/V1/ReturnClaims.pm b/Koha/REST/V1/ReturnClaims.pm new file mode 100644 index 0000000000..d221518650 --- /dev/null +++ b/Koha/REST/V1/ReturnClaims.pm @@ -0,0 +1,257 @@ +package Koha::REST::V1::ReturnClaims; + +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Try::Tiny; + +use Koha::Checkouts::ReturnClaims; +use Koha::Checkouts; +use Koha::DateUtils qw( dt_from_string output_pref ); + +=head1 NAME + +Koha::REST::V1::ReturnClaims + +=head2 Operations + +=head3 claim_returned + +Claim that a checked out item was returned. + +=cut + +sub claim_returned { + my $c = shift->openapi->valid_input or return; + my $body = $c->validation->param('body'); + + return try { + my $itemnumber = $body->{item_id}; + my $charge_lost_fee = $body->{charge_lost_fee} ? 1 : 0; + my $created_by = $body->{created_by}; + my $notes = $body->{notes}; + + my $user = $c->stash('koha.user'); + $created_by //= $user->borrowernumber; + + my $checkout = Koha::Checkouts->find( { itemnumber => $itemnumber } ); + + return $c->render( + openapi => { error => "Not found - Checkout not found" }, + status => 404 + ) unless $checkout; + + my $claim = Koha::Checkouts::ReturnClaims->find( + { + issue_id => $checkout->id + } + ); + return $c->render( + openapi => { error => "Bad request - claim exists" }, + status => 400 + ) if $claim; + + $claim = $checkout->claim_returned( + { + charge_lost_fee => $charge_lost_fee, + created_by => $created_by, + notes => $notes, + } + ); + + $c->res->headers->location($c->req->url->to_string . '/' . $claim->id ); + return $c->render( + status => 201, + openapi => $claim->to_api + ); + } + catch { + if ( $_->isa('Koha::Exceptions::Checkouts::ReturnClaims') ) { + return $c->render( + status => 500, + openapi => { error => "$_" } + ); + } + else { + return $c->render( + status => 500, + openapi => { error => "Something went wrong, check the logs." } + ); + } + }; +} + +=head3 update_notes + +Update the notes of an existing claim + +=cut + +sub update_notes { + my $c = shift->openapi->valid_input or return; + my $input = $c->validation->output; + my $body = $c->validation->param('body'); + + return try { + my $id = $input->{claim_id}; + my $updated_by = $body->{updated_by}; + my $notes = $body->{notes}; + + $updated_by ||= + C4::Context->userenv ? C4::Context->userenv->{number} : undef; + + my $claim = Koha::Checkouts::ReturnClaims->find($id); + + return $c->render( + openapi => { error => "Not found - Claim not found" }, + status => 404 + ) unless $claim; + + $claim->set( + { + notes => $notes, + updated_by => $updated_by, + updated_on => dt_from_string(), + } + ); + $claim->store(); + + my $data = $claim->unblessed; + + my $c_dt = dt_from_string( $data->{created_on} ); + my $u_dt = dt_from_string( $data->{updated_on} ); + + $data->{created_on_formatted} = output_pref( { dt => $c_dt } ); + $data->{updated_on_formatted} = output_pref( { dt => $u_dt } ); + + $data->{created_on} = $c_dt->iso8601; + $data->{updated_on} = $u_dt->iso8601; + + return $c->render( openapi => $data, status => 200 ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( + status => 500, + openapi => { error => $_->{msg} } + ); + } + else { + return $c->render( + status => 500, + openapi => { error => "Something went wrong, check the logs." } + ); + } + }; +} + +=head3 resolve_claim + +Marks a claim as resolved + +=cut + +sub resolve_claim { + my $c = shift->openapi->valid_input or return; + my $input = $c->validation->output; + my $body = $c->validation->param('body'); + + return try { + my $id = $input->{claim_id}; + my $resolved_by = $body->{updated_by}; + my $resolution = $body->{resolution}; + + $resolved_by ||= + C4::Context->userenv ? C4::Context->userenv->{number} : undef; + + my $claim = Koha::Checkouts::ReturnClaims->find($id); + + return $c->render( + openapi => { error => "Not found - Claim not found" }, + status => 404 + ) unless $claim; + + $claim->set( + { + resolution => $resolution, + resolved_by => $resolved_by, + resolved_on => dt_from_string(), + } + ); + $claim->store(); + + return $c->render( openapi => $claim, status => 200 ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( + status => 500, + openapi => { error => $_->{msg} } + ); + } + else { + return $c->render( + status => 500, + openapi => { error => "Something went wrong, check the logs." } + ); + } + }; +} + +=head3 delete_claim + +Deletes the claim from the database + +=cut + +sub delete_claim { + my $c = shift->openapi->valid_input or return; + my $input = $c->validation->output; + + return try { + my $id = $input->{claim_id}; + + my $claim = Koha::Checkouts::ReturnClaims->find($id); + + return $c->render( + openapi => { error => "Not found - Claim not found" }, + status => 404 + ) unless $claim; + + $claim->delete(); + + return $c->render( openapi => $claim, status => 200 ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( + status => 500, + openapi => { error => $_->{msg} } + ); + } + else { + return $c->render( + status => 500, + openapi => { error => "Something went wrong, check the logs." } + ); + } + }; +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 4ac42cf925..90374145a7 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -43,5 +43,8 @@ }, "fund": { "$ref": "definitions/fund.json" + }, + "return_claim": { + "$ref": "definitions/return_claim.json" } } diff --git a/api/v1/swagger/definitions/return_claim.json b/api/v1/swagger/definitions/return_claim.json new file mode 100644 index 0000000000..62f2baec6e --- /dev/null +++ b/api/v1/swagger/definitions/return_claim.json @@ -0,0 +1,93 @@ +{ + "type": "object", + "properties": { + "claim_id": { + "type": [ + "integer" + ], + "description": "internally assigned return claim identifier" + }, + "item_id": { + "type": [ + "integer" + ], + "description": "internal identifier of the claimed item" + }, + "issue_id": { + "type": [ + "integer", + "null" + ], + "description": "internal identifier of the claimed checkout if still checked out" + }, + "old_issue_id": { + "type": [ + "integer", + "null" + ], + "description": "internal identifier of the claimed checkout if not longer checked out" + }, + "patron_id": { + "$ref": "../x-primitives.json#/patron_id" + }, + "notes": { + "type": [ + "string", + "null" + ], + "description": "notes about this claim" + }, + "created_on": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "date of claim creation" + }, + "created_by": { + "type": [ + "integer", + "null" + ], + "description": "patron id of librarian who made the claim" + }, + "updated_on": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "date the claim was last updated" + }, + "updated_by": { + "type": [ + "integer", + "null" + ], + "description": "patron id of librarian who last updated the claim" + }, + "resolution": { + "type": [ + "string", + "null" + ], + "description": "code of resolution type for this claim" + }, + "resolved_on": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "date the claim was resolved" + }, + "resolved_by": { + "type": [ + "integer", + "null" + ], + "description": "patron id of librarian who resolved this claim" + } + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index e786775e7e..95278ec23a 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -88,5 +88,17 @@ }, "/public/patrons/{patron_id}/guarantors/can_see_checkouts": { "$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1guarantors~1can_see_checkouts" + }, + "/return_claims": { + "$ref": "paths/return_claims.json#/~1return_claims" + }, + "/return_claims/{claim_id}/notes": { + "$ref": "paths/return_claims.json#/~1return_claims~1{claim_id}~1notes" + }, + "/return_claims/{claim_id}/resolve": { + "$ref": "paths/return_claims.json#/~1return_claims~1{claim_id}~1resolve" + }, + "/return_claims/{claim_id}": { + "$ref": "paths/return_claims.json#/~1return_claims~1{claim_id}" } } diff --git a/api/v1/swagger/paths/return_claims.json b/api/v1/swagger/paths/return_claims.json new file mode 100644 index 0000000000..068176f1bb --- /dev/null +++ b/api/v1/swagger/paths/return_claims.json @@ -0,0 +1,355 @@ +{ + "/return_claims": { + "post": { + "x-mojo-to": "ReturnClaims#claim_returned", + "operationId": "claimReturned", + "tags": [ + "claims", + "returned", + "return", + "claim" + ], + "parameters": [ + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { + "type": "object", + "properties": { + "item_id" : { + "description": "Internal item id to claim as returned", + "type": "integer" + }, + "notes": { + "description": "Notes about this return claim", + "type": "string" + }, + "created_by": { + "description": "User id for the librarian submitting this claim", + "type": "string" + }, + "charge_lost_fee": { + "description": "Charge a lost fee if true and Koha is set to allow a choice. Ignored otherwise.", + "type": "boolean" + } + } + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "Created claim", + "schema": { + "$ref": "../definitions.json#/return_claim" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Checkout not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "circulate": "circulate_remaining_permissions" + } + } + } + }, + "/return_claims/{claim_id}/notes": { + "put": { + "x-mojo-to": "ReturnClaims#update_notes", + "operationId": "updateClaimNotes", + "tags": [ + "claims", + "returned", + "return", + "claim", + "notes" + ], + "parameters": [ + { + "name": "claim_id", + "in": "path", + "required": true, + "description": "Unique identifier for the claim whose notes are to be updated", + "type": "integer" + }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { + "type": "object", + "properties": { + "notes": { + "description": "Notes about this return claim", + "type": "string" + }, + "updated_by": { + "description": "User id for the librarian updating the claim notes", + "type": "string" + } + } + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Claim notes updated", + "schema": { + "$ref": "../definitions.json#/return_claim" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Claim not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "circulate": "circulate_remaining_permissions" + } + } + } + }, + "/return_claims/{claim_id}": { + "delete": { + "x-mojo-to": "ReturnClaims#delete_claim", + "operationId": "deletedClaim", + "tags": [ + "claims", + "returned", + "return", + "claim", + "delete" + ], + "parameters": [ + { + "name": "claim_id", + "in": "path", + "required": true, + "description": "Unique identifier for the claim to be deleted", + "type": "integer" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Claim deleted", + "schema": { + "$ref": "../definitions.json#/return_claim" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Claim not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "circulate": "circulate_remaining_permissions" + } + } + } + }, + "/return_claims/{claim_id}/resolve": { + "put": { + "x-mojo-to": "ReturnClaims#resolve_claim", + "operationId": "updateClaimResolve", + "tags": [ + "claims", + "returned", + "return", + "claim", + "notes" + ], + "parameters": [ + { + "name": "claim_id", + "in": "path", + "required": true, + "description": "Unique identifier for the claim to be resolved", + "type": "integer" + }, + { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { + "type": "object", + "properties": { + "resolution": { + "description": "The RETURN_CLAIM_RESOLUTION code to be used to resolve the calim", + "type": "string" + }, + "resolved_by": { + "description": "User id for the librarian resolving the claim", + "type": "string" + } + } + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Claim resolved", + "schema": { + "$ref": "../definitions.json#/return_claim" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Claim not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "circulate": "circulate_remaining_permissions" + } + } + } + } +} diff --git a/t/db_dependent/api/v1/return_claims.t b/t/db_dependent/api/v1/return_claims.t new file mode 100644 index 0000000000..073bbd5eba --- /dev/null +++ b/t/db_dependent/api/v1/return_claims.t @@ -0,0 +1,162 @@ +#!/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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Test::More tests => 25; +use Test::MockModule; +use Test::Mojo; +use t::lib::Mocks; +use t::lib::TestBuilder; + +use DateTime; + +use C4::Context; +use C4::Circulation; + +use Koha::Checkouts::ReturnClaims; +use Koha::Database; +use Koha::DateUtils; + +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 $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 1 } + } +); +my $password = 'thePassword123'; +$librarian->set_password( { password => $password, skip_validation => 1 } ); +my $userid = $librarian->userid; + +my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } +); +my $unauth_password = 'thePassword000'; +$patron->set_password( + { password => $unauth_password, skip_validattion => 1 } ); +my $unauth_userid = $patron->userid; +my $patron_id = $patron->borrowernumber; + +my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; +my $module = new Test::MockModule('C4::Context'); +$module->mock( 'userenv', sub { { branch => $branchcode } } ); + +my $item1 = $builder->build_sample_item; +my $itemnumber1 = $item1->itemnumber; + +my $date_due = DateTime->now->add( weeks => 2 ); +my $issue1 = + C4::Circulation::AddIssue( $patron->unblessed, $item1->barcode, $date_due ); + +t::lib::Mocks::mock_preference( 'ClaimReturnedChargeFee', 'ask' ); +t::lib::Mocks::mock_preference( 'ClaimReturnedLostValue', '99' ); + +# Test creating a return claim +## Invalid id +$t->post_ok( + "//$userid:$password@/api/v1/return_claims" => json => { + item_id => 1, + charge_lost_fee => Mojo::JSON->false, + created_by => $librarian->id, + notes => "This is a test note." + } +)->status_is(404); + +## Valid id +$t->post_ok( + "//$userid:$password@/api/v1/return_claims" => json => { + item_id => $itemnumber1, + charge_lost_fee => Mojo::JSON->false, + created_by => $librarian->id, + notes => "This is a test note." + } +)->status_is(201); +my $claim_id = $t->tx->res->json->{claim_id}; + +## Duplicate id +$t->post_ok( + "//$userid:$password@/api/v1/return_claims" => json => { + item_id => $itemnumber1, + charge_lost_fee => Mojo::JSON->false, + created_by => $librarian->id, + notes => "This is a test note." + } +)->status_is(400); + +# Test editing a claim note +## Valid claim id +$t->put_ok( + "//$userid:$password@/api/v1/return_claims/$claim_id/notes" => json => { + notes => "This is a different test note.", + updated_by => $librarian->id, + } +)->status_is(200); +my $claim = Koha::Checkouts::ReturnClaims->find($claim_id); +is( $claim->notes, "This is a different test note." ); +is( $claim->updated_by, $librarian->id ); +ok( $claim->updated_on ); + +## Bad claim id +$t->put_ok( + "//$userid:$password@/api/v1/return_claims/99999999999/notes" => json => { + notes => "This is a different test note.", + updated_by => $librarian->id, + } +)->status_is(404); + +# Resolve a claim +## Valid claim id +$t->put_ok( + "//$userid:$password@/api/v1/return_claims/$claim_id/resolve" => json => { + resolved_by => $librarian->id, + resolution => "FOUNDINLIB", + } +)->status_is(200); +$claim = Koha::Checkouts::ReturnClaims->find($claim_id); +is( $claim->resolution, "FOUNDINLIB" ); +is( $claim->updated_by, $librarian->id ); +ok( $claim->resolved_on ); + +## Invalid claim id +$t->put_ok( + "//$userid:$password@/api/v1/return_claims/999999999999/resolve" => json => { + resolved_by => $librarian->id, + resolution => "FOUNDINLIB", + } +)->status_is(404); + +# Test deleting a return claim +$t = $t->delete_ok("//$userid:$password@/api/v1/return_claims/$claim_id") + ->status_is(200); +$claim = Koha::Checkouts::ReturnClaims->find($claim_id); +isnt( $claim, "Return claim was deleted" ); + +$t->delete_ok("//$userid:$password@/api/v1/return_claims/$claim_id") + ->status_is(404); -- 2.39.5