From b68b347e5a48ace20e11184968d02e79ecbee404 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 8 Aug 2025 13:18:59 +0100 Subject: [PATCH] Bug 40445: Add unit tests for cashup reconciliation functionality This patch adds comprehensive unit tests for the cashup reconciliation system: - Tests for balanced cashups (no surplus/deficit accountlines created) - Tests for surplus scenarios with CASHUP_SURPLUS credit creation - Tests for deficit scenarios with CASHUP_DEFICIT debit creation - Tests for user note handling and storage in reconciliation accountlines - Tests for transaction integrity ensuring atomicity - Tests for note sanitization (whitespace trimming, empty note handling) The tests ensure that: - Reconciliation accountlines are only created when there's a discrepancy - Surplus/deficit amounts are calculated and stored correctly - User notes are properly combined with system reconciliation details - Database transactions maintain consistency - Account types are linked correctly to cash registers --- t/db_dependent/Koha/Cash/Register.t | 435 +++++++++++++++++++++++++++- 1 file changed, 434 insertions(+), 1 deletion(-) diff --git a/t/db_dependent/Koha/Cash/Register.t b/t/db_dependent/Koha/Cash/Register.t index ee4f2fc0652..4267e94babd 100755 --- a/t/db_dependent/Koha/Cash/Register.t +++ b/t/db_dependent/Koha/Cash/Register.t @@ -20,11 +20,14 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 5; +use Test::More tests => 6; use Test::Exception; use Koha::Database; +use Koha::Account; +use Koha::Account::CreditTypes; +use Koha::Account::DebitTypes; use t::lib::TestBuilder; @@ -312,3 +315,433 @@ subtest 'cashup' => sub { $schema->storage->txn_rollback; }; + +subtest 'cashup_reconciliation' => sub { + plan tests => 5; + + $schema->storage->txn_begin; + + # Ensure required account types for reconciliation exist (they should already exist from mandatory data) + use Koha::Account::CreditTypes; + use Koha::Account::DebitTypes; + + my $surplus_credit_type = Koha::Account::CreditTypes->find( { code => 'CASHUP_SURPLUS' } ); + if ( !$surplus_credit_type ) { + $surplus_credit_type = $builder->build_object( + { + class => 'Koha::Account::CreditTypes', + value => { + code => 'CASHUP_SURPLUS', + description => 'Cash register surplus found during cashup', + can_be_added_manually => 0, + credit_number_enabled => 0, + is_system => 1, + archived => 0, + } + } + ); + } + + my $deficit_debit_type = Koha::Account::DebitTypes->find( { code => 'CASHUP_DEFICIT' } ); + if ( !$deficit_debit_type ) { + $deficit_debit_type = $builder->build_object( + { + class => 'Koha::Account::DebitTypes', + value => { + code => 'CASHUP_DEFICIT', + description => 'Cash register deficit found during cashup', + can_be_invoiced => 0, + can_be_sold => 0, + default_amount => undef, + is_system => 1, + archived => 0, + restricts_checkouts => 0, + } + } + ); + } + + my $register = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + # Create some outstanding accountlines to establish expected amount + my $accountline1 = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register->id, + borrowernumber => $patron->id, + amount => -10.00, # Credit (payment) + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + my $accountline2 = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register->id, + borrowernumber => $patron->id, + amount => -5.00, # Credit (payment) + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $expected_amount = $register->outstanding_accountlines->total; # Should be -15.00 + + subtest 'balanced_cashup' => sub { + plan tests => 3; + + # Test exact match - no surplus/deficit accountlines should be created + my $amount = abs($expected_amount); # 15.00 actual matches 15.00 expected + + my $cashup = $register->add_cashup( + { + manager_id => $patron->id, + amount => $amount + } + ); + + ok( $cashup, 'Cashup created successfully' ); + is( sprintf( '%.0f', $cashup->amount ), sprintf( '%.0f', $amount ), 'Cashup amount matches actual amount' ); + + # Check no surplus/deficit accountlines were created + my $reconciliation_lines = Koha::Account::Lines->search( + { + register_id => $register->id, + '-or' => [ + { credit_type_code => 'CASHUP_SURPLUS' }, + { debit_type_code => 'CASHUP_DEFICIT' } + ] + } + ); + + is( $reconciliation_lines->count, 0, 'No reconciliation accountlines created for balanced cashup' ); + }; + + subtest 'surplus_cashup' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + my $register2 = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline3 = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register2->id, + borrowernumber => $patron->id, + amount => -20.00, # Credit (payment) + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $expected = abs( $register2->outstanding_accountlines->total ); # 20.00 + my $actual = 25.00; # 5.00 surplus + my $surplus = $actual - $expected; + + my $cashup = $register2->add_cashup( + { + manager_id => $patron->id, + amount => $actual + } + ); + + ok( $cashup, 'Surplus cashup created successfully' ); + is( sprintf( '%.0f', $cashup->amount ), sprintf( '%.0f', $actual ), 'Cashup amount matches actual amount' ); + + # Check surplus accountline was created + my $surplus_lines = Koha::Account::Lines->search( + { + register_id => $register2->id, + credit_type_code => 'CASHUP_SURPLUS' + } + ); + + is( $surplus_lines->count, 1, 'One surplus accountline created' ); + + my $surplus_line = $surplus_lines->next; + is( + sprintf( '%.0f', $surplus_line->amount ), sprintf( '%.0f', -$surplus ), + 'Surplus amount is correct (negative for credit)' + ); + + # Note should be undef for surplus without user note + is( $surplus_line->note, undef, 'No note for surplus without user reconciliation note' ); + + # Test surplus with user note + my $register_with_note = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline_with_note = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register_with_note->id, + borrowernumber => $patron->id, + amount => -10.00, + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $cashup_with_note = $register_with_note->add_cashup( + { + manager_id => $patron->id, + amount => 15.00, # 5.00 surplus + reconciliation_note => 'Found extra \x{00A3}5 under the till drawer' # £5 in UTF-8 + } + ); + + my $surplus_with_note = Koha::Account::Lines->search( + { + register_id => $register_with_note->id, + credit_type_code => 'CASHUP_SURPLUS' + } + )->next; + + like( + $surplus_with_note->note, qr/Found extra .+5 under the till drawer/, + 'User note included in surplus accountline' + ); + is( + $surplus_with_note->note, 'Found extra \x{00A3}5 under the till drawer', + 'Only user note stored (no base reconciliation info)' + ); + + $schema->storage->txn_rollback; + }; + + subtest 'deficit_cashup' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + my $register3 = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline4 = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register3->id, + borrowernumber => $patron->id, + amount => -30.00, # Credit (payment) + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $expected = abs( $register3->outstanding_accountlines->total ); # 30.00 + my $actual = 25.00; # 5.00 deficit + my $deficit = $expected - $actual; + + my $cashup = $register3->add_cashup( + { + manager_id => $patron->id, + amount => $actual + } + ); + + ok( $cashup, 'Deficit cashup created successfully' ); + is( sprintf( '%.0f', $cashup->amount ), sprintf( '%.0f', $actual ), 'Cashup amount matches actual amount' ); + + # Check deficit accountline was created + my $deficit_lines = Koha::Account::Lines->search( + { + register_id => $register3->id, + debit_type_code => 'CASHUP_DEFICIT' + } + ); + + is( $deficit_lines->count, 1, 'One deficit accountline created' ); + + my $deficit_line = $deficit_lines->next; + is( + sprintf( '%.0f', $deficit_line->amount ), sprintf( '%.0f', $deficit ), + 'Deficit amount is correct (positive for debit)' + ); + + # Note should be undef for deficit without user note + is( $deficit_line->note, undef, 'No note for deficit without user reconciliation note' ); + + # Test deficit with user note + my $register_deficit_note = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline_deficit_note = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register_deficit_note->id, + borrowernumber => $patron->id, + amount => -20.00, + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $cashup_deficit_note = $register_deficit_note->add_cashup( + { + manager_id => $patron->id, + amount => 15.00, # 5.00 deficit + reconciliation_note => 'Till was short, possibly due to incorrect change given' + } + ); + + my $deficit_with_note = Koha::Account::Lines->search( + { + register_id => $register_deficit_note->id, + debit_type_code => 'CASHUP_DEFICIT' + } + )->next; + + like( + $deficit_with_note->note, qr/Till was short, possibly due to incorrect change given/, + 'User note included in deficit accountline' + ); + is( + $deficit_with_note->note, 'Till was short, possibly due to incorrect change given', + 'Only user note stored (no base reconciliation info)' + ); + + $schema->storage->txn_rollback; + }; + + subtest 'transaction_integrity' => sub { + plan tests => 4; + + $schema->storage->txn_begin; + + my $register4 = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline5 = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register4->id, + borrowernumber => $patron->id, + amount => -10.00, + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $initial_accountline_count = Koha::Account::Lines->search( { register_id => $register4->id } )->count; + + my $initial_action_count = $register4->cashups->count; + + # Test successful transaction + my $cashup = $register4->add_cashup( + { + manager_id => $patron->id, + amount => 15.00 # Creates surplus + } + ); + + # Check both cashup action and surplus accountline were created + is( $register4->cashups->count, $initial_action_count + 1, 'Cashup action created' ); + + my $final_accountline_count = Koha::Account::Lines->search( { register_id => $register4->id } )->count; + + is( $final_accountline_count, $initial_accountline_count + 1, 'Surplus accountline created' ); + + # Verify the new accountline is the surplus + my $surplus_line = Koha::Account::Lines->search( + { + register_id => $register4->id, + credit_type_code => 'CASHUP_SURPLUS' + } + )->next; + + ok( $surplus_line, 'Surplus accountline exists' ); + is( $surplus_line->register_id, $register4->id, 'Surplus linked to correct register' ); + + $schema->storage->txn_rollback; + }; + + subtest 'note_handling' => sub { + plan tests => 2; + + $schema->storage->txn_begin; + + my $register_note_test = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline_note_test = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register_note_test->id, + borrowernumber => $patron->id, + amount => -10.00, + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + # Test balanced cashup with note (should not create surplus/deficit) + my $balanced_cashup = $register_note_test->add_cashup( + { + manager_id => $patron->id, + amount => 10.00, # Balanced + reconciliation_note => 'This note should be ignored for balanced cashup' + } + ); + + my $balanced_reconciliation_lines = Koha::Account::Lines->search( + { + register_id => $register_note_test->id, + '-or' => [ + { credit_type_code => 'CASHUP_SURPLUS' }, + { debit_type_code => 'CASHUP_DEFICIT' } + ] + } + ); + + is( + $balanced_reconciliation_lines->count, 0, + 'No reconciliation accountlines created for balanced cashup with note' + ); + + # Test empty/whitespace note handling + my $register_empty_note = $builder->build_object( { class => 'Koha::Cash::Registers' } ); + my $accountline_empty_note = $builder->build_object( + { + class => 'Koha::Account::Lines', + value => { + register_id => $register_empty_note->id, + borrowernumber => $patron->id, + amount => -10.00, + credit_type_code => 'PAYMENT', + debit_type_code => undef, + } + } + ); + + my $empty_note_cashup = $register_empty_note->add_cashup( + { + manager_id => $patron->id, + amount => 12.00, # 2.00 surplus + reconciliation_note => ' ' # Whitespace only + } + ); + + my $empty_note_surplus = Koha::Account::Lines->search( + { + register_id => $register_empty_note->id, + credit_type_code => 'CASHUP_SURPLUS' + } + )->next; + + is( + $empty_note_surplus->note, undef, + 'No note stored when user note is empty/whitespace' + ); + + $schema->storage->txn_rollback; + }; + + $schema->storage->txn_rollback; +}; -- 2.50.1