From bbea7402ab9a16a8c3d678ea05297cdac983322b Mon Sep 17 00:00:00 2001 From: Agustin Moyano Date: Tue, 20 Oct 2020 13:31:58 -0300 Subject: [PATCH] Bug 15522: (Vue alternative) Modify api endpoint for circ rules --- Koha/CirculationRules.pm | 192 ++++++++++++- Koha/Exceptions/Authorization.pm | 3 + Koha/REST/V1/CirculationRules.pm | 53 ++++ api/v1/swagger/definitions.json | 3 + .../swagger/definitions/circ-rule-kind.json | 24 ++ api/v1/swagger/definitions/circ-rule.json | 31 ++ api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/circulation-rules.json | 132 ++++++++- api/v1/swagger/x-primitives.json | 4 + t/db_dependent/api/v1/circ_rules.t | 270 ++++++++++++++++++ 10 files changed, 709 insertions(+), 6 deletions(-) create mode 100644 api/v1/swagger/definitions/circ-rule.json create mode 100644 t/db_dependent/api/v1/circ_rules.t diff --git a/Koha/CirculationRules.pm b/Koha/CirculationRules.pm index 05850bee0f..216357e8ae 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -39,123 +39,283 @@ Koha::CirculationRules - Koha CirculationRule Object set class =head3 rule_kinds -This structure describes the possible rules that may be set, and what scopes they can be set at. +This structure describes the possible rules that may be set, what scopes they can be set at and any +default values. -Any attempt to set a rule with a nonsensical scope (for instance, setting the C for a branchcode and itemtype), is an error. +Any attempt to set a rule with a nonsensical scope (for instance, setting the C +for a branchcode and itemtype), is an error. + +These default values correspond to what would be stored in the database if nothing was changed on +the old circulation rules editor; this is a combination of the default values in the template and +the issuingrules table. =cut our $RULE_KINDS = { refund => { scope => [ 'branchcode' ], + default_value => 1, + group => 'fines', + description => "Refund lost item fee", + choices => [ + [ 0, "No" ], + [ 1, "Yes" ], + ], }, patron_maxissueqty => { scope => [ 'branchcode', 'categorycode' ], + default_value => "", + group => 'circulation', + description => "Total current checkouts allowed", }, patron_maxonsiteissueqty => { scope => [ 'branchcode', 'categorycode' ], + default_value => "", + group => 'circulation', + description => "Total current on-site checkouts allowed", }, max_holds => { scope => [ 'branchcode', 'categorycode' ], + default_value => "", + group => 'holds', + description => "Maximum total holds allowed", }, holdallowed => { scope => [ 'branchcode', 'itemtype' ], can_be_blank => 0, + default_value => 2, + group => 'holds', + description => "Hold policy", + choices => [ + [ 2, "From any library" ], + [ 1, "From home library" ], + [ 0, "No holds allowed" ], + ], }, hold_fulfillment_policy => { scope => [ 'branchcode', 'itemtype' ], can_be_blank => 0, + default_value => 'any', + group => 'holds', + description => "Hold pickup library match", + choices => [ + [ "any", "any library" ], + [ "homebranch", "item's home library" ], + [ "holdingbranch", "item's holding library" ], + ], }, returnbranch => { scope => [ 'branchcode', 'itemtype' ], can_be_blank => 0, + default_value => 'homebranch', + group => 'circulation', + description => "Return policy", + choices => [ + [ "homebranch", "Item returns home" ], + [ "holdingbranch", "Item returns to issuing library" ], + [ "noreturn", "Item floats" ], + ], }, article_requests => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 'no', + group => 'circulation', + description => "Article requests", + choices => [ + [ "no", "No" ], + [ "yes", "Yes" ], + [ "bib_only", "Record only" ], + [ "item_only", "Item only" ], + ], }, auto_renew => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'circulation', + description => "Automatic renewal", + choices => [ + [ 0, "No" ], + [ 1, "Yes" ], + ], }, cap_fine_to_replacement_price => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'fines', + description => "Cap fine at replacement price", + choices => [ + [ 0, "No" ], + [ 1, "Yes" ], + ], }, chargeperiod => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'fines', + description => "Fine charging interval", }, chargeperiod_charge_at => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'circulation', + description => "When to charge", + choices => [ + [ 0, "End of interval" ], + [ 1, "Start of interval" ], + ], }, fine => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'fines', + description => "Fine amount", }, finedays => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'fines', + description => "Suspension in days", }, firstremind => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'fines', + description => "Fine grace period", }, hardduedate => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'circulation', + description => "Hard due date", + type => "date", }, hardduedatecompare => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => -1, + group => 'circulation', + description => "Hard due date type", + choices => [ + [ -1, "Before" ], + [ 0, "Exactly on" ], + [ 1, "After" ], + ], }, holds_per_day => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + group => 'holds', }, holds_per_record => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 1, + group => 'holds', + description => "Holds per record (count)", }, issuelength => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'circulation', + description => "Loan period", }, daysmode => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], }, lengthunit => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 'days', + group => 'circulation', + description => "Loan period unit", }, - maxissueqty => { + patron_maxissueqty => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'circulation', + description => "Current checkouts allowed", }, - maxonsiteissueqty => { + patron_maxonsiteissueqty => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'circulation', + description => "Current on-site checkouts allowed", }, maxsuspensiondays => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'fines', + description => "Max. suspension duration (days)", }, no_auto_renewal_after => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'circulation', + description => "No automatic renewal after", }, no_auto_renewal_after_hard_limit => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'circulation', + description => "No automatic renewal after (hard limit)", + type => "date", }, norenewalbefore => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'circulation', + description => "No renewal before", }, onshelfholds => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'holds', + description => "On shelf holds allowed", + choices => [ + [ 1, "Yes" ], + [ 0, "If any unavailable" ], + [ 2, "If all unavailable" ], + ], }, opacitemholds => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "N", + group => 'holds', + description => "Item-level holds", + choices => [ + [ "N", "Don't allow" ], + [ "Y", "Allow" ], + [ "F", "Force" ], + ], }, overduefinescap => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => "", + group => 'fines', + description => "Overdue fines cap (amount)", }, renewalperiod => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'circulation', + description => "Renewal period", }, renewalsallowed => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'circulation', + description => "Renewals allowed (count)", }, rentaldiscount => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'circulation', + description => "Rental discount (%)", }, reservesallowed => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], + default_value => 0, + group => 'holds', + description => "Holds allowed (count)", }, suspension_chargeperiod => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], @@ -249,6 +409,25 @@ sub get_effective_rules { =head3 set_rule +=over 4 +Koha::CirculationRules->set_rule( { + branchcode => $branchcode, + [ categorycode => $categorycode, ] + [ itemtype => $itemtype, ] + rule_name => $rule_name, + rule_value => $rule_value, + [ allow_null_out_of_scope => 1, ] +} ) +=back + +Sets a single circulation rule. + +It is an error to specify a C or C unless the given C can be set +at that scope; i.e., you cannot set a C for an itemtype. + +If C is passed, and any excess C or C is set to C, +they are ignored. + =cut sub set_rule { @@ -272,7 +451,10 @@ sub set_rule { unless exists $params->{$scope_level}; } else { croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!" - if exists $params->{$scope_level}; + unless !exists $params->{$scope_level} || ( + $params->{allow_null_out_of_scope} && + !defined $params->{$scope_level} + ); } } diff --git a/Koha/Exceptions/Authorization.pm b/Koha/Exceptions/Authorization.pm index a3c625aca8..56c9d8b0cb 100644 --- a/Koha/Exceptions/Authorization.pm +++ b/Koha/Exceptions/Authorization.pm @@ -12,6 +12,9 @@ use Exception::Class ( description => 'Unauthorized', fields => ['required_permissions'] }, + 'Koha::Exceptions::Authorization::Restricted' => { + description => 'User can only modify settings for their branch' + }, ); diff --git a/Koha/REST/V1/CirculationRules.pm b/Koha/REST/V1/CirculationRules.pm index baa04b5211..2f30b12f65 100644 --- a/Koha/REST/V1/CirculationRules.pm +++ b/Koha/REST/V1/CirculationRules.pm @@ -19,7 +19,12 @@ use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; +use C4::Auth; +use C4::Context; + use Koha::CirculationRules; +use Koha::Database; +use Koha::Exceptions::Authorization; use Try::Tiny; @@ -42,4 +47,52 @@ sub get_kinds { ); } +sub get_rules { + my $c = shift->openapi->valid_input or return; + + return $c->render( + status => 200, + openapi => [ Koha::CirculationRules->search ], + ); +} + +sub save_rules { + my $c = shift->openapi->valid_input or return; + + my $schema = Koha::Database->new->schema; + + my $uid = $c->stash( 'koha.user' )->userid; + my $restricted_to_library = $uid && haspermission( $uid, { parameters => 'manage_circ_rules_from_any_libraries' } ) ? "" : $c->stash( 'koha.user' )->branchcode; + + return try { + my $rules = $c->req->json; + + $schema->storage->txn_do( sub { + foreach my $rule ( @$rules ) { + if ( $restricted_to_library && ( !$rule->{branchcode} || $rule->{branchcode} ne $restricted_to_library ) ) { + Koha::Exceptions::Authorization::Restricted->throw( + error => 'User can only modify settings for their branch.' + ); + } + + Koha::CirculationRules->set_rule( { %$rule, allow_null_out_of_scope => 1 } ); + } + } ); + + return $c->render( status => 200, openapi => "" ); + } + catch { + if ( $_->isa('Koha::Exceptions::Authorization::Restricted') ) { + return $c->render( status => 403, + openapi => { error => $_->message } ); + } else { + warn $_; + + 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 61e6c15306..20ec77940a 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -5,6 +5,9 @@ "basket": { "$ref": "definitions/basket.json" }, + "circ-rule": { + "$ref": "definitions/circ-rule.json" + }, "circ-rule-kind": { "$ref": "definitions/circ-rule-kind.json" }, diff --git a/api/v1/swagger/definitions/circ-rule-kind.json b/api/v1/swagger/definitions/circ-rule-kind.json index 90b956f9f6..cdfa08b015 100644 --- a/api/v1/swagger/definitions/circ-rule-kind.json +++ b/api/v1/swagger/definitions/circ-rule-kind.json @@ -8,6 +8,30 @@ "type": "string", "enum": [ "branchcode", "categorycode", "itemtype" ] } + }, + "default_value": { + "description": "effective default if no rules are set", + "type": [ "string", "null" ] + }, + "can_be_blank": { + "description": "if rule can be blank", + "type": [ "boolean", "null"] + }, + "group": { + "description": "which group this rule belongs to", + "type": "string" + }, + "description": { + "description": "text describing this rule", + "type": "string" + }, + "choices": { + "description": "predefined set of values this rule can be", + "type": "array" + }, + "type": { + "description": "type of rule data", + "type": "string" } }, "additionalProperties": false, diff --git a/api/v1/swagger/definitions/circ-rule.json b/api/v1/swagger/definitions/circ-rule.json new file mode 100644 index 0000000000..3125fecd10 --- /dev/null +++ b/api/v1/swagger/definitions/circ-rule.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "properties": { + "id": { + "description": "internal identifier of this rule", + "type": "integer" + }, + "branchcode": { + "type": [ "string", "null" ], + "description": "branch that this rule applies to" + }, + "categorycode": { + "type": [ "string", "null" ], + "description": "category that this rule applies to" + }, + "itemtype": { + "type": [ "string", "null" ], + "description": "itemtype that this rule applies to" + }, + "rule_name": { + "type": "string", + "description": "name of the kind of this rule" + }, + "rule_value": { + "type": [ "string", "null" ], + "description": "value of the rule" + } + }, + "additionalProperties": false, + "required": [ "branchcode", "rule_name", "rule_value" ] +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 8ba95189dc..31869b7d5b 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -26,6 +26,9 @@ "/checkouts/{checkout_id}/renewal": { "$ref": "paths/checkouts.json#/~1checkouts~1{checkout_id}~1renewal" }, + "/circulation-rules": { + "$ref": "paths/circulation-rules.json#/~1circulation-rules" + }, "/circulation-rules/kinds": { "$ref": "paths/circulation-rules.json#/~1circulation-rules~1kinds" }, diff --git a/api/v1/swagger/paths/circulation-rules.json b/api/v1/swagger/paths/circulation-rules.json index 0f118d344e..f772a70a5c 100644 --- a/api/v1/swagger/paths/circulation-rules.json +++ b/api/v1/swagger/paths/circulation-rules.json @@ -1,9 +1,122 @@ { + "/circulation-rules": { + "get": { + "x-mojo-to": "CirculationRules#get_rules", + "operationId": "getCirculationRules", + "tags": ["circulation-rules"], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A list of defined rules", + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/circ-rule" + } + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "manage_circ_rules": "1" + } + } + }, + "post": { + "x-mojo-to": "CirculationRules#save_rules", + "operationId": "saveCirculationRules", + "tags": ["circulation-rules"], + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing rules to set; null for a rule_value means to delete the rule", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/circ-rule" + } + } + }], + "responses": { + "200": { + "description": "Rules saved", + "schema": { + "type": "string" + } + }, + "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" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "manage_circ_rules": "1" + } + } + } + }, "/circulation-rules/kinds": { "get": { "x-mojo-to": "CirculationRules#get_kinds", "operationId": "getCirculationRuleKinds", - "tags": ["cities"], + "tags": ["circulation-rules"], "produces": [ "application/json" ], @@ -17,6 +130,12 @@ } } }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, "403": { "description": "Access forbidden", "schema": { @@ -28,6 +147,17 @@ "schema": { "$ref": "../definitions.json#/error" } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "manage_circ_rules": "1" } } } diff --git a/api/v1/swagger/x-primitives.json b/api/v1/swagger/x-primitives.json index 0594264be9..dbc234913f 100644 --- a/api/v1/swagger/x-primitives.json +++ b/api/v1/swagger/x-primitives.json @@ -18,6 +18,10 @@ "maxLength": 10, "minLength": 1 }, + "branchcode": { + "type": "string", + "description": "library assigned branch identifier" + }, "cardnumber": { "type": ["string", "null"], "description": "library assigned user identifier" diff --git a/t/db_dependent/api/v1/circ_rules.t b/t/db_dependent/api/v1/circ_rules.t new file mode 100644 index 0000000000..24bbb3cd72 --- /dev/null +++ b/t/db_dependent/api/v1/circ_rules.t @@ -0,0 +1,270 @@ +#!/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 => 5; +use Test::Deep qw( cmp_deeply set superhashof ); +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use C4::Auth; +use Koha::Acquisition::Booksellers; +use Koha::CirculationRules; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling +# this affects the other REST api tests +t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); + +my $remote_address = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +sub tx_maker { + my $args = shift; + + my ( $borrowernumber, $session_id ) = create_user_and_session( $args ); + + return sub { + my $tx = shift->ua->build_tx( @_ ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + + return $tx; + } +} + +subtest 'rule_kinds' => sub { + plan tests => 3; + + my $tx_maker = tx_maker( { permissions => 'authorized' } ); + + my $tx = $tx_maker->( $t, GET => '/api/v1/circulation-rules/kinds' ); + $t->request_ok($tx) + ->status_is(200) + ->json_is( Koha::CirculationRules->rule_kinds ); +}; + +subtest 'get_rules' => sub { + plan tests => 6; + + $schema->storage->txn_begin; + + Koha::CirculationRules->search->delete; + + my $tx_maker = tx_maker( { permissions => 'authorized' } ); + + my $tx = $tx_maker->( $t, GET => '/api/v1/circulation-rules' ); + $t->request_ok($tx) + ->status_is(200) + ->json_is( [] ); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my @rules = ( + { + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 10, + }, + { + branchcode => $branchcode, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 20, + }, + ); + + Koha::CirculationRules->set_rule( $_ ) for ( @rules ); + + $tx = $tx_maker->( $t, GET => '/api/v1/circulation-rules' ); + $t->request_ok($tx) + ->status_is(200); + cmp_deeply( $tx->res->json, set( map { superhashof( $_ ) } @rules ), 'rules retrieved correctly' ); + + $schema->storage->txn_rollback; +}; + +subtest 'set_rules | unauthorized' => sub { + plan tests => 2; + + $schema->storage->txn_begin; + + Koha::CirculationRules->search->delete; + + my $tx_maker = tx_maker( { permissions => 'none' } ); + + my $rule = { + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 10, + }; + + my $tx = $tx_maker->( $t, POST => '/api/v1/circulation-rules', json => $rule ); + $t->request_ok($tx) + ->status_is(403); + + $schema->storage->txn_rollback; +}; + +subtest 'set_rules' => sub { + plan tests => 3; + + $schema->storage->txn_begin; + + Koha::CirculationRules->search->delete; + + my $tx_maker = tx_maker( { permissions => 'authorized' } ); + + my @rules = ( + { + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 10, + }, + ); + + my $tx = $tx_maker->( $t, POST => '/api/v1/circulation-rules', json => \@rules ); + $t->request_ok($tx) + ->status_is(200); + + my $stored_rule = Koha::CirculationRules->search( { + branchcode => undef, + categorycode => undef, + itemtype => undef, + } )->next; + + cmp_deeply( + $stored_rule && $stored_rule->unblessed, + superhashof( $rules[0] ), + 'rule stored correctly' + ); + + $schema->storage->txn_rollback; +}; + +subtest 'set_rules | restricted' => sub { + plan tests => 6; + + $schema->storage->txn_begin; + + Koha::CirculationRules->search->delete; + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my $other_branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my $tx_maker = tx_maker( { permissions => 'authorized_restricted', branchcode => $branchcode } ); + + my @allowed_rules = ( + { + branchcode => $branchcode, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 10, + }, + ); + + my @forbidden_rules = ( + { + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 10, + }, + { + branchcode => $other_branchcode, + categorycode => undef, + itemtype => undef, + rule_name => 'maxissueqty', + rule_value => 20, + }, + ); + + for my $rule ( @allowed_rules ) { + my $tx = $tx_maker->( $t, POST => '/api/v1/circulation-rules', json => [ $rule ] ); + $t->request_ok($tx) + ->status_is(200); + } + + for my $rule ( @forbidden_rules ) { + my $tx = $tx_maker->( $t, POST => '/api/v1/circulation-rules', json => [ $rule ] ); + $t->request_ok($tx) + ->status_is(403); + } + + $schema->storage->txn_rollback; +}; + +sub create_user_and_session { + + my $args = shift; + my $flags = { + none => 0, + # catalogue and parameters permissions + authorized => (1 << 2) | (1 << 3), + # Just catalogue + authorized_restricted => (1 << 2), + }->{ $args->{permissions} || ( $args->{authorized} ? 'authorized' : 'none' ) }; + + my %branch_flags; + $branch_flags{branchcode} = $args->{branchcode} if ( $args->{branchcode} ); + + # my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0; + my $dbh = C4::Context->dbh; + + my $user = $builder->build( + { source => 'Borrower', + value => { flags => $flags, %branch_flags } + } + ); + + # Create a session for the authorized user + my $session = C4::Auth::get_session(''); + $session->param( 'number', $user->{borrowernumber} ); + $session->param( 'id', $user->{userid} ); + $session->param( 'ip', '127.0.0.1' ); + $session->param( 'lasttime', time() ); + $session->flush; + + # Allow circulation rules management for librarian's home library, but not + # other libraries + if ( $args->{permissions} eq 'authorized_restricted' ) { + $dbh->do( + q{ + INSERT INTO user_permissions (borrowernumber,module_bit,code) + VALUES (?,3,'manage_circ_rules')}, + undef, $user->{borrowernumber}, $user->{borrowernumber} + ); + } + + return ( $user->{borrowernumber}, $session->id ); +} + +1; -- 2.25.0