From 342a6dfdf22e9959b84272b8e9d7086c40f26e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johanna=20R=C3=A4is=C3=A4?= Date: Wed, 4 Jun 2025 09:09:00 +0300 Subject: [PATCH] Bug 35722: Create item transfer REST api This patch creates a REST API for item transfers, allowing users to transfer items between libraries via the API. Test Plan: 1. Apply the patch. 2. perl build-resources.PL 3. prove t/db_dependent/api/v1/items/item_transfers.t 4. Verify that the API endpoints for item transfers work as expected. Sponsored-by: Koha-Suomi Oy --- Koha/Item/Transfer.pm | 23 ++ Koha/REST/V1/Items/Transfers.pm | 185 +++++++++++ api/v1/swagger/definitions/item_transfer.yaml | 70 +++++ api/v1/swagger/paths/items.yaml | 259 ++++++++++++++++ api/v1/swagger/swagger.yaml | 6 + t/db_dependent/api/v1/items/item_transfers.t | 288 ++++++++++++++++++ 6 files changed, 831 insertions(+) create mode 100644 Koha/REST/V1/Items/Transfers.pm create mode 100644 api/v1/swagger/definitions/item_transfer.yaml create mode 100755 t/db_dependent/api/v1/items/item_transfers.t diff --git a/Koha/Item/Transfer.pm b/Koha/Item/Transfer.pm index 1cacb68475..61cef69901 100644 --- a/Koha/Item/Transfer.pm +++ b/Koha/Item/Transfer.pm @@ -216,6 +216,29 @@ sub strings_map { }; } +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::Item::Transfer object +on the API. + +=cut + +sub to_api_mapping { + return { + branchtransfer_id => 'library_transfer_id', + itemnumber => 'item_id', + frombranch => 'from_library_id', + tobranch => 'to_library_id', + daterequested => 'date_requested', + datesent => 'date_sent', + datecancelled => 'date_cancelled', + datearrived => 'date_arrived', + reason => 'reason', + cancellation_reason => 'cancellation_reason', + comments => 'comments', + }; +} + =head3 type =cut diff --git a/Koha/REST/V1/Items/Transfers.pm b/Koha/REST/V1/Items/Transfers.pm new file mode 100644 index 0000000000..77bebf0854 --- /dev/null +++ b/Koha/REST/V1/Items/Transfers.pm @@ -0,0 +1,185 @@ +package Koha::REST::V1::Items::Transfers; + +# 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 Mojo::Base 'Mojolicious::Controller'; +use Koha::Item::Transfers; +use Koha::Libraries; +use Koha::Items; +use Koha::Patrons; +use Koha::Recalls; +use C4::Context; + +use Koha::Exceptions::Item::Transfer; + +use Scalar::Util qw( blessed ); + +use Try::Tiny qw( catch try ); + +=head1 NAME + +Koha::REST::V1::Items::Transfers - Koha REST API for handling item transfers (V1) + +=head1 API + +=head2 Methods + +=cut + +=head3 list + +Controller function that handles listing Koha::Item::Transfers objects + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $transfers = $c->objects->search( Koha::Item::Transfers->new ); + return $c->render( status => 200, openapi => $transfers ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +Controller function that handles retrieving a single Koha::Item::Transfer + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + my $item_id = $c->param('item_id'); + my $item = Koha::Items->find($item_id); + + return $c->render_resource_not_found("Item") + unless $item; + try { + my $transfer = $item->transfer; + + return $c->render( + status => 404, + openapi => { error => "Item is not in transfer" }, + ) unless $transfer; + + return $c->render( + status => 200, + openapi => $c->objects->to_api($transfer), + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 add + +Controller function that handles adding a new transfer + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + my $item_id = $c->param('item_id'); + my $item = Koha::Items->find($item_id); + my $ignore_limits = $c->param('ignore_limits'); + my $enqueue = $c->param('enqueue'); + my $replace = $c->param('replace'); + + return $c->render_resource_not_found("Item") + unless $item; + + my $body = $c->req->json; + + my $to_library = Koha::Libraries->find( $body->{to_library_id} ); + + return $c->render_resource_not_found("Library") + unless $to_library; + return try { + my $params = { + to => $to_library, + reason => $body->{reason}, + comment => $body->{comments}, + }; + $params->{ignore_limits} = $ignore_limits if defined $ignore_limits; + $params->{enqueue} = $enqueue if defined $enqueue; + $params->{replace} = $replace if defined $replace; + + my $transfer = $item->request_transfer($params); + $transfer->transit; + return $c->render( + status => 201, + openapi => $c->objects->to_api($transfer), + ); + } catch { + if ( blessed $_ && $_->isa('Koha::Exceptions::Item::Transfer::InQueue') ) { + return $c->render( + status => 409, + openapi => { error => "Item is already in transfer queue" }, + ); + } + $c->unhandled_exception($_); + }; +} + +=head3 cancel + +Controller function that handles deleting a transfer + +=cut + +sub cancel { + my $c = shift->openapi->valid_input or return; + + my $item_id = $c->param('item_id'); + my $item = Koha::Items->find($item_id); + + return $c->render_resource_not_found("Item") + unless $item; + + my $reason = $c->param('reason') || "Manual"; + + return try { + my $transfer = $item->transfer; + return $c->render( + status => 404, + openapi => { error => "Item is not in transfer" }, + ) unless $transfer; + + $transfer->cancel( + { + reason => $reason, + force => 1, + } + ); + if ( C4::Context->preference('UseRecalls') ) { + my $recall_transfer_deleted = Koha::Recalls->find( { item_id => $item_id, status => 'in_transit' } ); + if ( defined $recall_transfer_deleted ) { + $recall_transfer_deleted->revert_transfer; + } + } + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/api/v1/swagger/definitions/item_transfer.yaml b/api/v1/swagger/definitions/item_transfer.yaml new file mode 100644 index 0000000000..2b8af5d35d --- /dev/null +++ b/api/v1/swagger/definitions/item_transfer.yaml @@ -0,0 +1,70 @@ +--- +type: object +properties: + library_transfer_id: + type: integer + description: Internal transfer limit identifier + item_id: + type: integer + description: Internal item identifier + date_requested: + type: + - string + - 'null' + format: date-time + description: Date when the transfer was requested + date_sent: + type: + - string + - 'null' + format: date-time + description: Date when the transfer was sent + to_library_id: + type: string + description: Internal library id for which library the item is going to + date_arrived: + type: + - string + - 'null' + format: date-time + description: Date when the transfer arrived at the destination library + date_cancelled: + type: + - string + - 'null' + format: date-time + description: Date when the transfer was cancelled + from_library_id: + type: string + description: Internal library id for which library the item is coming from + comments: + type: + - string + - 'null' + description: Comments associated with the transfer + reason: + type: + - string + description: Reason for the transfer + enum: + - 'Manual' + - 'StockrotationAdvance' + - 'StockrotationRepatriation' + - 'ReturnToHome' + - 'ReturnToHolding' + - 'RotatingCollection' + - 'Reserve' + - 'LostReserve' + - 'CancelReserve' + - 'TransferCancellation' + - 'Recall' + - 'RecallCancellation' + cancellation_reason: + type: + - string + - 'null' + description: Reason for cancelling the transfer +additionalProperties: false +required: + - to_library_id + - reason diff --git a/api/v1/swagger/paths/items.yaml b/api/v1/swagger/paths/items.yaml index 5acc18ad28..bb16260fd4 100644 --- a/api/v1/swagger/paths/items.yaml +++ b/api/v1/swagger/paths/items.yaml @@ -473,6 +473,265 @@ x-koha-authorization: permissions: reserveforothers: place_holds +/items/transfers: + get: + x-mojo-to: Items::Transfers#list + operationId: listItemsTransfers + description: This resource returns a list of existing transfers. + summary: List transfers + tags: + - transfers + parameters: + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + produces: + - application/json + responses: + "200": + description: A list of transfers + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/item_transfer" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Resource not found + 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: + parameters: manage_transfers +/items/{item_id}/transfers: + get: + x-mojo-to: Items::Transfers#get + operationId: getItemTransfer + description: This resource returns the active transfer for an item. + summary: Get active item transfer + tags: + - transfers + parameters: + - $ref: "../swagger.yaml#/parameters/item_id_pp" + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + produces: + - application/json + responses: + "200": + description: An active transfer for the item + schema: + type: object + items: + $ref: "../swagger.yaml#/definitions/item_transfer" + "400": + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Resource not found + 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: + parameters: manage_transfers + post: + x-mojo-to: Items::Transfers#add + operationId: addItemTransfer + description: This resource creates a new transfer for the specified item. + tags: + - transfers + summary: Add an item transfer + parameters: + - $ref: "../swagger.yaml#/parameters/item_id_pp" + - name: ignore_limits + in: query + description: | + If set to true, the transfer will be created even if it exceeds the transfer limits. + required: false + type: boolean + - name: enqueue + in: query + description: | + Used to queue up the transfer when the existing transfer is found to be in transit. + required: false + type: boolean + - name: replace + in: query + description: | + Used to replace the existing transfer request with your own. Reason for the cancellation of the existing transfer must be provided here. + required: false + type: string + enum: + - Manual + - StockrotationAdvance + - StockrotationRepatriation + - ReturnToHome + - ReturnToHolding + - RotatingCollection + - Reserve + - LostReserve + - CancelReserve + - ItemLost + - WrongTransfer + - RecallCancellation + - name: body + in: body + description: A JSON object containing information about a new transfer + required: true + schema: + $ref: "../swagger.yaml#/definitions/item_transfer" + produces: + - application/json + responses: + "201": + description: Transfer added + schema: + $ref: "../swagger.yaml#/definitions/item_transfer" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Resource not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict in creating resource + 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: + parameters: manage_transfers + delete: + x-mojo-to: Items::Transfers#cancel + operationId: cancelItemTransfer + description: This resource cancels the active transfer for the specified item. + tags: + - transfers + summary: Cancel an item transfer + parameters: + - $ref: "../swagger.yaml#/parameters/item_id_pp" + - name: reason + in: query + description: | + Reason for cancelling the transfer. If not provided, the transfer will be cancelled with the default reason. + required: false + type: string + default: "Manual" + produces: + - application/json + responses: + "204": + description: Transfer cancelled + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Resource not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "409": + description: Conflict in creating resource + 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: + parameters: manage_transfers "/public/items": get: x-mojo-to: Items#list_public diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index fb36579cea..604235dad1 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -126,6 +126,8 @@ definitions: $ref: ./definitions/invoice.yaml item: $ref: ./definitions/item.yaml + item_transfer: + $ref: ./definitions/item_transfer.yaml item_group: $ref: ./definitions/item_group.yaml item_type: @@ -445,6 +447,10 @@ 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/transfers": + $ref: "./paths/items.yaml#/~1items~1transfers" + "/items/{item_id}/transfers": + $ref: "./paths/items.yaml#/~1items~1{item_id}~1transfers" /jobs: $ref: ./paths/jobs.yaml#/~1jobs "/jobs/{job_id}": diff --git a/t/db_dependent/api/v1/items/item_transfers.t b/t/db_dependent/api/v1/items/item_transfers.t new file mode 100755 index 0000000000..94f8af2280 --- /dev/null +++ b/t/db_dependent/api/v1/items/item_transfers.t @@ -0,0 +1,288 @@ +#!/usr/bin/env perl + +use Modern::Perl; + +use Test::NoWarnings; +use Test::More tests => 5; +use Test::MockModule; +use Test::Mojo; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +my $t = Test::Mojo->new('Koha::REST::V1'); + +subtest 'list()' => sub { + plan tests => 9; + + $schema->storage->txn_begin; + + my $library1 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $item = $builder->build_sample_item( + { + homebranch => $library1->branchcode, + holdingbranch => $library1->branchcode, + } + ); + + my $transfer = $item->request_transfer( { to => $library2, reason => 'Manual' } ); + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 1 } + } + ); + + 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; + + $t->get_ok("//$userid:$password@/api/v1/items/transfers")->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/transfers")->status_is( 200, 'Transfer list retrieved successfully' ); + + my $res = $t->tx->res->json; + my $json = pop @$res; + is( $json->{item_id}, $item->itemnumber, 'Item number matches' ); + is( $json->{from_library_id}, $library1->branchcode, 'From library matches' ); + is( $json->{to_library_id}, $library2->branchcode, 'To library matches' ); + is( $json->{reason}, 'Manual', 'Reason matches' ); + + $schema->storage->txn_rollback; +}; + +subtest 'get()' => sub { + plan tests => 9; + + $schema->storage->txn_begin; + + my $library1 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $item = $builder->build_sample_item( + { + homebranch => $library1->branchcode, + holdingbranch => $library1->branchcode, + } + ); + + my $transfer = $item->request_transfer( { to => $library2, reason => 'Manual' } ); + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 1 } + } + ); + + 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; + + $t->get_ok("//$userid:$password@/api/v1/items/transfers")->status_is(403) + ->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + my $itemnumber = $item->itemnumber; + $t->get_ok("//$userid:$password@/api/v1/items/$itemnumber/transfers") + ->status_is( 200, 'Transfer retrieved successfully' ); + + my $res = $t->tx->res->json; + is( $res->{item_id}, $item->itemnumber, 'Item number matches' ); + is( $res->{from_library_id}, $library1->branchcode, 'From library matches' ); + is( $res->{to_library_id}, $library2->branchcode, 'To library matches' ); + is( $res->{reason}, 'Manual', 'Reason matches' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add()' => sub { + plan tests => 33; + + $schema->storage->txn_begin; + + my $library1 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $library3 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $item = $builder->build_sample_item( + { + homebranch => $library1->branchcode, + holdingbranch => $library1->branchcode, + } + ); + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 1 } + } + ); + + 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 $itemnumber = $item->itemnumber; + + my $params = { + to_library_id => $library2->branchcode, + reason => 'Manual', + }; + + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers" => json => $params )->status_is(403) + ->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers" => json => $params ) + ->status_is( 201, 'Transfer is created successfully' ); + + my $res = $t->tx->res->json; + is( $res->{item_id}, $item->itemnumber, 'Item number matches' ); + is( $res->{from_library_id}, $library1->branchcode, 'From library matches' ); + is( $res->{to_library_id}, $library2->branchcode, 'To library matches' ); + is( $res->{reason}, 'Manual', 'Reason matches' ); + + # Check transfer with limits + my $limit = $builder->build_object( { class => 'Koha::Item::Transfer::Limits' } ); + $limit->set( { toBranch => $library3->branchcode, fromBranch => $library1->branchcode } )->store; + + $params->{to_library_id} = $library3->branchcode; + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers" => json => $params ) + ->status_is( 409, 'Transfer is not created due to transfer limit' ); + + $t->post_ok( + "//$userid:$password@/api/v1/items/$itemnumber/transfers?ignore_limits=1&enqueue=1" => json => $params ) + ->status_is( 201, 'Transfer is created successfully ignoring transfer limit' ); + + # Duplicate transfer + $params->{to_library_id} = $library1->branchcode; + $params->{comments} = 'Test comment'; + + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers" => json => $params )->status_is(409) + ->json_is( '/error' => 'Item is already in transfer queue' ); + + # Add to enqueue even if already in transfer queue + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers?enqueue=1" => json => $params ) + ->status_is( 201, 'Transfer is created successfully to enqueue up transfer' ); + $res = $t->tx->res->json; + + is( $res->{to_library_id}, $library1->branchcode, 'To library matches' ); + is( $res->{reason}, 'Manual', 'Reason matches' ); + is( $res->{comments}, 'Test comment', 'Comments matches' ); + + my $library_transfer_id = $res->{library_transfer_id}; + + # Add to enqueue with replace + $params->{to_library_id} = $library2->branchcode; + $params->{reason} = 'Reserve'; + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers?replace=WrongTransfer" => json => $params ) + ->status_is( 201, 'Transfer is created successfully to replace existing transfer' ); + $res = $t->tx->res->json; + is( $res->{to_library_id}, $library2->branchcode, 'To library matches after replace' ); + is( $res->{reason}, 'Reserve', 'Reason matches after replace' ); + + # Invalid item number + my $invalid_itemnumber = $itemnumber + 1; + $t->post_ok( "//$userid:$password@/api/v1/items/$invalid_itemnumber/transfers" => json => $params )->status_is(404) + ->json_is( '/error' => 'Item not found' ); + + # Invalid library + $params->{to_library_id} = 'InvalidLibrary'; + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers" => json => $params )->status_is(404) + ->json_is( '/error' => 'Library not found' ); + + # Invalid reason + $params->{reason} = 'InvalidReason'; + $t->post_ok( "//$userid:$password@/api/v1/items/$itemnumber/transfers" => json => $params )->status_is(400); + + $schema->storage->txn_rollback; +}; + +subtest 'delete()' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + my $library1 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); + my $item = $builder->build_sample_item( + { + homebranch => $library1->branchcode, + holdingbranch => $library1->branchcode, + } + ); + + my $transfer = $item->request_transfer( { to => $library2, reason => 'Manual' } ); + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 1 } + } + ); + + 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 $itemnumber = $item->itemnumber; + $t->delete_ok("//$userid:$password@/api/v1/items/$itemnumber/transfers")->status_is(403) + ->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + is( $item->transfer->branchtransfer_id, $transfer->branchtransfer_id, 'Item transfer exists' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->delete_ok("//$userid:$password@/api/v1/items/$itemnumber/transfers") + ->status_is( 204, 'Transfer deleted successfully' ); + + is( $item->transfer, undef, 'Item transfer is removed' ); + + $schema->storage->txn_rollback; +}; -- 2.34.1