From bb5904b11a3904a3c197e2e39eb6e2c36c2b6f9a Mon Sep 17 00:00:00 2001
From: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Date: Mon, 13 Jan 2025 18:12:39 +0000
Subject: [PATCH] Bug 38689: Improve test coverage

This patch drastically enhances the existing tests.  We add tests for
the various codepaths for handling EDI QUOTES including LIN segment
splitting for multi-fund orders.
---
 t/db_dependent/Koha/EDI.t        | 830 ++++++++++++++++++++++---------
 t/edi_testfiles/INVOICE.CEI      |   3 +-
 t/edi_testfiles/QUOTES_BIG.CEQ   |   1 +
 t/edi_testfiles/QUOTES_MULTI.CEQ |   1 +
 4 files changed, 596 insertions(+), 239 deletions(-)
 create mode 100644 t/edi_testfiles/QUOTES_BIG.CEQ
 create mode 100644 t/edi_testfiles/QUOTES_MULTI.CEQ

diff --git a/t/db_dependent/Koha/EDI.t b/t/db_dependent/Koha/EDI.t
index e9016014de8..407d55eadb6 100755
--- a/t/db_dependent/Koha/EDI.t
+++ b/t/db_dependent/Koha/EDI.t
@@ -20,7 +20,7 @@
 use Modern::Perl;
 use FindBin qw( $Bin );
 
-use Test::More tests => 3;
+use Test::More tests => 2;
 use Test::Warn;
 use Test::MockModule;
 
@@ -30,305 +30,617 @@ use t::lib::TestBuilder;
 
 use Koha::EDI qw(process_quote process_invoice);
 use Koha::Edifact::Transport;
+use Koha::Edifact::File::Errors;
+use Koha::DateUtils qw(dt_from_string);
 
 my $schema  = Koha::Database->new->schema;
 my $builder = t::lib::TestBuilder->new;
 my $logger  = t::lib::Mocks::Logger->new();
 
 subtest 'process_quote' => sub {
-    plan tests => 16;
+    plan tests => 5;
 
     $schema->storage->txn_begin;
 
-    # Add our test quote file to the database for testing against
-    my $account = $builder->build(
-        {
-            source => 'VendorEdiAccount',
-            value  => {
-                description => 'test vendor', transport => 'FILE',
-            }
-        }
-    );
-    my $dirname  = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} );
-    my $filename = 'QUOTES_SMALL.CEQ';
-    ok( -e $dirname . $filename, 'File QUOTES_SMALL.CEQ found' );
-
-    my $trans = Koha::Edifact::Transport->new( $account->{id} );
-    $trans->working_directory($dirname);
-
-    my $mhash = $trans->message_hash();
-    $mhash->{message_type} = 'QUOTE';
-    $trans->ingest( $mhash, $filename );
-
-    my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
-
-    # Test quote expects REF to be a valid and active fund code
+    # Common setup for all test cases
+    my $test_san      = '5013546098818';
+    my $dirname       = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} );
     my $active_period = $builder->build(
         {
             source => 'Aqbudgetperiod',
             value  => { budget_period_active => 1 }
         }
     );
-    my $fund = $builder->build(
-        {
-            source => 'Aqbudget',
-            value  => {
-                budget_code      => 'REF',
-                budget_period_id => $active_period->{budget_period_id}
-            }
-        }
-    );
+    t::lib::Mocks::mock_preference( 'CataloguingLog', 0 );
 
-    # The quote expects a ROT1 stock rotation roata to exist
-    my $rota = $builder->build_object(
-        {
-            class => 'Koha::StockRotationRotas',
-            value => { title => 'ROT1' }
-        }
-    );
-    $builder->build(
-        {
-            source => 'Stockrotationstage',
-            value  => { rota_id => $rota->rota_id },
-        }
-    );
+    # Test 1: Basic Quote Processing
+    subtest 'basic_quote_processing' => sub {
+        plan tests => 27;
+
+        $schema->storage->txn_begin;
+
+        my $account = $builder->build(
+            {
+                source => 'VendorEdiAccount',
+                value  => {
+                    description    => 'test vendor',
+                    transport      => 'FILE',
+                    plugin         => '',
+                    san            => $test_san,
+                    orders_enabled => 1,
+                    auto_orders    => 0,
+                }
+            }
+        );
+        my $ean = $builder->build(
+            {
+                source => 'EdifactEan',
+                value  => {
+                    description => 'test ean',
+                    branchcode  => undef,
+                    ean         => $test_san
+                }
+            }
+        );
+
+        my $filename = 'QUOTES_SMALL.CEQ';
+        ok( -e $dirname . $filename, 'File QUOTES_SMALL.CEQ found' );
+
+        # Setup the fund code
+        my $fund = $builder->build(
+            {
+                source => 'Aqbudget',
+                value  => {
+                    budget_code      => 'REF',
+                    budget_period_id => $active_period->{budget_period_id}
+                }
+            }
+        );
 
-    t::lib::Mocks::mock_preference( 'CataloguingLog', 0 );
+        # Setup stock rotation
+        my $rota = $builder->build_object(
+            {
+                class => 'Koha::StockRotationRotas',
+                value => { title => 'ROT1' }
+            }
+        );
+        $builder->build(
+            {
+                source => 'Stockrotationstage',
+                value  => { rota_id => $rota->rota_id },
+            }
+        );
+
+        my $trans = Koha::Edifact::Transport->new( $account->{id} );
+        $trans->working_directory($dirname);
+
+        my $mhash = $trans->message_hash();
+        $mhash->{message_type} = 'QUOTE';
+        $trans->ingest( $mhash, $filename );
+
+        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
+
+        ok( $quote, 'Quote message created in database' );
+        is( $quote->status, 'new', 'Initial quote status is new' );
+
+        t::lib::Mocks::mock_preference( 'AcqCreateItem', 'ordering' );
+
+        # Process quote and check results
+        my $die;
+        eval {
+            process_quote($quote);
+            1;
+        } or do {
+            $die = $@;
+        };
+        ok( !$die, 'Basic quote processed without dying' );
+
+        # Test for expected logs for the passed quote file
+        is( $logger->count, 8, "8 log lines recorded for passed quote file" );
+
+        #$logger->diag();
+        $logger->trace_like(
+            qr/Created basket:.*/,
+            "Trace recorded adding basket"
+        )->trace_like(
+            qr/Checking db for matches.*/,
+            "Trace recorded checking db for matches"
+        )->trace_like(
+            qr/Added biblio:.*/,
+            "Trace recoded adding new biblio"
+        )->trace_like(
+            qr/Order created:.*/,
+            "Trace recorded adding order"
+        )->trace_like(
+            qr/Added item:.*/,
+            "Trace recorded adding new item"
+        )->trace_like(
+            qr/Item added to rota.*/,
+            "Trace recrded adding item to rota"
+        )->debug_like(
+            qr/.*specialUpdate biblioserver$/,
+            "Trace recorded biblioserver index"
+        )->clear();
+
+        # No errors expected for QUOTE_SMALL
+        my $errors = Koha::Edifact::File::Errors->search();
+        is( $errors->count, 0, '0 errors recorded for simple quote' );
+
+        # Status changed to received
+        $quote->get_from_storage;
+        is( $quote->status, 'received', 'Quote status set to received' );
+
+        # Generate basket
+        my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
+        is( $baskets->count, 1, "One basket created for single message quote file" );
+
+        my $basket = $baskets->next;
+        is( $basket->basketname, $filename, "Basket uses EDI filename for name" );
+
+        # Order lines
+        my $orders = $basket->orders;
+        is( $orders->count, 1, "One order line created for single record quote file" );
+
+        my $order = $orders->next;
+        ok( $order->biblionumber, 'Biblionumber assigned to order' );
+        is( $order->entrydate, dt_from_string()->ymd(), 'Entry date set correctly' );
+
+        # Fund allocation
+        $fund = $order->fund;
+        ok( $fund, 'Fund allocated to order' );
+        is( $fund->budget_code, 'REF', 'Correct fund allocated for order' );
+
+        # Test 008 handling
+        my $biblio       = $order->biblio;
+        my $record       = $biblio->record;
+        my $record_field = $record->field('008');
+        is( exists( $record_field->{_data} ), 1, '008 field added when missing from quote' );
+
+        # Item allocation
+        my $items = $order->items;
+        is(
+            $items->count, 1,
+            "One item created when AcqCreateItem is set to 'ordering' for single record, single quantity quote file"
+        );
+
+        # Test stock rotation handling
+        my $item    = $items->next;
+        my $on_rota = Koha::StockRotationItems->search( { itemnumber_id => $item->itemnumber } );
+        is( $on_rota->count,                 1,         "Item added to rotation" );
+        is( $on_rota->next->stage->rota->id, $rota->id, "Correct rotation assigned" );
+
+        # Confirm no ORDER message generated for 'auto_orders => 0'
+        is( $basket->closedate, undef, 'Basket left open for auto_orders disabled' );
+        my $edi_orders = $schema->resultset('EdifactMessage')->search(
+            {
+                message_type => 'ORDERS',
+                basketno     => $basket->basketno,
+            }
+        );
+        is( $edi_orders->count, 0, 'ORDER message not created for auto_orders disabled' );
 
-    # Process the test quote file
-    my $error;
-    eval {
-        process_quote($quote);
-        1;
-    } or do {
-        $error = $@;
+        $logger->clear();
+        $schema->storage->txn_rollback;
     };
-    ok( !$error, 'process_quote completed without dying' );
-
-    # Test for expected warnings for the passed quote file
-    is( $logger->count, 8, "8 log lines recorded for passed quote file" );
 
-    #$logger->diag();
-    $logger->trace_like(
-        qr/Created basket:.*/,
-        "Trace recorded adding basket"
-    )->trace_like(
-        qr/Checking db for matches.*/,
-        "Trace recorded checking db for matches"
-    )->trace_like(
-        qr/New biblio added.*/,
-        "Trace recoded adding new biblio"
-    )->trace_like(
-        qr/Order created:.*/,
-        "Trace recorded adding order"
-    )->trace_like(
-        qr/Added item:.*/,
-        "Trace recorded adding new item"
-    )->trace_like(
-        qr/Item added to rota.*/,
-        "Trace recrded adding item to rota"
-    )->debug_like(
-        qr/.*specialUpdate biblioserver$/,
-        "Trace recorded biblioserver index"
-    )->clear();
+    # Test 2: Auto Orders Processing
+    subtest 'auto_orders_processing' => sub {
+        plan tests => 7;
+
+        $schema->storage->txn_begin;
+        my $account = $builder->build(
+            {
+                source => 'VendorEdiAccount',
+                value  => {
+                    description    => 'auto order vendor',
+                    transport      => 'FILE',
+                    plugin         => '',
+                    san            => $test_san,
+                    orders_enabled => 1,
+                    auto_orders    => 1,
+                }
+            }
+        );
+        my $ean = $builder->build(
+            {
+                source => 'EdifactEan',
+                value  => {
+                    description => 'test ean',
+                    branchcode  => undef,
+                    ean         => $test_san
+                }
+            }
+        );
+
+        # Setup the fund
+        $builder->build(
+            {
+                source => 'Aqbudget',
+                value  => {
+                    budget_code      => 'REF',
+                    budget_period_id => $active_period->{budget_period_id}
+                }
+            }
+        );
 
-    # Test for quote status change
-    $quote->get_from_storage;
-    is( $quote->status, 'received', 'Quote status was set to received' );
+        my $filename = 'QUOTES_SMALL.CEQ';
+        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
+        $trans->working_directory($dirname);
 
-    # Tests for generated basket for passed quote file
-    my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
-    is( $baskets->count, 1, "1 basket created for a single quote file" );
-    my $basket = $baskets->next;
+        my $mhash = $trans->message_hash();
+        $mhash->{message_type} = 'QUOTE';
+        $trans->ingest( $mhash, $filename );
 
-    my $orders = $basket->orders;
-    is( $orders->count, 1, "1 order line attached to basket when only 1 order is in the edi message" );
-    my $order = $orders->next;
+        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
 
-    my $biblio = $order->biblio;
+        # Process quote and check results
+        my $die;
+        eval {
+            process_quote($quote);
+            1;
+        } or do {
+            $die = $@;
+        };
+        ok( !$die, 'Basic quote processed, with auto_orders enabled, without dying' );
 
-    my $items = $order->items;
-    is( $items->count, 1, "1 item added when AcqCreateItem eq ordering and 1 item is in the EDI quote" );
-    my $item = $items->next;
+        my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
+        is( $baskets->count, 1, "Basket created" );
 
-    # Test that item is added to rota appropriately
-    my $on_rota = Koha::StockRotationItems->search( { itemnumber_id => $item->itemnumber } );
-    is( $on_rota->count, 1, "Item added to stockrotation rota" );
+        my $basket = $baskets->next;
+        ok( $basket->closedate, 'Basket automatically closed for auto_orders' );
 
-    my $rota_item = $on_rota->next;
-    is( $rota_item->stage->rota->id, $rota->id, "Item is on correct rota" );
+        my $orders = $basket->orders;
+        is( $orders->count, 1, "One order created" );
 
-    $schema->storage->txn_rollback;
-};
+        my $order = $orders->next;
+        ok( $order->biblionumber, 'Biblionumber assigned to order' );
 
-subtest '_handle_008_field' => sub {
-    plan tests => 4;
+        # Check EDI order generation
+        my $edi_orders = $schema->resultset('EdifactMessage')->search(
+            {
+                message_type => 'ORDERS',
+                basketno     => $basket->basketno,
+            }
+        );
+        is( $edi_orders->count,        1,         'EDI order message created' );
+        is( $edi_orders->next->status, 'Pending', 'EDI order status is Pending' );
 
-    $schema->storage->txn_begin;
+        $logger->clear();
+        $schema->storage->txn_rollback;
+    };
 
-    # Add our test quote file to the database for testing against
-    my $account = $builder->build(
-        {
-            source => 'VendorEdiAccount',
-            value  => {
-                description => 'test vendor', transport => 'FILE',
+    # Test 3: Multiple item quote
+    subtest 'multi-item quote' => sub {
+        plan tests => 18;
+
+        $schema->storage->txn_begin;
+
+        # Create vendor EDI account
+        my $account = $builder->build(
+            {
+                source => 'VendorEdiAccount',
+                value  => {
+                    description => 'multi-item vendor',
+                    transport   => 'FILE',
+                    plugin      => '',
+                }
+            }
+        );
+        my $ean = $builder->build(
+            {
+                source => 'EdifactEan',
+                value  => {
+                    description => 'test ean',
+                    branchcode  => undef,
+                    ean         => $test_san
+                }
             }
+        );
+
+        # Setup multiple funds
+        my %funds;
+        for my $code (qw(REF LOAN)) {
+            $funds{$code} = $builder->build(
+                {
+                    source => 'Aqbudget',
+                    value  => {
+                        budget_code      => $code,
+                        budget_period_id => $active_period->{budget_period_id}
+                    }
+                }
+            );
         }
-    );
-    my $dirname  = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} );
-    my $filename = 'QUOTES_SMALL.CEQ';
-    ok( -e $dirname . $filename, 'File QUOTES_SMALL.CEQ found' );
 
-    my $trans = Koha::Edifact::Transport->new( $account->{id} );
-    $trans->working_directory($dirname);
+        # Setup multiple rota
+        my %rota;
+        for my $title (qw(TEST FAST SLOW)) {
+            $rota{$title} = $builder->build_object(
+                {
+                    class => 'Koha::StockRotationRotas',
+                    value => { title => $title }
+                }
+            );
+            $builder->build(
+                {
+                    source => 'Stockrotationstage',
+                    value  => { rota_id => $rota{$title}->rota_id },
+                }
+            );
+        }
 
-    my $mhash = $trans->message_hash();
-    $mhash->{message_type} = 'QUOTE';
-    $trans->ingest( $mhash, $filename );
+        my $description = <<~"END";
+            Loaded QUOTE file with 1 Message that contains 2 LIN segments with the first
+            consistent of 1 item and the second with 3 items with 2 different funds.
+        END
+        diag($description);
 
-    my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
+        my $filename = 'QUOTES_MULTI.CEQ';
+        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
+        $trans->working_directory($dirname);
+        my $mhash = $trans->message_hash();
+        $mhash->{message_type} = 'QUOTE';
+        $trans->ingest( $mhash, $filename );
 
-    # Test quote expects REF to be a valid and active fund code
-    my $active_period = $builder->build(
-        {
-            source => 'Aqbudgetperiod',
-            value  => { budget_period_active => 1 }
-        }
-    );
-    my $fund = $builder->build(
-        {
-            source => 'Aqbudget',
-            value  => {
-                budget_code      => 'REF',
-                budget_period_id => $active_period->{budget_period_id}
+        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
+        process_quote($quote);
+
+        #$logger->diag();
+
+        my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
+        is( $baskets->count, 1, "One basket created for quote" );
+
+        my $basket = $baskets->next;
+        my $orders = $basket->orders;
+        is( $orders->count, 3, "Three order lines created for quote" );
+
+        my %biblios;
+        my $orderline = 0;
+        while ( my $order = $orders->next ) {
+            $orderline++;
+            diag( "Looking at order: " . $orderline );
+            if ( $orderline == 1 ) {
+
+                # Fund allocation
+                my $fund = $order->fund;
+                is( $fund->budget_code, 'REF', 'Correct fund allocated for first orderline' );
+
+                # Check biblio
+                ok( $order->biblionumber, 'Biblionumber assigned to order' );
+                $biblios{ $order->biblionumber }++;
+
+                # Check items created
+                my $items = $order->items;
+                is( $items->count, 1, 'One item created for the first orderline' );
+
+                # Check first order GIR details
+                my $item = $items->next;
+                my $rota = $item->stockrotationitem;
+                ok( $rota, 'Item was assigned to a rota' );
+                is( $rota->stage->rota->title, 'TEST', "Item was assigned to the correct rota" );
+
+            } elsif ( $orderline == 2 ) {
+
+                # Fund allocation
+                my $fund = $order->fund;
+                is( $fund->budget_code, 'LOAN', 'Correct fund allocated for second orderline' );
+
+                # Check biblio
+                ok( $order->biblionumber, 'Biblionumber assigned to order' );
+                $biblios{ $order->biblionumber }++;
+
+                # Check items created
+                my $items = $order->items;
+                is( $items->count, 2, 'Two items created for the second orderline' );
+
+                # Check second order GIR details
+                my %rotas;
+                while ( my $item = $items->next ) {
+                    my $rota = $item->stockrotationitem;
+                    ok( $rota, 'Item was assigned to a rota' );
+                    $rotas{ $rota->stage->rota->title }++;
+                }
+                is( $rotas{'FAST'}, 1, "One item added to 'FAST' rota" );
+                is( $rotas{'SLOW'}, 1, "One item added to 'SLOW' rota" );
+            } elsif ( $orderline == 3 ) {
+                diag("Second LIN split into 2 Orderlines, one for each Fund");
+
+                # Fund allocation
+                my $fund = $order->fund;
+                is( $fund->budget_code, 'REF', 'Correct fund allocated for third orderline' );
+
+                # Check biblio
+                ok( $order->biblionumber, 'Biblionumber assigned to order' );
+                $biblios{ $order->biblionumber }++;
+
+                # Check items created
+                my $items = $order->items;
+                is( $items->count, 1, 'One item created for the third orderline' );
+
+                # Check first order GIR details
+                my $item = $items->next;
+                my $rota = $item->stockrotationitem;
+                ok( !$rota, 'Item was not assigned to a rota' );
             }
         }
-    );
 
-    # The quote expects a ROT1 stock rotation roata to exist
-    my $rota = $builder->build_object(
-        {
-            class => 'Koha::StockRotationRotas',
-            value => { title => 'ROT1' }
-        }
-    );
-    $builder->build(
-        {
-            source => 'Stockrotationstage',
-            value  => { rota_id => $rota->rota_id },
-        }
-    );
+        $logger->clear();
+        $schema->storage->txn_rollback;
+    };
 
-    # Process the test quote file
-    process_quote($quote);
+    # Test 4: Invalid Fund Error Handling
+    subtest 'invalid_fund_handling' => sub {
+        plan tests => 21;
 
-    $quote->get_from_storage;
+        $schema->storage->txn_begin;
 
-    # Tests for generated basket for passed quote file
-    my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
-    my $basket  = $baskets->next;
+        my $account = $builder->build(
+            {
+                source => 'VendorEdiAccount',
+                value  => {
+                    description => 'error test vendor',
+                    transport   => 'FILE',
+                }
+            }
+        );
+        my $ean = $builder->build(
+            {
+                source => 'EdifactEan',
+                value  => {
+                    description => 'test ean',
+                    branchcode  => undef,
+                    ean         => $test_san
+                }
+            }
+        );
 
-    my $orders = $basket->orders;
-    my $order  = $orders->next;
+        my $description = <<~"END";
+            Loading QUOTE file with 1 Message that contains 2 LIN segments with the first
+            consistent of 1 item and the second with 3 items with 2 different undefined funds.
+        END
+        diag($description);
 
-    my $biblio       = $order->biblio;
-    my $record       = $biblio->record;
-    my $record_field = $record->field('008');
+        my $filename = 'QUOTES_MULTI.CEQ';
+        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
+        $trans->working_directory($dirname);
 
-    is( exists( $record_field->{_data} ), 1, 'Field has been added' );
+        my $mhash = $trans->message_hash();
+        $mhash->{message_type} = 'QUOTE';
+        $trans->ingest( $mhash, $filename );
 
-    # Test without calling the 008 handler
-    $account = $builder->build(
-        {
-            source => 'VendorEdiAccount',
-            value  => {
-                description => 'test vendor', transport => 'FILE',
-            }
-        }
-    );
-    $dirname  = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} );
-    $filename = 'QUOTES_SMALL_2.CEQ';
-    ok( -e $dirname . $filename, 'File QUOTES_SMALL_2.CEQ found' );
+        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
 
-    $trans = Koha::Edifact::Transport->new( $account->{id} );
-    $trans->working_directory($dirname);
+        process_quote($quote);
 
-    $mhash = $trans->message_hash();
-    $mhash->{message_type} = 'QUOTE';
-    $trans->ingest( $mhash, $filename );
+        #$logger->diag();
+        $logger->trace_like(
+            qr/Created basket:.*/,
+            "Trace recorded basket added"
+        )->trace_is(
+            qq/Checking db for matches with 9781529923766/,
+            "Trace recorded isbn lookup"
+        )->trace_like(
+            qr/Added biblio:.*/,
+            "Trace recorded adding new biblio"
+        )->trace_is(
+            qq/Skipping orderline with invalid budget: REF/,
+            "Trace recorded skipping orderline with invalid fund"
+        )->trace_is(
+            qq/Checking db for matches with 9781785044342/,
+            "Trace recorded isbn lookup"
+        )->trace_like(
+            qr/Added biblio:.*/,
+            "Trace recorded adding new biblio"
+        )->trace_is(
+            qq/Skipping item with invalid budget: LOAN/,
+            "Trace recorded skipping item with invalid fund"
+        )->trace_is(
+            qq/Skipping item with invalid budget: REF/,
+            "Trace recorded skipping item with invalid fund"
+        )->trace_is(
+            qq/Skipping item with invalid budget: LOAN/,
+            "Trace recorded skipping item with invalid fund"
+        );
+
+        # Errors should be recorded for skipped sections
+        my $errors = Koha::Edifact::File::Errors->search();
+        is( $errors->count, 4, '4 errors recorded for missing funds in quote' );
+
+        my $error = $errors->next;
+        ok( $error->section, 'First error section is present' );
+        is( $error->details, 'Skipped orderline line with invalid budget: REF', 'First error details is correct' );
+
+        $error = $errors->next;
+        ok( $error->section, 'Second error section is present' );
+        is( $error->details, 'Skipped GIR line with invalid budget: LOAN', 'Second error details is correct' );
+
+        $error = $errors->next;
+        ok( $error->section, 'Third error section is present' );
+        is( $error->details, 'Invalid budget REF found', 'Third error details is correct' );
+
+        $error = $errors->next;
+        ok( $error->section, 'Fourth error section is present' );
+        is( $error->details, 'Invalid budget LOAN found', 'Fourth error details is correct' );
+
+        $quote->get_from_storage;
+        is( $quote->status, 'error', 'Quote status set to error for invalid fund' );
+
+        my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
+        is( $baskets->count, 1, 'Basket still created despite errors' );
+
+        my $orders = $baskets->next->orders;
+        is( $orders->count, 0, 'No orders created with invalid fund' );
+
+        $logger->clear();
+        $schema->storage->txn_rollback;
+    };
 
-    $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
+    # Test 5: Multiple message quote file
+    subtest 'multiple_message_handling' => sub {
+        plan tests => 3;
 
-    # Test quote expects REF to be a valid and active fund code
-    $active_period = $builder->build(
-        {
-            source => 'Aqbudgetperiod',
-            value  => { budget_period_active => 1 }
-        }
-    );
-    $fund = $builder->build(
-        {
-            source => 'Aqbudget',
-            value  => {
-                budget_code      => 'REF2',
-                budget_period_id => $active_period->{budget_period_id}
+        $schema->storage->txn_begin;
+
+        my $account = $builder->build(
+            {
+                source => 'VendorEdiAccount',
+                value  => {
+                    description => 'error test vendor',
+                    transport   => 'FILE',
+                }
             }
-        }
-    );
+        );
+        my $ean = $builder->build(
+            {
+                source => 'EdifactEan',
+                value  => {
+                    description => 'test ean',
+                    branchcode  => undef,
+                    ean         => $test_san
+                }
+            }
+        );
 
-    # The quote expects a ROT1 stock rotation roata to exist
-    $rota = $builder->build_object(
-        {
-            class => 'Koha::StockRotationRotas',
-            value => { title => 'ROT2' }
-        }
-    );
-    $builder->build(
-        {
-            source => 'Stockrotationstage',
-            value  => { rota_id => $rota->rota_id },
-        }
-    );
+        my $description = <<~"END";
+            Loading QUOTE file with 2 messages
+        END
+        diag($description);
 
-    # Process the test quote file
-    my $edi_module = Test::MockModule->new('Koha::EDI');
-    $edi_module->mock(
-        '_check_for_existing_bib',
-        sub {
-            my $bib_record = shift;
-            return;
-        }
-    );
-    $edi_module->mock(
-        '_handle_008_field',
-        sub {
-            my $bib_record = shift;
-            return $bib_record;
-        }
-    );
-    process_quote($quote);
+        my $filename = 'QUOTES_BIG.CEQ';
+        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
+        $trans->working_directory($dirname);
+
+        my $mhash = $trans->message_hash();
+        $mhash->{message_type} = 'QUOTE';
+        $trans->ingest( $mhash, $filename );
 
-    $quote->get_from_storage;
+        my $quote = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
 
-    # Tests for generated basket for passed quote file
-    $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
-    $basket  = $baskets->next;
+        process_quote($quote);
 
-    $orders = $basket->orders;
-    $order  = $orders->next;
+        #$logger->diag();
 
-    $biblio       = $order->biblio;
-    $record       = $biblio->record;
-    $record_field = $record->field('008');
+        my $baskets = Koha::Acquisition::Baskets->search( { booksellerid => $account->{vendor_id} } );
+        is( $baskets->count, 2, 'Two baskets added for file containing 2 messages' );
 
-    is( $record_field->{_data}, undef, 'Field has not been added' );
+        # First basket
+        my $basket = $baskets->next;
+        is( $basket->basketname, $filename, "Basket uses EDI filename for name" );
 
+        # Second basket
+        $basket = $baskets->next;
+        is( $basket->basketname, $filename, "Basket uses EDI filename for name" );
+
+        $logger->clear();
+        $schema->storage->txn_rollback;
+    };
+
+    # Clean up
     $logger->clear();
     $schema->storage->txn_rollback;
 };
 
 subtest 'process_invoice' => sub {
-    plan tests => 19;
+    plan tests => 32;
 
     $schema->storage->txn_begin;
 
@@ -392,8 +704,8 @@ subtest 'process_invoice' => sub {
 
     my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
     my $raw_msg         = $invoice_message->raw_msg;
-    $raw_msg =~ s/P12345/$ordernumber1/g;
-    $raw_msg =~ s/P54321/$ordernumber2/g;
+    $raw_msg =~ s/ORDERNUMBER1/$ordernumber1/g;
+    $raw_msg =~ s/ORDERNUMBER2/$ordernumber2/g;
     $invoice_message->update( { raw_msg => $raw_msg } );
 
     # Process the test invoice file
@@ -403,6 +715,7 @@ subtest 'process_invoice' => sub {
         1;
     } or do {
         $error = $@;
+        diag($error);
     };
     ok( !$error, 'process_invoice completed without dying' );
 
@@ -453,6 +766,49 @@ subtest 'process_invoice' => sub {
         'Warn recorded for unmatched item'
     )->clear();
 
+    # Errors should be recorded for skipped sections
+    my $errors = Koha::Edifact::File::Errors->search();
+    is( $errors->count, 6, '6 errors recorded for invoice' );
+
+    my @expected_errors = (
+        {
+            'section' =>
+                "QTY+47:5\nGIR+001+DIT:LLO+34148000123456:LAC+P28837:LCO+DITATB:LFN\nPRI+AAA:9.99\nPRI+AAB:12.99\nMOA+203:49.95\nMOA+52:15.00",
+            'details' => 'Skipped invoice line 1, missing ordernumber'
+        },
+        {
+            'section' =>
+                "QTY+47:5\nGIR+001+HLE:LLO+34148000123457:LAC+P28838:LCO+HLEATB:LFN\nPRI+AAA:9.99\nPRI+AAB:12.99\nMOA+203:49.95\nMOA+52:15.00\nRFF+LI:P28837",
+            'details' => 'Skipped invoice line 2, cannot find order with ordernumber P28837'
+        },
+        {
+            'section' =>
+                "QTY+47:10\nGIR+001+RUN:LLO+34148000123458:LAC+P28839:LCO+RUNATB:LFN\nPRI+AAA:15.00\nPRI+AAB:18.00\nMOA+203:150.00\nMOA+52:30.00\nRFF+LI:$ordernumber1",
+            'details' => "Skipped invoice line 3, cannot find biblio for ordernumber $ordernumber1"
+        },
+        {
+            'section' =>
+                "QTY+47:1\nGIR+001+WID:LLO+34148000123459:LAC+P28840:LCO+WIDATB:LFN\nPRI+AAA:30.00\nPRI+AAB:35.00\nMOA+203:600.00\nMOA+52:5.00\nRFF+LI:$ordernumber2",
+            'details' => 'No matching item found for invoice line 4:0 at branch WID'
+        },
+        {
+            'section' =>
+                "QTY+47:1\nGIR+001+DIT:LLO+34148000123460:LAC+P54322:LCO+DITATB:LFN\nPRI+AAA:5.00\nPRI+AAB:6.00\nMOA+203:5.00\nMOA+52:1.00\nRFF+LI:$ordernumber2",
+            'details' => 'No matching item found for invoice line 5:0 at branch DIT'
+        },
+        {
+            'section' => "NAD+SU+9999999999999",
+            'details' => 'Skipped invoice INV00002 with unmatched vendor san: 9999999999999'
+        }
+    );
+
+    my $index = 0;
+    while ( my $error = $errors->next ) {
+        is( $error->section, $expected_errors[$index]->{section}, "Error $index section is correct" );
+        is( $error->details, $expected_errors[$index]->{details}, "Error $index details is correct" );
+        $index++;
+    }
+
     my $invoice3 = Koha::Acquisition::Invoices->search( { invoicenumber => 'INV00003' }, { rows => 1 } )->single;
     ok( $invoice3, "Invoice added to database" );
     is( $invoice3->booksellerid, $account->{vendor_id}, 'Invoice has test booksellerid' );
diff --git a/t/edi_testfiles/INVOICE.CEI b/t/edi_testfiles/INVOICE.CEI
index e8f8bcce387..f638a0648c8 100644
--- a/t/edi_testfiles/INVOICE.CEI
+++ b/t/edi_testfiles/INVOICE.CEI
@@ -1,2 +1 @@
-UNA:+.? 'UNB+UNOC:3+5013546027173+5013546098818+230101:0000+0000000002'UNH+00003+INVOIC:D:96A:UN'BGM+380+INV00003+9'DTM+137:20231210:102'NAD+BY+12345::9'NAD+SU+5013546027173::9'LIN+1++987654321:EN'QTY+47:5'PRI+AAA:9.99'MOA+203:49.95'LIN+2++987654321:EN'QTY+47:5'PRI+AAA:9.99'MOA+203:49.95'RFF+LI:P28837'LIN+3++999999999:EN'QTY+47:10'PRI+AAA:15.00'MOA+203:150.00'RFF+LI:P12345'LIN+4++123456789:EN'QTY+47:1'PRI+AAA:30.00'MOA+203:600.00'RFF+LI:P54321'LIN+5++987654123:EN'QTY+47:1'PRI+AAA:5.00'MOA+203:5.00'RFF+LI:P54321'UNS+S'CNT+4:4'UNT+15+00003'UNZ+1+0000000002'
-UNA:+.? 'UNB+UNOC:3+9999999999999+5013546098818+230101:0000+0000000001'UNH+00002+INVOIC:D:96A:UN'BGM+380+INV00002+9'DTM+137:20231210:102'NAD+BY+54321::9'NAD+SU+9999999999999::9'UNS+S'CNT+1:1'UNT+11+00002'
+UNA:+.? 'UNB+UNOC:3+5013546027173+5013546098818+230101:0000+0000000002'UNH+00003+INVOIC:D:96A:UN'BGM+380+INV00003+9'DTM+137:20231210:102'NAD+BY+12345::9'NAD+SU+5013546027173::9'LIN+1++987654321:EN'QTY+47:5'GIR+001+DIT:LLO+34148000123456:LAC+P28837:LCO+DITATB:LFN'PRI+AAA:9.99'PRI+AAB:12.99'MOA+203:49.95'MOA+52:15.00'LIN+2++987654321:EN'QTY+47:5'GIR+001+HLE:LLO+34148000123457:LAC+P28838:LCO+HLEATB:LFN'PRI+AAA:9.99'PRI+AAB:12.99'MOA+203:49.95'MOA+52:15.00'RFF+LI:P28837'LIN+3++999999999:EN'QTY+47:10'GIR+001+RUN:LLO+34148000123458:LAC+P28839:LCO+RUNATB:LFN'PRI+AAA:15.00'PRI+AAB:18.00'MOA+203:150.00'MOA+52:30.00'RFF+LI:ORDERNUMBER1'LIN+4++123456789:EN'QTY+47:1'GIR+001+WID:LLO+34148000123459:LAC+P28840:LCO+WIDATB:LFN'PRI+AAA:30.00'PRI+AAB:35.00'MOA+203:600.00'MOA+52:5.00'RFF+LI:ORDERNUMBER2'LIN+5++987654123:EN'QTY+47:1'GIR+001+DIT:LLO+34148000123460:LAC+P54322:LCO+DITATB:LFN'PRI+AAA:5.00'PRI+AAB:6.00'MOA+203:5.00'MOA+52:1.00'RFF+LI:ORDERNUMBER2'UNS+S'CNT+4:4'MOA+79:854.90'MOA+129:854.90'MOA+122:854.90'UNT+15+00003'UNZ+1+0000000002''UNA:+.? 'UNB+UNOC:3+9999999999999+5013546098818+230101:0000+0000000001'UNH+00002+INVOIC:D:96A:UN'BGM+380+INV00002+9'DTM+137:20231210:102'NAD+BY+54321::9'NAD+SU+9999999999999::9'UNS+S'CNT+1:1'UNT+11+00002'''
\ No newline at end of file
diff --git a/t/edi_testfiles/QUOTES_BIG.CEQ b/t/edi_testfiles/QUOTES_BIG.CEQ
new file mode 100644
index 00000000000..ed5ac665286
--- /dev/null
+++ b/t/edi_testfiles/QUOTES_BIG.CEQ
@@ -0,0 +1 @@
+UNA:+.? 'UNB+UNOC:3+5013546027173+5013546098818+240123:0945+1044416+ASKEDI:+QUOTES++++'UNH+1044416001+QUOTES:D:96A:UN:EAN002'BGM+31C::28+WO0800131+9'DTM+137:20240123:102'RFF+ON:orders 23/1'CUX+2:GBP:12'NAD+BY+5013546098818::9'NAD+SU+5013546027173::9'LIN+1++9781529923766:EN'PIA+5+152992376X:IB'IMD+L+010+:::Thompson'IMD+L+011+:::Louise'IMD+L+050+:::Lucky'IMD+L+060+:::learning to live again'IMD+L+120+:::Ebury Spotlight'IMD+L+170+:::2024'IMD+L+180+:::304'IMD+L+220+:::Hbk'IMD+L+230+:::791.45'IMD+L+250+:::AN'QTY+1:1'GIR+001+BOOK:LST+ANF:LSQ+CPL:LLO+REF:LFN+791.45:LCL'GIR+001+22.00:LCV'GIR+001+ROT1:LRP'PRI+AAE:22.00'RFF+QLI:WO0800131000001'UNS+S'CNT+2:1'UNT+27+1044416001'UNZ+1+1044416'UNA:+.? 'UNB+UNOC:3+5013546027173+5013546098818+230713:1031+1044417+ASKEDI+QUOTES++++'UNH+1+QUOTES:D:96A:UN:EAN002'BGM+31C::28+WO0800132+9'DTM+137:20230713:102'NAD+BY+5412345000013::9'NAD+SU+4012345000094::9'CUX+2:GBP:9'LIN+1++9781529923766:EN'PIA+1+152992376X:SA'IMD+L+010+:::Thompson'IMD+L+011+:::Louise'IMD+L+050+:::Lucky'IMD+L+060+:::Learning to live again'GIR+001+BOOK:LST+ANF:LSQ+CPL:LLO+REF:LFN+791.45:LCL'GIR+001+22.00:LCV'GIR+001+TEST:LRP'QTY+21:1:PCE'MOA+203:25.99'RFF+ABO:REF'LIN+2++9781785044342:EN'PIA+5+1785044346:IB'IMD+L+010+:::Hansen'IMD+L+011+:::Anders'IMD+L+050+:::The attention fix'IMD+L+060+:::how to focus in a world that wants to distract you'QTY+1:3'GIR+001+BOOK:LST+ANF:LSQ+CPL:LLO+LOAN:LFN+153.733:LCL'GIR+001+14.99:LCV'GIR+001+FAST:LRP'GIR+002+BOOK:LST+ANF:LSQ+FFL:LLO+REF:LFN+153.733:LCL'GIR+002+14.99:LCV'GIR+003+BOOK:LST+ANF:LSQ+FPL:LLO+LOAN:LFN+153.733:LCL'GIR+003+14.99:LCV'GIR+003+SLOW:LRP'PRI+AAE:14.99'RFF+QLI:WO0774049000002'UNS+S'MOA+86:87.97'UNT+34+1'UNZ+1+1'
\ No newline at end of file
diff --git a/t/edi_testfiles/QUOTES_MULTI.CEQ b/t/edi_testfiles/QUOTES_MULTI.CEQ
new file mode 100644
index 00000000000..b1d7e5b67a4
--- /dev/null
+++ b/t/edi_testfiles/QUOTES_MULTI.CEQ
@@ -0,0 +1 @@
+UNA:+.? 'UNB+UNOC:3+5013546027173+5013546098818+230713:1031+1044417+ASKEDI+QUOTES++++'UNH+1+QUOTES:D:96A:UN:EAN002'BGM+31C::28+WO0800132+9'DTM+137:20230713:102'NAD+BY+5412345000013::9'NAD+SU+4012345000094::9'CUX+2:GBP:9'LIN+1++9781529923766:EN'PIA+1+152992376X:SA'IMD+L+010+:::Thompson'IMD+L+011+:::Louise'IMD+L+050+:::Lucky'IMD+L+060+:::Learning to live again'GIR+001+BOOK:LST+ANF:LSQ+CPL:LLO+REF:LFN+791.45:LCL'GIR+001+22.00:LCV'GIR+001+TEST:LRP'QTY+21:1:PCE'MOA+203:25.99'RFF+ABO:REF'LIN+2++9781785044342:EN'PIA+5+1785044346:IB'IMD+L+010+:::Hansen'IMD+L+011+:::Anders'IMD+L+050+:::The attention fix'IMD+L+060+:::how to focus in a world that wants to distract you'QTY+1:3'GIR+001+BOOK:LST+ANF:LSQ+CPL:LLO+LOAN:LFN+153.733:LCL'GIR+001+14.99:LCV'GIR+001+FAST:LRP'GIR+002+BOOK:LST+ANF:LSQ+FFL:LLO+REF:LFN+153.733:LCL'GIR+002+14.99:LCV'GIR+003+BOOK:LST+ANF:LSQ+FPL:LLO+LOAN:LFN+153.733:LCL'GIR+003+14.99:LCV'GIR+003+SLOW:LRP'PRI+AAE:14.99'RFF+QLI:WO0774049000002'UNS+S'MOA+86:87.97'UNT+34+1'UNZ+1+1'
\ No newline at end of file
-- 
2.48.1