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>
master
17 changed files with 1159 additions and 380 deletions
  1. +5
    -4
      Koha/Cash/Register.pm
  2. +0
    -97
      Koha/Cash/Register/Action.pm
  3. +208
    -0
      Koha/Cash/Register/Cashup.pm
  4. +63
    -0
      Koha/Cash/Register/Cashups.pm
  5. +99
    -0
      Koha/REST/V1/CashRegisters/Cashups.pm
  6. +3
    -0
      api/v1/swagger/definitions.json
  7. +30
    -0
      api/v1/swagger/definitions/cashup.json
  8. +6
    -0
      api/v1/swagger/parameters.json
  9. +9
    -0
      api/v1/swagger/parameters/cash_register.json
  10. +9
    -0
      api/v1/swagger/parameters/cashup.json
  11. +6
    -0
      api/v1/swagger/paths.json
  12. +104
    -0
      api/v1/swagger/paths/cash_registers.json
  13. +11
    -11
      t/db_dependent/Koha/Cash/Register.t
  14. +1
    -268
      t/db_dependent/Koha/Cash/Register/Action.t
  15. +352
    -0
      t/db_dependent/Koha/Cash/Register/Cashup.t
  16. +76
    -0
      t/db_dependent/Koha/Cash/Register/Cashups.t
  17. +177
    -0
      t/db_dependent/api/v1/cashups.t

+ 5
- 4
Koha/Cash/Register.pm View File

@@ -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


+ 0
- 97
Koha/Cash/Register/Action.pm View File

@@ -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
- 0
Koha/Cash/Register/Cashup.pm View File

@@ -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
- 0
Koha/Cash/Register/Cashups.pm View File

@@ -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
- 0
Koha/REST/V1/CashRegisters/Cashups.pm View File

@@ -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
- 0
api/v1/swagger/definitions.json View File

@@ -5,6 +5,9 @@
"basket": {
"$ref": "definitions/basket.json"
},
"cashup": {
"$ref": "definitions/cashup.json"
},
"circ-rule-kind": {
"$ref": "definitions/circ-rule-kind.json"
},


+ 30
- 0
api/v1/swagger/definitions/cashup.json View File

@@ -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
- 0
api/v1/swagger/parameters.json View File

@@ -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
- 0
api/v1/swagger/parameters/cash_register.json View File

@@ -0,0 +1,9 @@
{
"cash_register_id_pp": {
"name": "cash_register_id",
"in": "path",
"description": "Cash register internal identifier",
"required": true,
"type": "integer"
}
}

+ 9
- 0
api/v1/swagger/parameters/cashup.json View File

@@ -0,0 +1,9 @@
{
"cashup_id_pp": {
"name": "cashup_id",
"in": "path",
"description": "Cashup internal identifier",
"required": true,
"type": "integer"
}
}

+ 6
- 0
api/v1/swagger/paths.json View File

@@ -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
- 0
api/v1/swagger/paths/cash_registers.json View File

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

+ 11
- 11
t/db_dependent/Koha/Cash/Register.t View File

@@ -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'


+ 1
- 268
t/db_dependent/Koha/Cash/Register/Action.t View File

@@ -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
- 0
t/db_dependent/Koha/Cash/Register/Cashup.t View File

@@ -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
- 0
t/db_dependent/Koha/Cash/Register/Cashups.t View File

@@ -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
- 0
t/db_dependent/api/v1/cashups.t View File

@@ -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->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;
};

Loading…
Cancel
Save