Browse Source

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 <david@davidnind.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
21.05.x
Martin Renvoize 3 years ago
committed by Jonathan Druart
parent
commit
9ba40e1adf
  1. 9
      Koha/Cash/Register.pm
  2. 97
      Koha/Cash/Register/Action.pm
  3. 208
      Koha/Cash/Register/Cashup.pm
  4. 63
      Koha/Cash/Register/Cashups.pm
  5. 99
      Koha/REST/V1/CashRegisters/Cashups.pm
  6. 3
      api/v1/swagger/definitions.json
  7. 30
      api/v1/swagger/definitions/cashup.json
  8. 6
      api/v1/swagger/parameters.json
  9. 9
      api/v1/swagger/parameters/cash_register.json
  10. 9
      api/v1/swagger/parameters/cashup.json
  11. 6
      api/v1/swagger/paths.json
  12. 104
      api/v1/swagger/paths/cash_registers.json
  13. 22
      t/db_dependent/Koha/Cash/Register.t
  14. 269
      t/db_dependent/Koha/Cash/Register/Action.t
  15. 352
      t/db_dependent/Koha/Cash/Register/Cashup.t
  16. 76
      t/db_dependent/Koha/Cash/Register/Cashups.t
  17. 177
      t/db_dependent/api/v1/cashups.t

9
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

97
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

208
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 <http://www.gnu.org/licenses>.
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 <martin.renvoize@ptfs-europe.com>
=cut

63
Koha/Cash/Register/Cashups.pm

@ -0,0 +1,63 @@
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 <http://www.gnu.org/licenses>.
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 ) = @_;
unless ( exists $attr->{order_by} ) {
$attr->{order_by} =
[ { '-asc' => 'register_id' }, { '-desc' => 'timestamp' } ];
}
my $rs = $self->SUPER::search({ code => 'CASHUP' });
return $rs->SUPER::search( $where, $attr );
}
=head3 object_class
=cut
sub object_class {
return 'Koha::Cash::Register::Cashup';
}
1;

99
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 <http://www.gnu.org/licenses>.
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;

3
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"
},

30
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"
}
}
}

6
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",

9
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"
}
}

9
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"
}
}

6
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"
},

104
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"
]
}
}
}

22
t/db_dependent/Koha/Cash/Register.t

@ -176,17 +176,17 @@ subtest 'cashup' => sub {
is(
ref($cashup1),
'Koha::Cash::Register::Action',
'return is Koha::Cash::Register::Action'
'Koha::Cash::Register::Cashup',
'return is Koha::Cash::Register::Cashup'
);
is( $cashup1->code, 'CASHUP',
'CASHUP code set in Koha::Cash::Register::Action' );
'CASHUP code set in Koha::Cash::Register::Cashup' );
is( $cashup1->manager_id, $patron->id,
'manager_id set correctly in Koha::Cash::Register::Action' );
'manager_id set correctly in Koha::Cash::Register::Cashup' );
is( $cashup1->amount, '12.000000',
'amount set correctly in Koha::Cash::Register::Action' );
'amount set correctly in Koha::Cash::Register::Cashup' );
isnt( $cashup1->timestamp, undef,
'timestamp set in Koha::Cash::Register::Action' );
'timestamp set in Koha::Cash::Register::Cashup' );
};
subtest 'last_cashup' => sub {
@ -198,7 +198,7 @@ subtest 'cashup' => sub {
my $last_cashup = $register->last_cashup;
is(
ref($last_cashup),
'Koha::Cash::Register::Action',
'Koha::Cash::Register::Cashup',
'A cashup was returned when one existed'
);
is( $last_cashup->id, $cashup2->id,
@ -213,8 +213,8 @@ subtest 'cashup' => sub {
plan tests => 4;
my $cashups = $register->cashups;
is( ref($cashups), 'Koha::Cash::Register::Actions',
'Koha::Cash::Register->cashups should always return a Koha::Cash::Register::Actions set'
is( ref($cashups), 'Koha::Cash::Register::Cashups',
'Koha::Cash::Register->cashups should always return a Koha::Cash::Register::Cashups set'
);
is( $cashups->count, 0,
'Koha::Cash::Register->cashups should always return the correct number of cashups'
@ -224,8 +224,8 @@ subtest 'cashup' => sub {
$register->add_cashup( { manager_id => $patron->id, amount => '6.00' } );
$cashups = $register->cashups;
is( ref($cashups), 'Koha::Cash::Register::Actions',
'Koha::Cash::Register->cashups should return a Koha::Cash::Register::Actions set'
is( ref($cashups), 'Koha::Cash::Register::Cashups',
'Koha::Cash::Register->cashups should return a Koha::Cash::Register::Cashups set'
);
is( $cashups->count, 1,
'Koha::Cash::Register->cashups should return the correct number of cashups'

269
t/db_dependent/Koha/Cash/Register/Action.t

@ -18,7 +18,7 @@
# along with Koha; if not, see <http://www.gnu.org/licenses>.
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;

352
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 <http://www.gnu.org/licenses>.
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;

76
t/db_dependent/Koha/Cash/Register/Cashups.t

@ -0,0 +1,76 @@
#!/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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use Test::More tests => 1;
use Koha::Database;
use t::lib::TestBuilder;
my $builder = t::lib::TestBuilder->new;
my $schema = Koha::Database->new->schema;
subtest 'search' => sub {
plan tests => 3;
$schema->storage->txn_begin;
my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
my $register =
$builder->build_object( { class => 'Koha::Cash::Registers' } );
my $cashup1 = $builder->build_object(
{
class => 'Koha::Cash::Register::Actions',
value => {
manager_id => $manager->borrowernumber,
register_id => $register->id,
code => 'CASHOUT'
},
}
);
my $cashup2 = $builder->build_object(
{
class => 'Koha::Cash::Register::Actions',
value => {
manager_id => $manager->borrowernumber,
register_id => $register->id,
code => 'CASHUP'
},
}
);
my $cashups = Koha::Cash::Register::Cashups->search();
is(
ref($cashups),
'Koha::Cash::Register::Cashups',
'Returns a Koha::Cash::Register::Cashups resultset'
);
is( $cashups->count, 1, 'Returns only CASHUP actions' );
is(
ref( $cashups->next ),
'Koha::Cash::Register::Cashup',
'Result is a Koha::Cash::Register::Cashup object'
);
$schema->storage->txn_rollback;
};
1;

177
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 <http://www.gnu.org/licenses>.
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