From fd92a05fe937be4e68b53246ca062cebbdb21ede Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Wed, 19 Aug 2020 08:34:35 +0100 Subject: [PATCH] Bug 26274: Add cashups api routes This patch creates a Koha::Cash::Register::Cashup(s) class pair which subclass Koha::Cash::Register::Action(s) and moves the cashup specific code into these new classes to improve code separation. We then introduce API routes based on these classes to allow fetching a list of cashups associated to a cash register and a full cashup with emeddable summary for individual cashups. Test plan 1/ Run the updated unit tests. t/db_dependent/Koha/Cash/Register/Action.t t/db_dependent/Koha/Cash/Register/Cashup.t 2/ Run the incuded api tests. t/db_dependent/api/v1/cashups.t Signed-off-by: David Nind --- Koha/Cash/Register.pm | 9 +- Koha/Cash/Register/Action.pm | 97 ----- Koha/Cash/Register/Cashup.pm | 208 +++++++++++ Koha/Cash/Register/Cashups.pm | 64 ++++ Koha/REST/V1/CashRegisters/Cashups.pm | 99 ++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/cashup.json | 30 ++ api/v1/swagger/parameters.json | 6 + api/v1/swagger/parameters/cash_register.json | 9 + api/v1/swagger/parameters/cashup.json | 9 + api/v1/swagger/paths.json | 6 + api/v1/swagger/paths/cash_registers.json | 104 ++++++ t/db_dependent/Koha/Cash/Register/Action.t | 269 +------------- t/db_dependent/Koha/Cash/Register/Cashup.t | 352 +++++++++++++++++++ t/db_dependent/api/v1/cashups.t | 177 ++++++++++ 15 files changed, 1073 insertions(+), 369 deletions(-) create mode 100644 Koha/Cash/Register/Cashup.pm create mode 100644 Koha/Cash/Register/Cashups.pm create mode 100644 Koha/REST/V1/CashRegisters/Cashups.pm create mode 100644 api/v1/swagger/definitions/cashup.json create mode 100644 api/v1/swagger/parameters/cash_register.json create mode 100644 api/v1/swagger/parameters/cashup.json create mode 100644 api/v1/swagger/paths/cash_registers.json create mode 100644 t/db_dependent/Koha/Cash/Register/Cashup.t create mode 100644 t/db_dependent/api/v1/cashups.t diff --git a/Koha/Cash/Register.pm b/Koha/Cash/Register.pm index e54ce5f228..2daa52ed41 100644 --- a/Koha/Cash/Register.pm +++ b/Koha/Cash/Register.pm @@ -22,6 +22,7 @@ use Carp; use Koha::Account::Lines; use Koha::Account::Offsets; use Koha::Cash::Register::Actions; +use Koha::Cash::Register::Cashups; use Koha::Database; use base qw(Koha::Object); @@ -66,7 +67,7 @@ sub cashups { $self->_result->search_related( 'cash_register_actions', $merged_conditions, $attrs ); - return Koha::Cash::Register::Actions->_new_from_dbic($rs); + return Koha::Cash::Register::Cashups->_new_from_dbic($rs); } =head3 last_cashup @@ -85,7 +86,7 @@ sub last_cashup { )->single; return unless $rs; - return Koha::Cash::Register::Action->_new_from_dbic($rs); + return Koha::Cash::Register::Cashup->_new_from_dbic($rs); } =head3 accountlines @@ -208,7 +209,7 @@ sub drop_default { =head3 add_cashup - my $action = $cash_register->add_cashup( + my $cashup = $cash_register->add_cashup( { manager_id => $logged_in_user->id, amount => $cash_register->outstanding_accountlines->total @@ -230,7 +231,7 @@ sub add_cashup { } )->discard_changes; - return Koha::Cash::Register::Action->_new_from_dbic($rs); + return Koha::Cash::Register::Cashup->_new_from_dbic($rs); } =head2 Internal methods diff --git a/Koha/Cash/Register/Action.pm b/Koha/Cash/Register/Action.pm index 759bd98ccc..ede413c21d 100644 --- a/Koha/Cash/Register/Action.pm +++ b/Koha/Cash/Register/Action.pm @@ -60,103 +60,6 @@ sub register { return Koha::Cash::Register->_new_from_dbic($rs); } -=head3 cashup_summary - - my $cashup_summary = $action->cashup_summary; - -Return a hashref containing a summary of transactions that make up this cashup action. - -=cut - -sub cashup_summary { - my ($self) = @_; - my $summary; - my $prior_cashup = Koha::Cash::Register::Actions->search( - { - 'code' => 'CASHUP', - 'timestamp' => { '<' => $self->timestamp }, - register_id => $self->register_id - }, - { - order_by => { '-desc' => [ 'timestamp', 'id' ] }, - rows => 1 - } - )->single; - - my $conditions = - $prior_cashup - ? { - 'date' => { - '-between' => - [ $prior_cashup->timestamp, $self->timestamp ] - } - } - : { 'date' => { '<' => $self->timestamp } }; - - my $outgoing_transactions = $self->register->accountlines->search( - { %{$conditions}, credit_type_code => undef }, - ); - my $income_transactions = $self->register->accountlines->search( - { %{$conditions}, debit_type_code => undef }, - ); - - my $income_summary = Koha::Account::Offsets->search( - { - 'me.credit_id' => - { '-in' => [ $income_transactions->get_column('accountlines_id') ] }, - 'me.debit_id' => { '!=' => undef } - }, - { - join => { 'debit' => 'debit_type_code' }, - group_by => [ 'debit.debit_type_code', 'debit_type_code.description' ], - order_by => { '-asc' => 'debit.debit_type_code' }, - 'select' => [ { sum => 'me.amount' }, 'debit.debit_type_code', 'debit_type_code.description' ], - 'as' => [ 'total', 'debit_type_code', 'debit_description' ], - } - ); - - my $outgoing_summary = Koha::Account::Offsets->search( - { - 'me.debit_id' => - { '-in' => [ $outgoing_transactions->get_column('accountlines_id') ] }, - 'me.credit_id' => { '!=' => undef } - }, - { - join => { 'credit' => 'credit_type_code' }, - group_by => [ 'credit.credit_type_code', 'credit_type_code.description' ], - order_by => { '-asc' => 'credit.credit_type_code' }, - 'select' => [ { sum => 'me.amount' }, 'credit.credit_type_code', 'credit_type_code.description' ], - 'as' => [ 'total', 'credit_type_code', 'credit_description' ], - } - ); - - my @income = map { - { - total => $_->get_column('total'), - debit_type_code => $_->get_column('debit_type_code'), - debit_type => { description => $_->get_column('debit_description') } - } - } $income_summary->as_list; - my @outgoing = map { - { - total => $_->get_column('total'), - credit_type_code => $_->get_column('credit_type_code'), - credit_type => { description => $_->get_column('credit_description') } - } - } $outgoing_summary->as_list; - - $summary = { - from_date => $prior_cashup? $prior_cashup->timestamp : undef, - to_date => $self->timestamp, - income => \@income, - outgoing => \@outgoing, - income_transactions => $income_transactions, - outgoing_transactions => $outgoing_transactions, - }; - - return $summary; -} - =head2 Internal methods =cut diff --git a/Koha/Cash/Register/Cashup.pm b/Koha/Cash/Register/Cashup.pm new file mode 100644 index 0000000000..a7e071afae --- /dev/null +++ b/Koha/Cash/Register/Cashup.pm @@ -0,0 +1,208 @@ +package Koha::Cash::Register::Cashup; + +# 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 Carp; + +use Koha::Database; + +use base qw(Koha::Cash::Register::Action); + +=head1 NAME + +Koha::Cash::Register::Actions - Koha Cash Register Action Object set class + +=head1 API + +=head2 Class methods + +=head3 search + + my $cashup = Koha::Cash::Register::Actions::Cashup->search( $where, $attr ); + +Returns a list of cash register cashup. + +=cut + +sub search { + my ( $self, $where, $attr ) = @_; + + $where->{code} = 'CASHUP'; + + unless ( exists $attr->{order_by} ) { + $attr->{order_by} = + [ { '-asc' => 'register_id' }, { '-desc' => 'timestamp' } ]; + } + + return $self->SUPER::search( $where, $attr ); +} + +=head3 summary + + my $summary = $cashup->summary; + +Return a hashref containing a summary of transactions that make up this cashup. + +=cut + +sub summary { + my ($self) = @_; + my $summary; + my $prior_cashup = Koha::Cash::Register::Cashups->search( + { + 'timestamp' => { '<' => $self->timestamp }, + register_id => $self->register_id + }, + { + order_by => { '-desc' => [ 'timestamp', 'id' ] }, + rows => 1 + } + ); + + my $previous = $prior_cashup->single; + + my $conditions = + $previous + ? { + 'date' => { + '-between' => + [ $previous->_result->get_column('timestamp'), $self->timestamp ] + } + } + : { 'date' => { '<' => $self->timestamp } }; + + my $payout_transactions = $self->register->accountlines->search( + { %{$conditions}, credit_type_code => undef }, + ); + my $income_transactions = $self->register->accountlines->search( + { %{$conditions}, debit_type_code => undef }, + ); + + my $income_summary = Koha::Account::Offsets->search( + { + 'me.credit_id' => { + '-in' => $income_transactions->_resultset->get_column( + 'accountlines_id')->as_query + }, + 'me.debit_id' => { '!=' => undef } + }, + { + join => { 'debit' => 'debit_type_code' }, + group_by => + [ 'debit.debit_type_code', 'debit_type_code.description' ], + 'select' => [ + { sum => 'me.amount' }, 'debit.debit_type_code', + 'debit_type_code.description' + ], + 'as' => [ 'total', 'debit_type_code', 'debit_description' ], + } + ); + + my $payout_summary = Koha::Account::Offsets->search( + { + 'me.debit_id' => { + '-in' => $payout_transactions->_resultset->get_column( + 'accountlines_id')->as_query + }, + 'me.credit_id' => { '!=' => undef } + }, + { + join => { 'credit' => 'credit_type_code' }, + group_by => + [ 'credit.credit_type_code', 'credit_type_code.description' ], + 'select' => [ + { sum => 'me.amount' }, 'credit.credit_type_code', + 'credit_type_code.description' + ], + 'as' => [ 'total', 'credit_type_code', 'credit_description' ], + } + ); + + my @income = map { + { + total => $_->get_column('total') * -1, + debit_type_code => $_->get_column('debit_type_code'), + debit_type => { description => $_->get_column('debit_description') } + } + } $income_summary->as_list; + my @payout = map { + { + total => $_->get_column('total') * -1, + credit_type_code => $_->get_column('credit_type_code'), + credit_type => + { description => $_->get_column('credit_description') } + } + } $payout_summary->as_list; + + my $income_total = $income_transactions->total; + my $payout_total = $payout_transactions->total; + my $total = ( $income_total + $payout_total ); + + my $payment_types = Koha::AuthorisedValues->search( + { + category => 'PAYMENT_TYPE' + }, + { + order_by => ['lib'], + } + ); + + my @total_grouped; + for my $type ( $payment_types->as_list ) { + my $typed_income = $income_transactions->total( { payment_type => $type->authorised_value } ); + my $typed_payout = $payout_transactions->total( { payment_type => $type->authorised_value } ); + my $typed_total = ( $typed_income + $typed_payout ); + push @total_grouped, { payment_type => $type->lib, total => $typed_total }; + } + + $summary = { + from_date => $previous ? $previous->timestamp : undef, + to_date => $self->timestamp, + income_grouped => \@income, + income_total => abs($income_total), + payout_grouped => \@payout, + payout_total => abs($payout_total), + total => $total * -1, + total_grouped => \@total_grouped + }; + + return $summary; +} + +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::Cash::Regiser::Cashup object +on the API. + +=cut + +sub to_api_mapping { + return { + id => 'cashup_id', + register_id => 'cash_register_id', + code => undef + }; +} + +1; + +=head1 AUTHORS + +Martin Renvoize + +=cut diff --git a/Koha/Cash/Register/Cashups.pm b/Koha/Cash/Register/Cashups.pm new file mode 100644 index 0000000000..9100f38266 --- /dev/null +++ b/Koha/Cash/Register/Cashups.pm @@ -0,0 +1,64 @@ +package Koha::Cash::Register::Cashups; + +# 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 Carp; + +use Koha::Database; +use Koha::Cash::Register::Cashup; + +use base qw(Koha::Cash::Register::Actions); + +=head1 NAME + +Koha::Cash::Register::Actions - Koha Cash Register Action Object set class + +=head1 API + +=head2 Class methods + +=head3 search + + my $cashups = Koha::Cash::Register::Cashups->search( $where, $attr ); + +Returns a list of cash register cashups. + +=cut + +sub search { + my ( $self, $where, $attr ) = @_; + + $where->{code} = 'CASHUP'; + + unless ( exists $attr->{order_by} ) { + $attr->{order_by} = + [ { '-asc' => 'register_id' }, { '-desc' => 'timestamp' } ]; + } + + return $self->SUPER::search( $where, $attr ); +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Cash::Register::Cashup'; +} + +1; diff --git a/Koha/REST/V1/CashRegisters/Cashups.pm b/Koha/REST/V1/CashRegisters/Cashups.pm new file mode 100644 index 0000000000..8bb483bdde --- /dev/null +++ b/Koha/REST/V1/CashRegisters/Cashups.pm @@ -0,0 +1,99 @@ +package Koha::REST::V1::CashRegisters::Cashups; + +# 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 Scalar::Util qw(blessed); +use Try::Tiny; + +use Koha::Cash::Registers; + +=head1 NAME + +Koha::REST::V1::CashRegisters::Cashups + +=head1 API + +=head2 Methods + +=head3 list + +Controller function that handles retrieving a cash registers cashup actions + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + my $register = Koha::Cash::Registers->find( + { + id => $c->validation->param('cash_register_id') + } + ); + + unless ($register) { + return $c->render( + status => 404, + openapi => { + error => "Register not found" + } + ); + } + + return try { + my $cashups_rs = $register->cashups; + my $cashups = $c->objects->search($cashups_rs); + return $c->render( status => 200, openapi => $cashups ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +Controller function that handles retrieving a cash register cashup + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $cashup = Koha::Cash::Register::Cashups->find( + $c->validation->param('cashup_id') ); + unless ($cashup) { + return $c->render( + status => 404, + openapi => { error => "Cashup not found" } + ); + } + + my $embed = $c->stash('koha.embed'); + return $c->render( + status => 200, + openapi => $cashup->to_api( { embed => $embed } ) + ); + } + catch { + $c->unhandled_exception($_); + } +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index bc844bdc8f..fdd44d9535 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -5,6 +5,9 @@ "basket": { "$ref": "definitions/basket.json" }, + "cashup": { + "$ref": "definitions/cashup.json" + }, "circ-rule-kind": { "$ref": "definitions/circ-rule-kind.json" }, diff --git a/api/v1/swagger/definitions/cashup.json b/api/v1/swagger/definitions/cashup.json new file mode 100644 index 0000000000..e643a9c493 --- /dev/null +++ b/api/v1/swagger/definitions/cashup.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "properties": { + "cashup_id": { + "type": "integer", + "description": "Internal cashup identifier" + }, + "cash_register_id": { + "type": "integer", + "description": "Internal identifier for the register the cashup belongs to" + }, + "manager_id": { + "type": "integer", + "description": "Internal identifier for the manager the cashup was performed by" + }, + "amount": { + "type": "number", + "description": "Account line amount" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp for the latest line update" + }, + "summary": { + "type": "object", + "description": "A summary of the cashup action" + } + } +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 8fc892a6af..92653a41aa 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -47,6 +47,12 @@ "seen_pp": { "$ref": "parameters/checkout.json#/seen_pp" }, + "cash_register_id_pp": { + "$ref": "parameters/cash_register.json#/cash_register_id_pp" + }, + "cashup_id_pp": { + "$ref": "parameters/cashup.json#/cashup_id_pp" + }, "match": { "name": "_match", "in": "query", diff --git a/api/v1/swagger/parameters/cash_register.json b/api/v1/swagger/parameters/cash_register.json new file mode 100644 index 0000000000..5d46cc94a1 --- /dev/null +++ b/api/v1/swagger/parameters/cash_register.json @@ -0,0 +1,9 @@ +{ + "cash_register_id_pp": { + "name": "cash_register_id", + "in": "path", + "description": "Cash register internal identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/parameters/cashup.json b/api/v1/swagger/parameters/cashup.json new file mode 100644 index 0000000000..c0c3883959 --- /dev/null +++ b/api/v1/swagger/parameters/cashup.json @@ -0,0 +1,9 @@ +{ + "cashup_id_pp": { + "name": "cashup_id", + "in": "path", + "description": "Cashup internal identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index df30f36459..0462a70b56 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -23,6 +23,12 @@ "/biblios/{biblio_id}/items": { "$ref": "paths/biblios.json#/~1biblios~1{biblio_id}~1items" }, + "/cash_registers/{cash_register_id}/cashups": { + "$ref": "paths/cash_registers.json#/~1cash_registers~1{cash_register_id}~1cashups" + }, + "/cashups/{cashup_id}": { + "$ref": "paths/cash_registers.json#/~1cashups~1{cashup_id}" + }, "/checkouts": { "$ref": "paths/checkouts.json#/~1checkouts" }, diff --git a/api/v1/swagger/paths/cash_registers.json b/api/v1/swagger/paths/cash_registers.json new file mode 100644 index 0000000000..837f1ad35b --- /dev/null +++ b/api/v1/swagger/paths/cash_registers.json @@ -0,0 +1,104 @@ +{ + "/cash_registers/{cash_register_id}/cashups": { + "get": { + "x-mojo-to": "CashRegisters::Cashups#list", + "operationId": "listCashups", + "tags": ["cash_registers", "cashups"], + "produces": ["application/json"], + "parameters": [{ + "$ref": "../parameters.json#/cash_register_id_pp" + }, + { + "$ref": "../parameters.json#/match" + }, + { + "$ref": "../parameters.json#/order_by" + }, + { + "$ref": "../parameters.json#/page" + }, + { + "$ref": "../parameters.json#/per_page" + }, + { + "$ref": "../parameters.json#/q_param" + }, + { + "$ref": "../parameters.json#/q_body" + }, + { + "$ref": "../parameters.json#/q_header" + } + ], + "responses": { + "200": { + "description": "Cashups performed on this register", + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/cashup" + } + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Register not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "cash_management": "cashup" + } + } + } + }, + "/cashups/{cashup_id}": { + "get": { + "x-mojo-to": "CashRegisters::Cashups#get", + "operationId": "getCashup", + "tags": ["cash_registers", "cashups"], + "parameters": [{ + "$ref": "../parameters.json#/cashup_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A cashup", + "schema": { + "$ref": "../definitions.json#/cashup" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Patron not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "cash_management": "cashup" + } + }, + "x-koha-embed": [ + "summary" + ] + } + } +} diff --git a/t/db_dependent/Koha/Cash/Register/Action.t b/t/db_dependent/Koha/Cash/Register/Action.t index 0d9c70e76c..233c1a7590 100755 --- a/t/db_dependent/Koha/Cash/Register/Action.t +++ b/t/db_dependent/Koha/Cash/Register/Action.t @@ -18,7 +18,7 @@ # along with Koha; if not, see . use Modern::Perl; -use Test::More tests => 3; +use Test::More tests => 2; use Koha::Database; @@ -80,271 +80,4 @@ subtest 'register' => sub { }; -subtest 'cashup_summary' => sub { - plan tests => 14; - - $schema->storage->txn_begin; - - my $register = - $builder->build_object( { class => 'Koha::Cash::Registers' } ); - my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); - my $manager = $builder->build_object( { class => 'Koha::Patrons' } ); - - # Transaction 1 - my $debt1 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => undef, - amount => '1.00', - amountoutstanding => '0.00', - credit_type_code => undef, - debit_type_code => 'OVERDUE', - date => \'NOW() - INTERVAL 10 MINUTE' - }, - } - ); - my $income1 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => $register->id, - amount => '-1.00', - amountoutstanding => '0.00', - credit_type_code => 'PAYMENT', - debit_type_code => undef, - date => \'NOW() - INTERVAL 5 MINUTE' - }, - } - ); - $builder->build_object( - { - class => 'Koha::Account::Offsets', - value => { - credit_id => $income1->accountlines_id, - debit_id => $debt1->accountlines_id, - amount => '1.00', - type => 'Payment' - }, - } - ); - - # Transaction 2 - my $debt2 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => undef, - amount => '1.00', - amountoutstanding => '0.00', - credit_type_code => undef, - debit_type_code => 'ACCOUNT', - date => \'NOW() - INTERVAL 3 MINUTE' - }, - } - ); - my $debt3 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => undef, - amount => '0.50', - amountoutstanding => '0.00', - credit_type_code => undef, - debit_type_code => 'LOST', - date => \'NOW() - INTERVAL 3 MINUTE' - }, - } - ); - my $income2 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => $register->id, - amount => '-1.50', - amountoutstanding => '0.00', - credit_type_code => 'PAYMENT', - debit_type_code => undef, - date => \'NOW() - INTERVAL 3 MINUTE' - }, - } - ); - $builder->build_object( - { - class => 'Koha::Account::Offsets', - value => { - credit_id => $income2->accountlines_id, - debit_id => $debt2->accountlines_id, - amount => '1.00', - type => 'Payment' - }, - } - ); - $builder->build_object( - { - class => 'Koha::Account::Offsets', - value => { - credit_id => $income2->accountlines_id, - debit_id => $debt3->accountlines_id, - amount => '0.50', - type => 'Payment' - }, - } - ); - my $expected_income = [ - { - debit_type_code => 'ACCOUNT', - total => '1.000000', - debit_type => { 'description' => 'Account creation fee' } - }, - { - debit_type_code => 'LOST', - total => '0.500000', - debit_type => { description => 'Lost item' } - }, - { - debit_type_code => 'OVERDUE', - total => '1.000000', - debit_type => { 'description' => 'Overdue fine' } - } - ]; - - # Transaction 3 - my $refund1 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => undef, - amount => '-0.50', - amountoutstanding => '0.00', - credit_type_code => 'REFUND', - debit_type_code => undef, - date => \'NOW() - INTERVAL 3 MINUTE' - }, - } - ); - my $outgoing1 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => $register->id, - amount => '0.50', - amountoutstanding => '0.00', - credit_type_code => undef, - debit_type_code => 'PAYOUT', - date => \'NOW() - INTERVAL 3 MINUTE' - }, - } - ); - $builder->build_object( - { - class => 'Koha::Account::Offsets', - value => { - credit_id => $refund1->accountlines_id, - debit_id => $outgoing1->accountlines_id, - amount => '0.50', - type => 'Refund' - }, - } - ); - my $expected_outgoing = [ - { - 'total' => '0.500000', - 'credit_type' => { - 'description' => 'A refund applied to a patrons fine' - }, - 'credit_type_code' => 'REFUND' - } - ]; - - my $cashup1 = - $register->add_cashup( { manager_id => $manager->id, amount => '2.00' } ); - - my $summary = $cashup1->cashup_summary; - - is( $summary->{from_date}, undef, - "from_date is undefined if there is only one recorded" ); - is( $summary->{to_date}, $cashup1->timestamp, - "to_date equals cashup timestamp" ); - is( ref( $summary->{income_transactions} ), - 'Koha::Account::Lines', - "income_transactions contains Koha::Account::Lines" ); - is( $summary->{income_transactions}->count, - 2, "income_transactions contains 2 transactions" ); - is( ref( $summary->{outgoing_transactions} ), - 'Koha::Account::Lines', - "outgoing_transactions contains Koha::Account::Lines" ); - is( $summary->{outgoing_transactions}->count, - 1, "outgoing_transactions contains 1 transaction" ); - is_deeply( $summary->{income}, $expected_income, - "income arrayref is correct" ); - is_deeply( $summary->{outgoing}, $expected_outgoing, - "outgoing arrayref is correct" ); - - # Backdate cashup1 so we can add a new cashup to check 'previous' - $cashup1->timestamp(\'NOW() - INTERVAL 2 MINUTE')->store(); - $cashup1->discard_changes; - - # Transaction 4 - my $debt4 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => undef, - amount => '2.75', - amountoutstanding => '0.00', - credit_type_code => undef, - debit_type_code => 'OVERDUE', - date => \'NOW() - INTERVAL 1 MINUTE' - }, - } - ); - my $income3 = $builder->build_object( - { - class => 'Koha::Account::Lines', - value => { - register_id => $register->id, - amount => '-2.75', - amountoutstanding => '0.00', - credit_type_code => 'PAYMENT', - debit_type_code => undef, - date => \'NOW() - INTERVAL 1 MINUTE' - }, - } - ); - $builder->build_object( - { - class => 'Koha::Account::Offsets', - value => { - credit_id => $income3->accountlines_id, - debit_id => $debt4->accountlines_id, - amount => '2.75', - type => 'Payment' - }, - } - ); - - my $cashup2 = - $register->add_cashup( { manager_id => $manager->id, amount => '2.75' } ); - - $summary = $cashup2->cashup_summary; - - is( $summary->{from_date}, $cashup1->timestamp, - "from_date returns the timestamp of the previous cashup action" ); - is( $summary->{to_date}, $cashup2->timestamp, - "to_date equals cashup timestamp" ); - is( ref( $summary->{income_transactions} ), - 'Koha::Account::Lines', - "income_transactions contains Koha::Account::Lines" ); - is( $summary->{income_transactions}->count, - 1, "income_transactions contains 2 transactions" ); - is( ref( $summary->{outgoing_transactions} ), - 'Koha::Account::Lines', - "outgoing_transactions contains Koha::Account::Lines" ); - is( $summary->{outgoing_transactions}->count, - 0, "outgoing_transactions contains 1 transaction" ); - - $schema->storage->txn_rollback; -}; - 1; diff --git a/t/db_dependent/Koha/Cash/Register/Cashup.t b/t/db_dependent/Koha/Cash/Register/Cashup.t new file mode 100644 index 0000000000..cc21a9ce05 --- /dev/null +++ b/t/db_dependent/Koha/Cash/Register/Cashup.t @@ -0,0 +1,352 @@ +#!/usr/bin/perl + +# Copyright 2020 Koha Development team +# +# 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 Test::More tests => 3; + +use Koha::Database; + +use t::lib::TestBuilder; + +my $builder = t::lib::TestBuilder->new; +my $schema = Koha::Database->new->schema; + +subtest 'manager' => sub { + plan tests => 2; + + $schema->storage->txn_begin; + + my $manager = $builder->build_object( { class => 'Koha::Patrons' } ); + my $cashup = $builder->build_object( + { + class => 'Koha::Cash::Register::Cashups', + value => { manager_id => $manager->borrowernumber }, + } + ); + + is( ref( $cashup->manager ), + 'Koha::Patron', + 'Koha::Cash::Register::Cashup->manager should return a Koha::Patron' ); + + is( $cashup->manager->id, $manager->id, + 'Koha::Cash::Register::Cashup->manager returns the correct Koha::Patron' + ); + + $schema->storage->txn_rollback; + +}; + +subtest 'register' => sub { + plan tests => 2; + + $schema->storage->txn_begin; + + my $register = + $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $cashup = $builder->build_object( + { + class => 'Koha::Cash::Register::Cashups', + value => { register_id => $register->id }, + } + ); + + is( + ref( $cashup->register ), + 'Koha::Cash::Register', +'Koha::Cash::Register::Cashup->register should return a Koha::Cash::Register' + ); + + is( $cashup->register->id, $register->id, +'Koha::Cash::Register::Cashup->register returns the correct Koha::Cash::Register' + ); + + $schema->storage->txn_rollback; + +}; + +subtest 'summary' => sub { + plan tests => 29; + + $schema->storage->txn_begin; + + my $register = + $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + my $manager = $builder->build_object( { class => 'Koha::Patrons' } ); + my $account = $patron->account; + my $expected_total = 0; + my $expected_income_total = 0; + my $expected_income_grouped = []; + my $expected_payout_total = 0; + my $expected_payout_grouped = []; + + # Transaction 1 (Fine (1.00) + Payment (-1.00)) + my $fine1 = $account->add_debit( + { + amount => '1.00', + type => 'OVERDUE', + interface => 'cron' + } + ); + $fine1->date( \'NOW() - INTERVAL 20 MINUTE' )->store; + + my $payment1 = $account->pay( + { + cash_register => $register->id, + amount => '1.00', + credit_type => 'PAYMENT', + lines => [$fine1] + } + ); + $payment1 = Koha::Account::Lines->find( $payment1->{payment_id} ); + $payment1->date( \'NOW() - INTERVAL 15 MINUTE' )->store; + $expected_income_total += '1.00'; + + # Overdue of 1.0 fully paid + unshift @{$expected_income_grouped}, + { + debit_type_code => 'OVERDUE', + total => '1', + debit_type => { description => 'Overdue fine' } + }; + + # Transaction 2 (Account (1.00) + Lost (0.50) + Payment (-1.50)) + my $account1 = $account->add_debit( + { + amount => '1.00', + type => 'ACCOUNT', + interface => 'cron' + } + ); + $account1->date( \'NOW() - INTERVAL 13 MINUTE' )->store; + my $lost1 = $account->add_debit( + { + amount => '0.50', + type => 'LOST', + interface => 'cron' + } + ); + $lost1->date( \'NOW() - INTERVAL 13 MINUTE' )->store; + my $payment2 = $account->pay( + { + cash_register => $register->id, + amount => '1.50', + credit_type => 'PAYMENT', + lines => [ $account1, $lost1 ] + } + ); + $payment2 = Koha::Account::Lines->find( $payment2->{payment_id} ); + $payment2->date( \'NOW() - INTERVAL 13 MINUTE' )->store; + $expected_income_total += '1.5'; + + # Lost charge of 0.5 fully paid + unshift @{$expected_income_grouped}, + { + debit_type_code => 'LOST', + total => '0.5', + debit_type => { description => 'Lost item' } + }; + + # Account fee of 1.0 fully paid + unshift @{$expected_income_grouped}, + { + debit_type_code => 'ACCOUNT', + total => '1', + debit_type => { description => 'Account creation fee' } + }; + + # Transaction 3 (Refund (-0.50) + Payout (0.50)) + $lost1->discard_changes; + my $refund1 = $lost1->reduce( + { + amount => '0.50', + reduction_type => 'REFUND', + interface => 'cron' + } + ); + $refund1->date( \'NOW() - INTERVAL 13 MINUTE' )->store; + + my $payout1 = $refund1->payout( + { + cash_register => $register->id, + amount => '0.50', + payout_type => 'CASH', + interface => 'intranet', + staff_id => $manager->borrowernumber, + branch => $manager->branchcode + } + ); + $payout1->date( \'NOW() - INTERVAL 13 MINUTE' )->store; + $expected_payout_total += '0.5'; + + # Lost fee of 0.50 fully refunded + unshift @{$expected_payout_grouped}, + { + 'total' => '0.5', + 'credit_type' => { + 'description' => 'A refund applied to a patrons fine' + }, + 'credit_type_code' => 'REFUND' + }; + + $expected_total += $expected_income_total; + $expected_total -= $expected_payout_total; + + diag("Cashup 1"); + my $cashup1 = + $register->add_cashup( { manager_id => $manager->id, amount => '2.00' } ); + + my $summary = $cashup1->summary; + + is( $summary->{from_date}, undef, "from_date is undefined if there is only one recorded" ); + is( $summary->{to_date}, $cashup1->timestamp, "to_date equals cashup timestamp" ); + is( ref( $summary->{income_grouped} ), 'ARRAY', "income_grouped contains an arrayref" ); + is( scalar @{ $summary->{income_grouped} }, 3, "income_grouped contains 3 transactions" ); + is_deeply( $summary->{income_grouped}, $expected_income_grouped, "income_grouped arrayref is correct" ); + is( $summary->{income_total}, $expected_income_total, "income_total is correct" ); + + is( ref( $summary->{payout_grouped} ), 'ARRAY', "payout_grouped contains an arrayref" ); + is( scalar @{ $summary->{payout_grouped} }, 1, "payout_grouped contains 1 transaction" ); + is_deeply( $summary->{payout_grouped}, $expected_payout_grouped, "payout_grouped arrayref is correct" ); + is( $summary->{payout_total}, $expected_payout_total, "payout_total is correct" ); + is( $summary->{total}, $expected_total,"total equals expected_total" ); + + # Backdate cashup1 so we can add a new cashup to check 'previous' + $cashup1->timestamp( \'NOW() - INTERVAL 12 MINUTE' )->store(); + $cashup1->discard_changes; + $expected_total = 0; + $expected_income_total = 0; + $expected_income_grouped = []; + $expected_payout_total = 0; + $expected_payout_grouped = []; + + # Transaction 4 ( Fine (2.75) + Partial payment (-2.00) ) + my $fine2 = $account->add_debit( + { + amount => '2.75', + type => 'OVERDUE', + interface => 'cron' + } + ); + $fine2->date( \'NOW() - INTERVAL 10 MINUTE' )->store; + + my $payment3 = $account->pay( + { + cash_register => $register->id, + amount => '2.00', + credit_type => 'PAYMENT', + lines => [$fine2] + } + ); + $payment3 = Koha::Account::Lines->find( $payment3->{payment_id} ); + $payment3->date( \'NOW() - INTERVAL 10 MINUTE' )->store; + $expected_income_total += '2.00'; + + unshift @{$expected_income_grouped}, + { + debit_type_code => 'OVERDUE', + total => '-2.000000' * -1, + debit_type => { 'description' => 'Overdue fine' } + }; + + $expected_total += $expected_income_total; + $expected_total -= $expected_payout_total; + + diag("Cashup 2"); + my $cashup2 = + $register->add_cashup( { manager_id => $manager->id, amount => '2.00' } ); + + $summary = $cashup2->summary; + + is( $summary->{from_date}, $cashup1->timestamp, "from_date returns the timestamp of the previous cashup cashup" ); + is( $summary->{to_date}, $cashup2->timestamp, "to_date equals cashup timestamp" ); + is( ref( $summary->{income_grouped} ), 'ARRAY', "income_grouped contains Koha::Account::Lines" ); + is( scalar @{ $summary->{income_grouped} }, 1, "income_grouped contains 1 transaction" ); + is_deeply( $summary->{income_grouped}, $expected_income_grouped, "income_grouped arrayref is correct for partial payment" ); + is( ref( $summary->{payout_grouped} ), 'ARRAY', "payout_grouped contains Koha::Account::Lines" ); + is( scalar @{ $summary->{payout_grouped} }, 0, "payout_grouped contains 0 transactions" ); + is_deeply( $summary->{payout_grouped}, $expected_payout_grouped, "payout_grouped arrayref is correct" ); + is( $summary->{total}, $expected_total, "total equals expected_total" ); + + # Backdate cashup2 so we can add a new cashup to check + $cashup2->timestamp( \'NOW() - INTERVAL 6 MINUTE' )->store(); + $cashup2->discard_changes; + $expected_total = 0; + $expected_income_total = 0; + $expected_income_grouped = []; + $expected_payout_total = 0; + $expected_payout_grouped = []; + + # Transaction 5 (Refund (-1) + Payout (1)) + $account1->discard_changes; + my $refund2 = $account1->reduce( + { + amount => '1.00', + reduction_type => 'REFUND', + interface => 'cron' + } + ); + $refund2->date( \'NOW() - INTERVAL 3 MINUTE' )->store; + + my $payout2 = $refund2->payout( + { + cash_register => $register->id, + amount => '1.00', + payout_type => 'CASH', + interface => 'intranet', + staff_id => $manager->borrowernumber, + branch => $manager->branchcode + } + ); + $payout2->date( \'NOW() - INTERVAL 3 MINUTE' )->store; + $expected_payout_total += '1.00'; + + # Account fee of 1.00 fully refunded (Accross cashup boundary) + unshift @{$expected_payout_grouped}, + { + 'total' => '1', + 'credit_type' => { + 'description' => 'A refund applied to a patrons fine' + }, + 'credit_type_code' => 'REFUND' + }; + + $expected_total += $expected_income_total; + $expected_total -= $expected_payout_total; + + diag("Cashup 3"); + my $cashup3 = + $register->add_cashup( { manager_id => $manager->id, amount => '2.00' } ); + + $summary = $cashup3->summary; + + is( $summary->{from_date}, $cashup2->timestamp, "from_date returns the timestamp of the previous cashup cashup" ); + is( $summary->{to_date}, $cashup3->timestamp, "to_date equals cashup timestamp" ); + is( ref( $summary->{income_grouped} ), 'ARRAY', "income_grouped contains Koha::Account::Lines" ); + is( scalar @{ $summary->{income_grouped} }, 0, "income_grouped contains 1 transaction" ); + is_deeply( $summary->{income_grouped}, $expected_income_grouped, "income_grouped arrayref is correct for partial payment" ); + is( ref( $summary->{payout_grouped} ), 'ARRAY', "payout_grouped contains Koha::Account::Lines" ); + is( scalar @{ $summary->{payout_grouped} }, 1, "payout_grouped contains 0 transactions" ); + is_deeply( $summary->{payout_grouped}, $expected_payout_grouped, "payout_grouped arrayref is correct" ); + is( $summary->{total}, $expected_total, "total equals expected_total" ); + + $schema->storage->txn_rollback; +}; + +1; diff --git a/t/db_dependent/api/v1/cashups.t b/t/db_dependent/api/v1/cashups.t new file mode 100644 index 0000000000..4dcb4ae6a6 --- /dev/null +++ b/t/db_dependent/api/v1/cashups.t @@ -0,0 +1,177 @@ +#!/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, see . + +use Modern::Perl; + +use Test::More tests => 2; +use Test::Mojo; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::Cash::Register::Cashups; +use Koha::Cash::Register::Cashups; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +my $t = Test::Mojo->new('Koha::REST::V1'); +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +subtest 'list() tests' => sub { + + plan tests => 17; + + $schema->storage->txn_begin; + + Koha::Cash::Register::Cashups->search->delete; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 25**2 } # cash_management flag = 25 + } + ); + 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 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + ## Authorized user tests + # No cash register, so 404 should be returned + $t->get_ok("//$userid:$password@/api/v1/cash_registers/1/cashups") + ->status_is(404)->json_is( '/error' => 'Register not found' ); + + my $register = $builder->build_object( + { + class => 'Koha::Cash::Registers' + } + ); + my $register_id = $register->id; + + # No cashups, so empty array should be returned + $t->get_ok( + "//$userid:$password@/api/v1/cash_registers/$register_id/cashups") + ->status_is(200)->json_is( [] ); + + my $cashup = $builder->build_object( + { + class => 'Koha::Cash::Register::Cashups', + value => { + register_id => $register->id, + code => 'CASHUP', + timestamp => \'NOW() - INTERVAL 15 MINUTE' + } + } + ); + + # One cashup created, should get returned + $t->get_ok( + "//$userid:$password@/api/v1/cash_registers/$register_id/cashups") + ->status_is(200)->json_is( [ $cashup->to_api ] ); + + my $another_cashup = $builder->build_object( + { + class => 'Koha::Cash::Register::Cashups', + value => { + register_id => $register->id, + code => 'CASHUP', + timestamp => \'NOW()' + } + } + ); + + # One more cashup created, both should be returned + $t->get_ok( + "//$userid:$password@/api/v1/cash_registers/$register_id/cashups") + ->status_is(200) + ->json_is( [ $another_cashup->to_api, $cashup->to_api, ] ); + + # Warn on unsupported query parameter + $t->get_ok( +"//$userid:$password@/api/v1/cash_registers/$register_id/cashups?cashup_blah=blah" + )->status_is(400)->json_is( + [ + { + path => '/query/cashup_blah', + message => 'Malformed query string' + } + ] + ); + + # Unauthorized access + $t->get_ok( + "//$unauth_userid:$password@/api/v1/cash_registers/$register_id/cashups" + )->status_is(403); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $cashup = + $builder->build_object( { class => 'Koha::Cash::Register::Cashups' } ); + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 25**2 } # cash_management flag = 25 + } + ); + 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 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/cashups/" . $cashup->id ) + ->status_is(200)->json_is( $cashup->to_api ); + + $t->get_ok( "//$unauth_userid:$password@/api/v1/cashups/" . $cashup->id ) + ->status_is(403); + + my $cashup_to_delete = + $builder->build_object( { class => 'Koha::Cash::Register::Cashups' } ); + my $non_existent_id = $cashup_to_delete->id; + $cashup_to_delete->delete; + + $t->get_ok("//$userid:$password@/api/v1/cashups/$non_existent_id") + ->status_is(404)->json_is( '/error' => 'Cashup not found' ); + + $schema->storage->txn_rollback; +}; -- 2.20.1