Bugzilla – Attachment 189963 Details for
Bug 41297
Add duplicate invoice number detection on EDI invoice import
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41297: Block duplicate EDIFACT invoice processing
7dd2c97.patch (text/plain), 64.05 KB, created by
Martin Renvoize (ashimema)
on 2025-11-26 15:46:44 UTC
(
hide
)
Description:
Bug 41297: Block duplicate EDIFACT invoice processing
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-11-26 15:46:44 UTC
Size:
64.05 KB
patch
obsolete
>From 7dd2c9770a9c4d7555347d72778d41e3a84ad54f Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >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 >--- > 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") }} > </label> > </li> >+ <li> >+ <label> >+ <input >+ type="checkbox" >+ :id="`contact_edi_error_notification_${index}`" >+ class="contact_edi_error_notification" >+ v-model="contact.edi_error_notification" >+ /> >+ {{ $__("Contact about EDI errors") }} >+ </label> >+ </li> > </ol> > </fieldset> > </div> >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 - <<invoicenumber>>', >+ content => 'Duplicate invoice <<invoicenumber>>. 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 - <<invoicenumber>>', >+ content => 'Duplicate invoice <<invoicenumber>>. 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 - <<invoicenumber>>', >+ content => 'Duplicate invoice <<invoicenumber>>. 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 - <<invoicenumber>>', >+ content => 'Duplicate invoice <<invoicenumber>>. 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 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 41297
:
189962
|
189963
|
189964