From 5af35ec54d8e7c62e87968154ed59a4877f36ff5 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Wed, 26 Nov 2025 15:05:49 +0000 Subject: [PATCH] Bug 41297: Block duplicate EDIFACT invoice processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds duplicate invoice detection and blocking for EDIFACT invoice processing, with email notifications to library staff and vendors. Key features: 1. System preferences to enable/disable duplicate blocking and email notifications 2. Database index on aqinvoices (invoicenumber, booksellerid) for performance 3. Email notifications using the message_queue (GetPreparedLetter → EnqueueLetter pattern) 4. Vendor contact-based notifications via a new aqcontacts.edi_error_notification flag 5. Two notice templates (EDI_DUP_INV_LIBRARY and EDI_DUP_INV_VENDOR) Changes: - Add EdiBlockDuplicateInvoice system preference - Add EdiBlockDuplicateInvoiceEmailNotice system preference - Add EdiBlockDuplicateInvoiceEmailAddresses system preference - Add edi_error_notification column to aqcontacts table - Add duplicate invoice detection logic to Koha::EDI::process_invoice - Add email notification support using standard Koha messaging queue - Add "Contact about EDI errors" checkbox to vendor contacts UI - Add notice templates to sample_notices.yml for fresh installations - Add test coverage in t/db_dependent/Koha/EDI.t Test plan: 1. Run database update: perl installer/data/mysql/updatedatabase.pl 2. Enable system preferences: - Set EdiBlockDuplicateInvoice to "Block" - Set EdiBlockDuplicateInvoiceEmailNotice to "Send" - Set EdiBlockDuplicateInvoiceEmailAddresses to valid email(s) 3. Edit a vendor and add a contact with "Contact about EDI errors" enabled 4. Process an EDIFACT invoice 5. Attempt to process the same invoice again - should be blocked 6. Check message_queue table for queued notifications 7. Run tests: prove t/db_dependent/Koha/EDI.t :: duplicate_invoice_blocking Sponsored-by: Westminster City Council Sponsored-by: Royal Borough of Kensington and Chelsea --- Koha/EDI.pm | 197 +++- Koha/Schema/Result/Aqcontact.pm | 21 +- Koha/Schema/Result/VendorEdiAccount.pm | 4 +- .../data/mysql/atomicupdate/bug_40383.pl | 163 +++ .../mysql/en/mandatory/sample_notices.yml | 57 + .../admin/preferences/acquisitions.pref | 17 + .../vue/components/Vendors/VendorContacts.vue | 11 + t/db_dependent/Koha/EDI.t | 1011 ++++++++++++++++- 8 files changed, 1472 insertions(+), 9 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_40383.pl diff --git a/Koha/EDI.pm b/Koha/EDI.pm index 8d0dc84e3c3..e47a5ed3d08 100644 --- a/Koha/EDI.pm +++ b/Koha/EDI.pm @@ -331,6 +331,48 @@ sub process_invoice { next; } $invoice_message->edi_acct( $vendor_acct->id ); + + # Check for duplicate invoices if preference enabled + if ( C4::Context->preference('EdiBlockDuplicateInvoice') ) { + my $duplicate_invoice = $schema->resultset('Aqinvoice')->search( + { + invoicenumber => $invoicenumber, + booksellerid => $invoice_message->vendor_id, + } + )->first; + + if ($duplicate_invoice) { + $logger->error( "Duplicate invoice $invoicenumber for vendor " + . $invoice_message->vendor_id . " in " + . $invoice_message->filename ); + + # Log to edifact_errors table + $invoice_message->add_to_edifact_errors( + { + section => "BGM+" . $invoicenumber, + details => "Duplicate invoice number '$invoicenumber'. " + . "Original invoice ID: " + . $duplicate_invoice->invoiceid . ". " + . "Processing blocked." + } + ); + + # Send email notification if enabled + _send_duplicate_invoice_email_notice( + $invoice_message, + $invoicenumber, + $vendor_acct, + $duplicate_invoice + ); + + # Mark message as error and stop processing this invoice + $invoice_message->status('error'); + $invoice_message->update; + + next; # Skip to next message in transmission + } + } + $logger->trace("Adding invoice: $invoicenumber"); my $new_invoice = $schema->resultset('Aqinvoice')->create( { @@ -470,7 +512,11 @@ sub process_invoice { } } - $invoice_message->status('received'); + # Only set status to 'received' if not already set to 'error' + $invoice_message->discard_changes; + if ( $invoice_message->status ne 'error' ) { + $invoice_message->status('received'); + } $invoice_message->update; # status and basketno link return; } @@ -1400,6 +1446,127 @@ sub _handle_008_field { return $bib_record; } +sub _send_duplicate_invoice_email_notice { + my ( $invoice_message, $invoicenumber, $vendor_acct, $duplicate_invoice ) = @_; + + my $logger = Koha::Logger->get( { interface => 'edi' } ); + + # Check if email notifications enabled + return unless C4::Context->preference('EdiBlockDuplicateInvoiceEmailNotice'); + + # Get vendor information + my $vendor_id = $invoice_message->vendor_id; + + # Prepare template substitution values + my $substitute = { + invoicenumber => $invoicenumber, + vendor_id => $vendor_id, + vendor_san => $vendor_acct ? $vendor_acct->san : '', + filename => $invoice_message->filename, + message_id => $invoice_message->id, + original_invoiceid => $duplicate_invoice->invoiceid, + original_shipmentdate => $duplicate_invoice->shipmentdate || 'N/A', + received_date => DateTime->now->ymd, + }; + + # 1. Send notification to library staff + my $library_email_addresses = C4::Context->preference('EdiBlockDuplicateInvoiceEmailAddresses'); + if ($library_email_addresses) { + my @library_addresses = split /\s*,\s*/, $library_email_addresses; + + foreach my $to_address (@library_addresses) { + $to_address =~ s/^\s+|\s+$//g; # trim whitespace + next unless $to_address; + next unless Koha::Email->is_valid($to_address); + + my $letter = C4::Letters::GetPreparedLetter( + module => 'acquisition', + letter_code => 'EDI_DUP_INV_LIBRARY', + message_transport_type => 'email', + tables => { + aqbooksellers => $vendor_id, + }, + substitute => $substitute, + ); + + if ($letter) { + my $message_id = C4::Letters::EnqueueLetter( + { + letter => $letter, + to_address => $to_address, + message_transport_type => 'email', + } + ); + + if ($message_id) { + $logger->info( + "Library duplicate invoice notification queued (message_id: $message_id) for $to_address, invoice $invoicenumber. Message will be sent by message_queue cronjob." + ); + } else { + $logger->warn("Failed to enqueue library notification to $to_address for invoice $invoicenumber"); + } + } else { + $logger->warn("Could not generate library notification letter for invoice $invoicenumber"); + } + } + } + + # 2. Send notification to vendor contacts + if ($vendor_acct) { + my $schema = Koha::Database->new()->schema(); + my $vendor = $schema->resultset('Aqbookseller')->find($vendor_id); + if ($vendor) { + my @edi_contacts = $vendor->aqcontacts->search( + { + edi_error_notification => 1, + email => { '!=' => undef }, + } + )->all; + + foreach my $contact (@edi_contacts) { + my $vendor_email = $contact->email; + + if ( Koha::Email->is_valid($vendor_email) ) { + my $letter = C4::Letters::GetPreparedLetter( + module => 'acquisition', + letter_code => 'EDI_DUP_INV_VENDOR', + message_transport_type => 'email', + tables => { + aqbooksellers => $vendor_id, + }, + substitute => $substitute, + ); + + if ($letter) { + my $message_id = C4::Letters::EnqueueLetter( + { + letter => $letter, + to_address => $vendor_email, + message_transport_type => 'email', + } + ); + + if ($message_id) { + $logger->info( + "Vendor duplicate invoice notification queued (message_id: $message_id) for $vendor_email (contact: " + . $contact->name + . "), invoice $invoicenumber. Message will be sent by message_queue cronjob." ); + } else { + $logger->warn( + "Failed to enqueue vendor notification to $vendor_email for invoice $invoicenumber"); + } + } else { + $logger->warn("Could not generate vendor notification letter for invoice $invoicenumber"); + } + } else { + $logger->warn( + "Invalid vendor contact email address: $vendor_email for contact: " . $contact->name ); + } + } + } + } +} + 1; __END__ @@ -1515,6 +1682,34 @@ Koha::EDI If all else fails returns empty string +=head2 _send_duplicate_invoice_email_notice + + _send_duplicate_invoice_email_notice($invoice_message, $invoicenumber, $vendor_acct, $duplicate_invoice) + + Internal function to queue email notifications when duplicate EDIFACT invoices are detected. + + Uses the standard Koha messaging pattern: + - GetPreparedLetter: Generates the notice content from templates + - EnqueueLetter: Adds the message to the message_queue table for delivery by the cronjob + + Queues two types of notifications using letter templates: + 1. EDI_DUP_INV_LIBRARY - to library staff (addresses from EdiBlockDuplicateInvoiceEmailAddresses) + 2. EDI_DUP_INV_VENDOR - to vendor (address from vendor_edi_accounts.vendor_email) + + All messages are recorded in the message_queue table for auditing and can be reviewed + in the Koha notices interface. Messages will be sent by the message_queue cronjob + (misc/cronjobs/process_message_queue.pl). + + Only runs if EdiBlockDuplicateInvoiceEmailNotice preference is enabled. + + Parameters: + - $invoice_message: The EdifactMessage object being processed + - $invoicenumber: The duplicate invoice number found + - $vendor_acct: The VendorEdiAccount object + - $duplicate_invoice: The existing Aqinvoice object with the same invoice number + + Returns: nothing + =head2 _create_bib_from_quote marc_record_obj = _create_bib_from_quote(lineitem, quote) diff --git a/Koha/Schema/Result/Aqcontact.pm b/Koha/Schema/Result/Aqcontact.pm index fbb25c59660..9363d938a40 100644 --- a/Koha/Schema/Result/Aqcontact.pm +++ b/Koha/Schema/Result/Aqcontact.pm @@ -126,6 +126,14 @@ is this the primary contact for acquisitions messages is this the primary contact for serials messages +=head2 edi_error_notification + + data_type: 'tinyint' + default_value: 0 + is_nullable: 0 + +should this contact receive EDI error notifications (e.g. duplicate invoices) + =head2 booksellerid data_type: 'integer' @@ -161,6 +169,8 @@ __PACKAGE__->add_columns( { data_type => "tinyint", default_value => 0, is_nullable => 0 }, "serialsprimary", { data_type => "tinyint", default_value => 0, is_nullable => 0 }, + "edi_error_notification", + { data_type => "tinyint", default_value => 0, is_nullable => 0 }, "booksellerid", { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, ); @@ -199,11 +209,12 @@ __PACKAGE__->belongs_to( # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:IefjqDsoXPLWKSfhYGne1A __PACKAGE__->add_columns( - '+orderacquisition' => { is_boolean => 1 }, - '+claimacquisition' => { is_boolean => 1 }, - '+claimissues' => { is_boolean => 1 }, - '+acqprimary' => { is_boolean => 1 }, - '+serialsprimary' => { is_boolean => 1 }, + '+orderacquisition' => { is_boolean => 1 }, + '+claimacquisition' => { is_boolean => 1 }, + '+claimissues' => { is_boolean => 1 }, + '+acqprimary' => { is_boolean => 1 }, + '+serialsprimary' => { is_boolean => 1 }, + '+edi_error_notification' => { is_boolean => 1 }, ); =head2 koha_object_class diff --git a/Koha/Schema/Result/VendorEdiAccount.pm b/Koha/Schema/Result/VendorEdiAccount.pm index e5095af66d2..b9dee7cfd9d 100644 --- a/Koha/Schema/Result/VendorEdiAccount.pm +++ b/Koha/Schema/Result/VendorEdiAccount.pm @@ -248,8 +248,8 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-11-03 20:27:30 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1sHAak6V/HC2E1AUHzDapg +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-11-26 00:00:00 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:original_schema_bug_41297 __PACKAGE__->add_columns( '+auto_orders' => { is_boolean => 1 }, diff --git a/installer/data/mysql/atomicupdate/bug_40383.pl b/installer/data/mysql/atomicupdate/bug_40383.pl new file mode 100644 index 00000000000..a87bca2aba9 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_40383.pl @@ -0,0 +1,163 @@ +use Modern::Perl; + +return { + bug_number => "41297", + description => "Add system preferences for blocking duplicate EDI invoices", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + # Master preference to enable duplicate blocking + $dbh->do( + q{ + INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type) + VALUES ( + 'EdiBlockDuplicateInvoice', + '0', + NULL, + 'Block processing of EDIFACT invoices when a duplicate invoice number is detected for the same supplier. When enabled, duplicate invoices will be rejected and logged as errors.', + 'YesNo' + ) + } + ); + say $out "Added system preference 'EdiBlockDuplicateInvoice'"; + + # Email notification toggle + $dbh->do( + q{ + INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type) + VALUES ( + 'EdiBlockDuplicateInvoiceEmailNotice', + '0', + NULL, + 'Send email notification when duplicate EDIFACT invoices are detected. Requires EdiBlockDuplicateInvoice to be enabled.', + 'YesNo' + ) + } + ); + say $out "Added system preference 'EdiBlockDuplicateInvoiceEmailNotice'"; + + # Email recipient list + $dbh->do( + q{ + INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type) + VALUES ( + 'EdiBlockDuplicateInvoiceEmailAddresses', + '', + NULL, + 'Comma-separated list of email addresses to notify when duplicate EDIFACT invoices are detected (e.g., "purchasing@library.org,edi_support@library.org"). Requires EdiBlockDuplicateInvoiceEmailNotice to be enabled.', + 'Textarea' + ) + } + ); + say $out "Added system preference 'EdiBlockDuplicateInvoiceEmailAddresses'"; + + # Add database index for performance + my $index_exists = $dbh->selectrow_array( + q{ + SELECT COUNT(*) + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'aqinvoices' + AND index_name = 'idx_invoicenumber_booksellerid' + } + ); + + unless ($index_exists) { + $dbh->do( + q{ + CREATE INDEX idx_invoicenumber_booksellerid + ON aqinvoices (invoicenumber(100), booksellerid) + } + ); + say $out "Added index idx_invoicenumber_booksellerid to aqinvoices table"; + } + + # Add edi_error_notification column to aqcontacts + unless ( column_exists( 'aqcontacts', 'edi_error_notification' ) ) { + $dbh->do( + q{ + ALTER TABLE aqcontacts + ADD COLUMN edi_error_notification TINYINT(1) NOT NULL DEFAULT 0 + AFTER serialsprimary + } + ); + say $out "Added edi_error_notification column to aqcontacts table"; + } + + # Delete any truncated templates first + $dbh->do(q{DELETE FROM letter WHERE code = 'EDI_DUPLICATE_INVOIC' AND module = 'acquisition'}); + + # Add notice templates for duplicate invoice notifications + $dbh->do( + q{ + INSERT IGNORE INTO letter (module, code, branchcode, name, is_html, title, content, message_transport_type, lang) + VALUES ( + 'acquisition', + 'EDI_DUP_INV_LIBRARY', + '', + 'EDIFACT duplicate invoice detected - library notification', + 0, + 'EDIFACT Duplicate Invoice Blocked - [% invoicenumber | html %]', + 'Duplicate EDIFACT Invoice Detected and Blocked + +Invoice Number: [% invoicenumber | html %] +Vendor: [% aqbooksellers.name | html %] (ID: [% vendor_id | html %]) +EDI Message File: [% filename | html %] +Original Invoice ID: [% original_invoiceid | html %] +Original Invoice Date: [% original_shipmentdate | html %] + +Status: Processing has been blocked. The invoice was NOT created in Koha. + +Action Required: +The supplier must resend this invoice with a unique invoice number. + +View EDI Message: [% OPACBaseURL | uri %]/cgi-bin/koha/acqui/edimsg.pl?id=[% message_id | uri %] +View Original Invoice: [% OPACBaseURL | uri %]/cgi-bin/koha/acqui/invoice.pl?invoiceid=[% original_invoiceid | uri %] + +This is an automated notification from your Koha system.', + 'email', + 'default' + ) + } + ); + say $out "Added letter template 'EDI_DUP_INV_LIBRARY'"; + + $dbh->do( + q{ + INSERT IGNORE INTO letter (module, code, branchcode, name, is_html, title, content, message_transport_type, lang) + VALUES ( + 'acquisition', + 'EDI_DUP_INV_VENDOR', + '', + 'EDIFACT duplicate invoice detected - vendor notification', + 0, + 'Duplicate Invoice Number - Action Required - [% invoicenumber | html %]', + 'Dear Supplier, + +We have received an EDIFACT invoice message from your system with a duplicate invoice number. + +Invoice Number: [% invoicenumber | html %] +Your Reference (SAN): [% vendor_san | html %] +EDI Message File: [% filename | html %] +Received Date: [% received_date | html %] + +Issue: +This invoice number has already been processed in our system (original invoice ID: [% original_invoiceid | html %], date: [% original_shipmentdate | html %]). + +Action Required: +Please resend this invoice using a UNIQUE invoice number. Duplicate invoice numbers cannot be processed by our system. + +If you believe this is in error, please contact our acquisitions department. + +Library: [% aqbooksellers.name | html %] + +This is an automated notification. Please do not reply to this email.', + 'email', + 'default' + ) + } + ); + say $out "Added letter template 'EDI_DUP_INV_VENDOR'"; + }, +}; diff --git a/installer/data/mysql/en/mandatory/sample_notices.yml b/installer/data/mysql/en/mandatory/sample_notices.yml index 69ff86f532b..24f76542f94 100644 --- a/installer/data/mysql/en/mandatory/sample_notices.yml +++ b/installer/data/mysql/en/mandatory/sample_notices.yml @@ -43,6 +43,63 @@ tables: - "" - "Your library." + - module: acquisition + code: EDI_DUP_INV_LIBRARY + branchcode: "" + name: "EDIFACT duplicate invoice detected - library notification" + is_html: 0 + title: "EDIFACT Duplicate Invoice Blocked - [% invoicenumber %]" + message_transport_type: email + lang: default + content: + - "Duplicate EDIFACT Invoice Detected and Blocked" + - "" + - "Invoice Number: [% invoicenumber %]" + - "Vendor: [% aqbooksellers.name %] (ID: [% vendor_id %])" + - "EDI Message File: [% filename %]" + - "Original Invoice ID: [% original_invoiceid %]" + - "Original Invoice Date: [% original_shipmentdate %]" + - "" + - "Status: Processing has been blocked. The invoice was NOT created in Koha." + - "" + - "Action Required:" + - "The supplier must resend this invoice with a unique invoice number." + - "" + - "View EDI Message: [% OPACBaseURL %]/cgi-bin/koha/acqui/edimsg.pl?id=[% message_id %]" + - "View Original Invoice: [% OPACBaseURL %]/cgi-bin/koha/acqui/invoice.pl?invoiceid=[% original_invoiceid %]" + - "" + - "This is an automated notification from your Koha system." + + - module: acquisition + code: EDI_DUP_INV_VENDOR + branchcode: "" + name: "EDIFACT duplicate invoice detected - vendor notification" + is_html: 0 + title: "Duplicate Invoice Number - Action Required - [% invoicenumber %]" + message_transport_type: email + lang: default + content: + - "Dear Supplier," + - "" + - "We have received an EDIFACT invoice message from your system with a duplicate invoice number." + - "" + - "Invoice Number: [% invoicenumber %]" + - "Your Reference (SAN): [% vendor_san %]" + - "EDI Message File: [% filename %]" + - "Received Date: [% received_date %]" + - "" + - "Issue:" + - "This invoice number has already been processed in our system (original invoice ID: [% original_invoiceid %], date: [% original_shipmentdate %])." + - "" + - "Action Required:" + - "Please resend this invoice using a UNIQUE invoice number. Duplicate invoice numbers cannot be processed by our system." + - "" + - "If you believe this is in error, please contact our acquisitions department." + - "" + - "Library: [% aqbooksellers.name %]" + - "" + - "This is an automated notification. Please do not reply to this email." + - module: bookings code: BOOKING_CANCELLATION branchcode: "" diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref index c8f8d395d26..c9859160e8a 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref @@ -186,3 +186,20 @@ Acquisitions: location: "location" ccode: "collection" - " in items." + - + - pref: EdiBlockDuplicateInvoice + choices: + 1: Block + 0: "Don't block" + - processing of EDIFACT invoices when a duplicate invoice number is detected for the same supplier. + - + - pref: EdiBlockDuplicateInvoiceEmailNotice + choices: + 1: Send + 0: "Don't send" + - email notifications when duplicate EDIFACT invoices are detected. + - + - "Send duplicate invoice notifications to these email addresses (comma-separated):" + - pref: EdiBlockDuplicateInvoiceEmailAddresses + type: textarea + class: code diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorContacts.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorContacts.vue index a9958c8cddd..c37630cdef9 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorContacts.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorContacts.vue @@ -44,6 +44,17 @@ {{ $__("Contact about late orders") }} +
  • + +
  • diff --git a/t/db_dependent/Koha/EDI.t b/t/db_dependent/Koha/EDI.t index 9c8b9d02b12..3d45d34674d 100755 --- a/t/db_dependent/Koha/EDI.t +++ b/t/db_dependent/Koha/EDI.t @@ -21,7 +21,7 @@ use Modern::Perl; use FindBin qw( $Bin ); use Test::NoWarnings; -use Test::More tests => 6; +use Test::More tests => 7; use Test::MockModule; use t::lib::Mocks; @@ -1616,3 +1616,1012 @@ subtest 'create_edi_order_logging' => sub { $schema->storage->txn_rollback; }; + +subtest 'duplicate_invoice_blocking' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + # Get dirname for transport + my $dirname = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} ); + + # Test 1: Backward compatibility - duplicate detection disabled + subtest 'duplicate_detection_disabled' => sub { + plan tests => 4; + + $schema->storage->txn_begin; + + # Disable duplicate blocking preference + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 0 ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create vendor EDI account + my $account = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + # Create test invoice that already exists + my $existing_invoice = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + shipmentdate => '2020-01-01', + } + } + ); + + # Create test basket and order + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + my $ordernumber = $order->ordernumber; + + # Prepare invoice message + my $filename = 'INVOICE.CEI'; + ok( -e $dirname . $filename, 'File INVOICE.CEI found' ); + + my $trans = Koha::Edifact::Transport->new( $account->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } ); + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$ordernumber/g; + $invoice_message->update( { raw_msg => $raw_msg } ); + + # Clear logger + $logger->clear(); + + # Process the invoice - should succeed despite duplicate + my $error; + eval { + process_invoice($invoice_message); + 1; + } or do { + $error = $@; + }; + ok( !$error, 'Invoice processing completed without dying when preference disabled' ); + + # Verify duplicate was NOT blocked (second invoice created) + my $duplicate_invoices = $schema->resultset('Aqinvoice')->search( + { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + } + ); + is( $duplicate_invoices->count, 2, 'Duplicate invoice was allowed when preference disabled' ); + + # Verify no duplicate error was logged + my $errors = $invoice_message->edifact_errors; + my $duplicate_error = $errors->search( { details => { 'like', '%Duplicate invoice%' } } )->count; + is( $duplicate_error, 0, 'No duplicate error logged when preference disabled' ); + + $logger->clear(); + $schema->storage->txn_rollback; + }; + + # Test 2: Duplicate blocking enabled + subtest 'duplicate_blocking_enabled' => sub { + plan tests => 6; + + $schema->storage->txn_begin; + + # Enable duplicate blocking preference + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 0 ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create vendor EDI account + my $account = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + # Create test invoice that already exists + my $existing_invoice = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + shipmentdate => '2020-01-01', + } + } + ); + + # Create test basket and order + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + my $ordernumber = $order->ordernumber; + + # Prepare invoice message + my $filename = 'INVOICE.CEI'; + my $trans = Koha::Edifact::Transport->new( $account->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } ); + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$ordernumber/g; + $invoice_message->update( { raw_msg => $raw_msg } ); + + # Clear logger + $logger->clear(); + + # Process the invoice - should block duplicate + my $error; + eval { + process_invoice($invoice_message); + 1; + } or do { + $error = $@; + }; + ok( !$error, 'Invoice processing completed without dying' ); + + # Verify duplicate was blocked (only original invoice exists) + my $duplicate_invoices = $schema->resultset('Aqinvoice')->search( + { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + } + ); + is( $duplicate_invoices->count, 1, 'Duplicate invoice was blocked' ); + + # Verify error was logged + $logger->error_like( + qr/Duplicate invoice INV00003 for vendor.*/, + 'Error logged for duplicate invoice' + ); + + # Verify error recorded in edifact_errors table + my $errors = $invoice_message->edifact_errors; + my $duplicate_error = $errors->search( { details => { 'like', '%Duplicate invoice%' } } )->first; + ok( $duplicate_error, 'Duplicate error recorded in edifact_errors table' ); + like( + $duplicate_error->details, qr/Duplicate invoice number 'INV00003'/, + 'Error details contain invoice number' + ); + + # Verify message status set to error + $invoice_message->discard_changes; + is( $invoice_message->status, 'error', 'Message status set to error' ); + + $logger->clear(); + $schema->storage->txn_rollback; + }; + + # Test 3: Same invoice number with different vendor should be allowed + subtest 'different_vendor_allowed' => sub { + plan tests => 3; + + $schema->storage->txn_begin; + + # Enable duplicate blocking preference + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create two different vendors + my $account1 = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor 1', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + my $account2 = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor 2', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027999', + } + } + ); + + # Create invoice for vendor 1 + my $invoice1 = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account1->{vendor_id}, + } + } + ); + + # Create test basket and order for vendor 2 + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account2->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + + # Prepare invoice message for vendor 2 using same file + my $filename = 'INVOICE.CEI'; + my $trans = Koha::Edifact::Transport->new( $account2->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->search( + { filename => $filename }, + { order_by => { -desc => 'id' }, rows => 1 } + )->single; + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g; + $raw_msg =~ s/5013546027173/$account2->{san}/g; # Replace vendor SAN + $invoice_message->update( + { + raw_msg => $raw_msg, + vendor_id => $account2->{vendor_id}, + edi_acct => $account2->{id} + } + ); + + # Clear logger + $logger->clear(); + + # Process the invoice - should succeed (different vendor) + my $error; + eval { + process_invoice($invoice_message); + 1; + } or do { + $error = $@; + }; + ok( !$error, 'Invoice processing completed without dying' ); + + # Verify both invoices exist (one per vendor) + my $invoices_vendor1 = $schema->resultset('Aqinvoice')->search( + { + invoicenumber => 'INV00003', + booksellerid => $account1->{vendor_id}, + } + ); + is( $invoices_vendor1->count, 1, 'Invoice exists for vendor 1' ); + + my $invoices_vendor2 = $schema->resultset('Aqinvoice')->search( + { + invoicenumber => 'INV00003', + booksellerid => $account2->{vendor_id}, + } + ); + is( $invoices_vendor2->count, 1, 'Invoice allowed for vendor 2 with same invoice number' ); + + $logger->clear(); + $schema->storage->txn_rollback; + }; + + # Test 4: Library staff email notification + subtest 'library_email_notification' => sub { + plan tests => 5; + + $schema->storage->txn_begin; + + # Enable duplicate blocking and email notifications + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailAddresses', 'library@example.com' ); + + # Create letter templates for notifications (delete first if exist) + $schema->resultset('Letter')->search( + { + module => 'acquisition', + code => [ 'EDI_DUP_INV_LIBRARY', 'EDI_DUP_INV_VENDOR' ], + } + )->delete; + + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_LIBRARY', + branchcode => '', + name => 'Test library notification', + is_html => 0, + title => 'Duplicate Invoice - <>', + content => 'Duplicate invoice <>. Processing has been blocked.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_VENDOR', + branchcode => '', + name => 'Test vendor notification', + is_html => 0, + title => 'Duplicate Invoice - <>', + content => 'Duplicate invoice <>. Please use UNIQUE invoice number.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create vendor EDI account + my $account = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + # Create test invoice that already exists + my $existing_invoice = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + shipmentdate => '2020-01-01', + } + } + ); + + # Create test basket and order + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + + # Prepare invoice message + my $filename = 'INVOICE.CEI'; + my $trans = Koha::Edifact::Transport->new( $account->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } ); + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g; + $invoice_message->update( { raw_msg => $raw_msg } ); + + # Process the invoice + process_invoice($invoice_message); + + # Verify library email was queued in message_queue + my $library_messages = $schema->resultset('MessageQueue')->search( + { + to_address => 'library@example.com', + status => [ 'sent', 'pending', 'failed' ], # Accept any status - queuing is what matters + } + ); + is( $library_messages->count, 1, 'Library notification was queued' ); + + my $library_message = $library_messages->next; + like( $library_message->subject, qr/Duplicate Invoice/, 'Library email has correct subject' ); + like( $library_message->content, qr/INV00003/, 'Library email contains invoice number' ); + like( $library_message->content, qr/Processing has been blocked/, 'Library email contains blocking message' ); + + # Verify message was recorded in message_queue for audit trail + ok( $library_message->message_id, 'Message has ID for audit trail' ); + + $schema->storage->txn_rollback; + }; + + # Test 5: Vendor email notification + subtest 'vendor_email_notification' => sub { + plan tests => 5; + + $schema->storage->txn_begin; + + # Enable duplicate blocking and email notifications + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailAddresses', 'library@example.com' ); + + # Create letter templates for notifications (delete first if exist) + $schema->resultset('Letter')->search( + { + module => 'acquisition', + code => [ 'EDI_DUP_INV_LIBRARY', 'EDI_DUP_INV_VENDOR' ], + } + )->delete; + + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_LIBRARY', + branchcode => '', + name => 'Test library notification', + is_html => 0, + title => 'Duplicate Invoice - <>', + content => 'Duplicate invoice <>. Processing has been blocked.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_VENDOR', + branchcode => '', + name => 'Test vendor notification', + is_html => 0, + title => 'Duplicate Invoice - <>', + content => 'Duplicate invoice <>. Please use UNIQUE invoice number.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create vendor EDI account + my $account = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + # Create vendor contact with EDI error notification enabled + my $vendor_contact = $builder->build( + { + source => 'Aqcontact', + value => { + name => 'Test Vendor Contact', + email => 'vendor@supplier.com', + booksellerid => $account->{vendor_id}, + edi_error_notification => 1, + } + } + ); + + # Create test invoice that already exists + my $existing_invoice = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + shipmentdate => '2020-01-01', + } + } + ); + + # Create test basket and order + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + + # Prepare invoice message + my $filename = 'INVOICE.CEI'; + my $trans = Koha::Edifact::Transport->new( $account->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } ); + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g; + $invoice_message->update( { raw_msg => $raw_msg } ); + + # Process the invoice + process_invoice($invoice_message); + + # Verify vendor email was queued in message_queue + my $vendor_messages = $schema->resultset('MessageQueue')->search( + { + to_address => 'vendor@supplier.com', + status => [ 'sent', 'pending', 'failed' ], # Accept any status - queuing is what matters + } + ); + is( $vendor_messages->count, 1, 'Vendor notification was queued' ); + + my $vendor_message = $vendor_messages->next; + like( $vendor_message->subject, qr/Duplicate Invoice/, 'Vendor email has correct subject' ); + like( $vendor_message->content, qr/INV00003/, 'Vendor email contains invoice number' ); + like( $vendor_message->content, qr/UNIQUE invoice number/, 'Vendor email contains action required message' ); + + # Verify message was recorded for audit trail + ok( $vendor_message->message_id, 'Message has ID for audit trail' ); + + $schema->storage->txn_rollback; + }; + + # Test 6: Multiple email recipients + subtest 'multiple_email_recipients' => sub { + plan tests => 3; + + $schema->storage->txn_begin; + + # Enable duplicate blocking and email notifications with multiple addresses + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 1 ); + t::lib::Mocks::mock_preference( + 'EdiBlockDuplicateInvoiceEmailAddresses', + 'library1@example.com, library2@example.com, library3@example.com' + ); + + # Create letter templates (delete first if exist) + $schema->resultset('Letter')->search( + { + module => 'acquisition', + code => 'EDI_DUP_INV_LIBRARY', + } + )->delete; + + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_LIBRARY', + branchcode => '', + name => 'Test library notification', + is_html => 0, + title => 'Duplicate Invoice', + content => 'Duplicate invoice.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create vendor EDI account + my $account = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + # Create test invoice that already exists + my $existing_invoice = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + } + } + ); + + # Create test basket and order + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + + # Prepare invoice message + my $filename = 'INVOICE.CEI'; + my $trans = Koha::Edifact::Transport->new( $account->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } ); + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g; + $invoice_message->update( { raw_msg => $raw_msg } ); + + # Process the invoice + process_invoice($invoice_message); + + # Verify all three library emails were queued + my $library_messages = $schema->resultset('MessageQueue')->search( + { + to_address => [ 'library1@example.com', 'library2@example.com', 'library3@example.com' ], + status => [ 'sent', 'pending', 'failed' ], # Accept any status - queuing is what matters + } + ); + is( $library_messages->count, 3, 'Three library notification emails queued' ); + + my @to_addresses = map { $_->to_address } $library_messages->all; + ok( ( grep { $_ eq 'library1@example.com' } @to_addresses ), 'Email sent to library1' ); + ok( ( grep { $_ eq 'library2@example.com' } @to_addresses ), 'Email sent to library2' ); + + $schema->storage->txn_rollback; + }; + + # Test 7: Invalid email handling + subtest 'invalid_email_handling' => sub { + plan tests => 3; + + $schema->storage->txn_begin; + + # Enable duplicate blocking and email notifications with invalid addresses + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 1 ); + t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailAddresses', 'invalid-email, valid@example.com' ); + + # Create letter templates (delete first if exist) + $schema->resultset('Letter')->search( + { + module => 'acquisition', + code => [ 'EDI_DUP_INV_LIBRARY', 'EDI_DUP_INV_VENDOR' ], + } + )->delete; + + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_LIBRARY', + branchcode => '', + name => 'Test library notification', + is_html => 0, + title => 'Duplicate Invoice', + content => 'Duplicate invoice.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + $builder->build( + { + source => 'Letter', + value => { + module => 'acquisition', + code => 'EDI_DUP_INV_VENDOR', + branchcode => '', + name => 'Test vendor notification', + is_html => 0, + title => 'Duplicate Invoice', + content => 'Duplicate invoice.', + message_transport_type => 'email', + lang => 'default', + } + } + ); + + # Create file transport for local testing + my $file_transport = $builder->build( + { + source => 'FileTransport', + value => { + name => 'Test Invoice Transport', + transport => 'local', + download_directory => $dirname, + upload_directory => $dirname, + } + } + ); + + # Create vendor EDI account + my $account = $builder->build( + { + source => 'VendorEdiAccount', + value => { + description => 'test vendor', + file_transport_id => $file_transport->{file_transport_id}, + plugin => '', + san => '5013546027173', + } + } + ); + + # Create vendor contact with invalid email + my $vendor_contact = $builder->build( + { + source => 'Aqcontact', + value => { + name => 'Test Vendor Contact', + email => 'not-an-email', + booksellerid => $account->{vendor_id}, + edi_error_notification => 1, + } + } + ); + + # Create test invoice that already exists + my $existing_invoice = $builder->build( + { + source => 'Aqinvoice', + value => { + invoicenumber => 'INV00003', + booksellerid => $account->{vendor_id}, + } + } + ); + + # Create test basket and order + my $basket = $builder->build_object( + { + class => 'Koha::Acquisition::Baskets', + value => { + booksellerid => $account->{vendor_id}, + basketname => 'Test Basket', + } + } + ); + my $order = $builder->build_object( + { + class => 'Koha::Acquisition::Orders', + value => { + basketno => $basket->id, + orderstatus => 'new', + biblionumber => undef, + } + } + ); + + # Prepare invoice message + my $filename = 'INVOICE.CEI'; + my $trans = Koha::Edifact::Transport->new( $account->{id} ); + $trans->working_directory($dirname); + + my $mhash = $trans->message_hash(); + $mhash->{message_type} = 'INVOICE'; + $trans->ingest( $mhash, $filename ); + + my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } ); + my $raw_msg = $invoice_message->raw_msg; + $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g; + $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g; + $invoice_message->update( { raw_msg => $raw_msg } ); + + # Clear logger + $logger->clear(); + + # Process the invoice + process_invoice($invoice_message); + + # Verify only valid email was queued (invalid email skipped) + my $valid_messages = $schema->resultset('MessageQueue')->search( + { + to_address => 'valid@example.com', + status => [ 'sent', 'pending', 'failed' ], # Accept any status - queuing is what matters + } + ); + is( $valid_messages->count, 1, 'Only valid library email queued' ); + + # Verify no message for invalid email + my $invalid_messages = $schema->resultset('MessageQueue')->search( + { + to_address => 'invalid-email', + } + ); + is( $invalid_messages->count, 0, 'No message queued for invalid email' ); + + # Verify invalid vendor contact email was logged + $logger->warn_like( + qr/Invalid vendor contact email address/, + 'Warning logged for invalid vendor contact email' + ); + + $logger->clear(); + $schema->storage->txn_rollback; + }; + + $schema->storage->txn_rollback; +}; -- 2.52.0