View | Details | Raw Unified | Return to bug 41297
Collapse All | Expand All

(-)a/Koha/EDI.pm (-1 / +196 lines)
Lines 328-333 sub process_invoice { Link Here
328
                next;
328
                next;
329
            }
329
            }
330
            $invoice_message->edi_acct( $vendor_acct->id );
330
            $invoice_message->edi_acct( $vendor_acct->id );
331
332
            # Check for duplicate invoices if preference enabled
333
            if ( C4::Context->preference('EdiBlockDuplicateInvoice') ) {
334
                my $duplicate_invoice = $schema->resultset('Aqinvoice')->search(
335
                    {
336
                        invoicenumber => $invoicenumber,
337
                        booksellerid  => $invoice_message->vendor_id,
338
                    }
339
                )->first;
340
341
                if ($duplicate_invoice) {
342
                    $logger->error( "Duplicate invoice $invoicenumber for vendor "
343
                            . $invoice_message->vendor_id . " in "
344
                            . $invoice_message->filename );
345
346
                    # Log to edifact_errors table
347
                    $invoice_message->add_to_edifact_errors(
348
                        {
349
                            section => "BGM+" . $invoicenumber,
350
                            details => "Duplicate invoice number '$invoicenumber'. "
351
                                . "Original invoice ID: "
352
                                . $duplicate_invoice->invoiceid . ". "
353
                                . "Processing blocked."
354
                        }
355
                    );
356
357
                    # Send email notification if enabled
358
                    _send_duplicate_invoice_email_notice(
359
                        $invoice_message,
360
                        $invoicenumber,
361
                        $vendor_acct,
362
                        $duplicate_invoice
363
                    );
364
365
                    # Mark message as error and stop processing this invoice
366
                    $invoice_message->status('error');
367
                    $invoice_message->update;
368
369
                    next;    # Skip to next message in transmission
370
                }
371
            }
372
331
            $logger->trace("Adding invoice: $invoicenumber");
373
            $logger->trace("Adding invoice: $invoicenumber");
332
            my $new_invoice = $schema->resultset('Aqinvoice')->create(
374
            my $new_invoice = $schema->resultset('Aqinvoice')->create(
333
                {
375
                {
Lines 467-473 sub process_invoice { Link Here
467
        }
509
        }
468
    }
510
    }
469
511
470
    $invoice_message->status('received');
512
    # Only set status to 'received' if not already set to 'error'
513
    $invoice_message->discard_changes;
514
    if ( $invoice_message->status ne 'error' ) {
515
        $invoice_message->status('received');
516
    }
471
    $invoice_message->update;    # status and basketno link
517
    $invoice_message->update;    # status and basketno link
472
    return;
518
    return;
473
}
519
}
Lines 1503-1508 sub _handle_008_field { Link Here
1503
    return $bib_record;
1549
    return $bib_record;
1504
}
1550
}
1505
1551
1552
sub _send_duplicate_invoice_email_notice {
1553
    my ( $invoice_message, $invoicenumber, $vendor_acct, $duplicate_invoice ) = @_;
1554
1555
    my $logger = Koha::Logger->get( { interface => 'edi' } );
1556
1557
    # Check if email notifications enabled
1558
    return unless C4::Context->preference('EdiBlockDuplicateInvoiceEmailNotice');
1559
1560
    # Get vendor information
1561
    my $vendor_id = $invoice_message->vendor_id;
1562
1563
    # Prepare template substitution values
1564
    my $substitute = {
1565
        invoicenumber         => $invoicenumber,
1566
        vendor_id             => $vendor_id,
1567
        vendor_san            => $vendor_acct ? $vendor_acct->san : '',
1568
        filename              => $invoice_message->filename,
1569
        message_id            => $invoice_message->id,
1570
        original_invoiceid    => $duplicate_invoice->invoiceid,
1571
        original_shipmentdate => $duplicate_invoice->shipmentdate || 'N/A',
1572
        received_date         => dt_from_string()->ymd,
1573
    };
1574
1575
    # 1. Send notification to library staff
1576
    my $library_email_addresses = C4::Context->preference('EdiBlockDuplicateInvoiceEmailAddresses');
1577
    if ($library_email_addresses) {
1578
        my @library_addresses = split /\s*,\s*/, $library_email_addresses;
1579
1580
        foreach my $to_address (@library_addresses) {
1581
            $to_address =~ s/^\s+|\s+$//g;    # trim whitespace
1582
            next unless $to_address;
1583
            next unless Koha::Email->is_valid($to_address);
1584
1585
            my $letter = C4::Letters::GetPreparedLetter(
1586
                module                 => 'acquisition',
1587
                letter_code            => 'EDI_DUP_INV_LIBRARY',
1588
                message_transport_type => 'email',
1589
                tables                 => {
1590
                    aqbooksellers => $vendor_id,
1591
                },
1592
                substitute => $substitute,
1593
            );
1594
1595
            if ($letter) {
1596
                my $message_id = C4::Letters::EnqueueLetter(
1597
                    {
1598
                        letter                 => $letter,
1599
                        to_address             => $to_address,
1600
                        message_transport_type => 'email',
1601
                    }
1602
                );
1603
1604
                if ($message_id) {
1605
                    $logger->info(
1606
                        "Library duplicate invoice notification queued (message_id: $message_id) for $to_address, invoice $invoicenumber. Message will be sent by message_queue cronjob."
1607
                    );
1608
                } else {
1609
                    $logger->warn("Failed to enqueue library notification to $to_address for invoice $invoicenumber");
1610
                }
1611
            } else {
1612
                $logger->warn("Could not generate library notification letter for invoice $invoicenumber");
1613
            }
1614
        }
1615
    }
1616
1617
    # 2. Send notification to vendor contacts
1618
    if ($vendor_acct) {
1619
        my $schema = Koha::Database->new()->schema();
1620
        my $vendor = $schema->resultset('Aqbookseller')->find($vendor_id);
1621
        if ($vendor) {
1622
            my @edi_contacts = $vendor->aqcontacts->search(
1623
                {
1624
                    edi_error_notification => 1,
1625
                    email                  => { '!=' => undef },
1626
                }
1627
            )->all;
1628
1629
            foreach my $contact (@edi_contacts) {
1630
                my $vendor_email = $contact->email;
1631
1632
                if ( Koha::Email->is_valid($vendor_email) ) {
1633
                    my $letter = C4::Letters::GetPreparedLetter(
1634
                        module                 => 'acquisition',
1635
                        letter_code            => 'EDI_DUP_INV_VENDOR',
1636
                        message_transport_type => 'email',
1637
                        tables                 => {
1638
                            aqbooksellers => $vendor_id,
1639
                        },
1640
                        substitute => $substitute,
1641
                    );
1642
1643
                    if ($letter) {
1644
                        my $message_id = C4::Letters::EnqueueLetter(
1645
                            {
1646
                                letter                 => $letter,
1647
                                to_address             => $vendor_email,
1648
                                message_transport_type => 'email',
1649
                            }
1650
                        );
1651
1652
                        if ($message_id) {
1653
                            $logger->info(
1654
                                "Vendor duplicate invoice notification queued (message_id: $message_id) for $vendor_email (contact: "
1655
                                    . $contact->name
1656
                                    . "), invoice $invoicenumber. Message will be sent by message_queue cronjob." );
1657
                        } else {
1658
                            $logger->warn(
1659
                                "Failed to enqueue vendor notification to $vendor_email for invoice $invoicenumber");
1660
                        }
1661
                    } else {
1662
                        $logger->warn("Could not generate vendor notification letter for invoice $invoicenumber");
1663
                    }
1664
                } else {
1665
                    $logger->warn(
1666
                        "Invalid vendor contact email address: $vendor_email for contact: " . $contact->name );
1667
                }
1668
            }
1669
        }
1670
    }
1671
}
1672
1506
1;
1673
1;
1507
__END__
1674
__END__
1508
1675
Lines 1618-1623 Koha::EDI Link Here
1618
1785
1619
      If all else fails returns empty string
1786
      If all else fails returns empty string
1620
1787
1788
=head2 _send_duplicate_invoice_email_notice
1789
1790
    _send_duplicate_invoice_email_notice($invoice_message, $invoicenumber, $vendor_acct, $duplicate_invoice)
1791
1792
    Internal function to queue email notifications when duplicate EDIFACT invoices are detected.
1793
1794
    Uses the standard Koha messaging pattern:
1795
    - GetPreparedLetter: Generates the notice content from templates
1796
    - EnqueueLetter: Adds the message to the message_queue table for delivery by the cronjob
1797
1798
    Queues two types of notifications using letter templates:
1799
    1. EDI_DUP_INV_LIBRARY - to library staff (addresses from EdiBlockDuplicateInvoiceEmailAddresses)
1800
    2. EDI_DUP_INV_VENDOR - to vendor (address from vendor_edi_accounts.vendor_email)
1801
1802
    All messages are recorded in the message_queue table for auditing and can be reviewed
1803
    in the Koha notices interface. Messages will be sent by the message_queue cronjob
1804
    (misc/cronjobs/process_message_queue.pl).
1805
1806
    Only runs if EdiBlockDuplicateInvoiceEmailNotice preference is enabled.
1807
1808
    Parameters:
1809
    - $invoice_message: The EdifactMessage object being processed
1810
    - $invoicenumber: The duplicate invoice number found
1811
    - $vendor_acct: The VendorEdiAccount object
1812
    - $duplicate_invoice: The existing Aqinvoice object with the same invoice number
1813
1814
    Returns: nothing
1815
1621
=head2 _create_bib_from_quote
1816
=head2 _create_bib_from_quote
1622
1817
1623
       marc_record_obj = _create_bib_from_quote(lineitem, quote)
1818
       marc_record_obj = _create_bib_from_quote(lineitem, quote)
(-)a/installer/data/mysql/atomicupdate/bug_40383.pl (+163 lines)
Line 0 Link Here
1
use Modern::Perl;
2
3
return {
4
    bug_number  => "41297",
5
    description => "Add system preferences for blocking duplicate EDI invoices",
6
    up          => sub {
7
        my ($args) = @_;
8
        my ( $dbh, $out ) = @$args{qw(dbh out)};
9
10
        # Master preference to enable duplicate blocking
11
        $dbh->do(
12
            q{
13
            INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type)
14
            VALUES (
15
                'EdiBlockDuplicateInvoice',
16
                '0',
17
                NULL,
18
                '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.',
19
                'YesNo'
20
            )
21
        }
22
        );
23
        say $out "Added system preference 'EdiBlockDuplicateInvoice'";
24
25
        # Email notification toggle
26
        $dbh->do(
27
            q{
28
            INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type)
29
            VALUES (
30
                'EdiBlockDuplicateInvoiceEmailNotice',
31
                '0',
32
                NULL,
33
                'Send email notification when duplicate EDIFACT invoices are detected. Requires EdiBlockDuplicateInvoice to be enabled.',
34
                'YesNo'
35
            )
36
        }
37
        );
38
        say $out "Added system preference 'EdiBlockDuplicateInvoiceEmailNotice'";
39
40
        # Email recipient list
41
        $dbh->do(
42
            q{
43
            INSERT IGNORE INTO systempreferences (variable, value, options, explanation, type)
44
            VALUES (
45
                'EdiBlockDuplicateInvoiceEmailAddresses',
46
                '',
47
                NULL,
48
                '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.',
49
                'Textarea'
50
            )
51
        }
52
        );
53
        say $out "Added system preference 'EdiBlockDuplicateInvoiceEmailAddresses'";
54
55
        # Add database index for performance
56
        my $index_exists = $dbh->selectrow_array(
57
            q{
58
            SELECT COUNT(*)
59
            FROM information_schema.statistics
60
            WHERE table_schema = DATABASE()
61
            AND table_name = 'aqinvoices'
62
            AND index_name = 'idx_invoicenumber_booksellerid'
63
        }
64
        );
65
66
        unless ($index_exists) {
67
            $dbh->do(
68
                q{
69
                CREATE INDEX idx_invoicenumber_booksellerid
70
                ON aqinvoices (invoicenumber(100), booksellerid)
71
            }
72
            );
73
            say $out "Added index idx_invoicenumber_booksellerid to aqinvoices table";
74
        }
75
76
        # Add edi_error_notification column to aqcontacts
77
        unless ( column_exists( 'aqcontacts', 'edi_error_notification' ) ) {
78
            $dbh->do(
79
                q{
80
                ALTER TABLE aqcontacts
81
                ADD COLUMN edi_error_notification TINYINT(1) NOT NULL DEFAULT 0
82
                AFTER serialsprimary
83
            }
84
            );
85
            say $out "Added edi_error_notification column to aqcontacts table";
86
        }
87
88
        # Delete any truncated templates first
89
        $dbh->do(q{DELETE FROM letter WHERE code = 'EDI_DUPLICATE_INVOIC' AND module = 'acquisition'});
90
91
        # Add notice templates for duplicate invoice notifications
92
        $dbh->do(
93
            q{
94
            INSERT IGNORE INTO letter (module, code, branchcode, name, is_html, title, content, message_transport_type, lang)
95
            VALUES (
96
                'acquisition',
97
                'EDI_DUP_INV_LIBRARY',
98
                '',
99
                'EDIFACT duplicate invoice detected - library notification',
100
                0,
101
                'EDIFACT Duplicate Invoice Blocked - [% invoicenumber | html %]',
102
                'Duplicate EDIFACT Invoice Detected and Blocked
103
104
Invoice Number: [% invoicenumber | html %]
105
Vendor: [% aqbooksellers.name | html %] (ID: [% vendor_id | html %])
106
EDI Message File: [% filename | html %]
107
Original Invoice ID: [% original_invoiceid | html %]
108
Original Invoice Date: [% original_shipmentdate | html %]
109
110
Status: Processing has been blocked. The invoice was NOT created in Koha.
111
112
Action Required:
113
The supplier must resend this invoice with a unique invoice number.
114
115
View EDI Message: [% OPACBaseURL | uri %]/cgi-bin/koha/acqui/edimsg.pl?id=[% message_id | uri %]
116
View Original Invoice: [% OPACBaseURL | uri %]/cgi-bin/koha/acqui/invoice.pl?invoiceid=[% original_invoiceid | uri %]
117
118
This is an automated notification from your Koha system.',
119
                'email',
120
                'default'
121
            )
122
        }
123
        );
124
        say $out "Added letter template 'EDI_DUP_INV_LIBRARY'";
125
126
        $dbh->do(
127
            q{
128
            INSERT IGNORE INTO letter (module, code, branchcode, name, is_html, title, content, message_transport_type, lang)
129
            VALUES (
130
                'acquisition',
131
                'EDI_DUP_INV_VENDOR',
132
                '',
133
                'EDIFACT duplicate invoice detected - vendor notification',
134
                0,
135
                'Duplicate Invoice Number - Action Required - [% invoicenumber | html %]',
136
                'Dear Supplier,
137
138
We have received an EDIFACT invoice message from your system with a duplicate invoice number.
139
140
Invoice Number: [% invoicenumber | html %]
141
Your Reference (SAN): [% vendor_san | html %]
142
EDI Message File: [% filename | html %]
143
Received Date: [% received_date | html %]
144
145
Issue:
146
This invoice number has already been processed in our system (original invoice ID: [% original_invoiceid | html %], date: [% original_shipmentdate | html %]).
147
148
Action Required:
149
Please resend this invoice using a UNIQUE invoice number. Duplicate invoice numbers cannot be processed by our system.
150
151
If you believe this is in error, please contact our acquisitions department.
152
153
Library: [% aqbooksellers.name | html %]
154
155
This is an automated notification. Please do not reply to this email.',
156
                'email',
157
                'default'
158
            )
159
        }
160
        );
161
        say $out "Added letter template 'EDI_DUP_INV_VENDOR'";
162
    },
163
};
(-)a/installer/data/mysql/en/mandatory/sample_notices.yml (+57 lines)
Lines 43-48 tables: Link Here
43
            - ""
43
            - ""
44
            - "Your library."
44
            - "Your library."
45
45
46
        - module: acquisition
47
          code: EDI_DUP_INV_LIBRARY
48
          branchcode: ""
49
          name: "EDIFACT duplicate invoice detected - library notification"
50
          is_html: 0
51
          title: "EDIFACT Duplicate Invoice Blocked - [% invoicenumber %]"
52
          message_transport_type: email
53
          lang: default
54
          content:
55
            - "Duplicate EDIFACT Invoice Detected and Blocked"
56
            - ""
57
            - "Invoice Number: [% invoicenumber %]"
58
            - "Vendor: [% aqbooksellers.name %] (ID: [% vendor_id %])"
59
            - "EDI Message File: [% filename %]"
60
            - "Original Invoice ID: [% original_invoiceid %]"
61
            - "Original Invoice Date: [% original_shipmentdate %]"
62
            - ""
63
            - "Status: Processing has been blocked. The invoice was NOT created in Koha."
64
            - ""
65
            - "Action Required:"
66
            - "The supplier must resend this invoice with a unique invoice number."
67
            - ""
68
            - "View EDI Message: [% OPACBaseURL %]/cgi-bin/koha/acqui/edimsg.pl?id=[% message_id %]"
69
            - "View Original Invoice: [% OPACBaseURL %]/cgi-bin/koha/acqui/invoice.pl?invoiceid=[% original_invoiceid %]"
70
            - ""
71
            - "This is an automated notification from your Koha system."
72
73
        - module: acquisition
74
          code: EDI_DUP_INV_VENDOR
75
          branchcode: ""
76
          name: "EDIFACT duplicate invoice detected - vendor notification"
77
          is_html: 0
78
          title: "Duplicate Invoice Number - Action Required - [% invoicenumber %]"
79
          message_transport_type: email
80
          lang: default
81
          content:
82
            - "Dear Supplier,"
83
            - ""
84
            - "We have received an EDIFACT invoice message from your system with a duplicate invoice number."
85
            - ""
86
            - "Invoice Number: [% invoicenumber %]"
87
            - "Your Reference (SAN): [% vendor_san %]"
88
            - "EDI Message File: [% filename %]"
89
            - "Received Date: [% received_date %]"
90
            - ""
91
            - "Issue:"
92
            - "This invoice number has already been processed in our system (original invoice ID: [% original_invoiceid %], date: [% original_shipmentdate %])."
93
            - ""
94
            - "Action Required:"
95
            - "Please resend this invoice using a UNIQUE invoice number. Duplicate invoice numbers cannot be processed by our system."
96
            - ""
97
            - "If you believe this is in error, please contact our acquisitions department."
98
            - ""
99
            - "Library: [% aqbooksellers.name %]"
100
            - ""
101
            - "This is an automated notification. Please do not reply to this email."
102
46
        - module: bookings
103
        - module: bookings
47
          code: BOOKING_CANCELLATION
104
          code: BOOKING_CANCELLATION
48
          branchcode: ""
105
          branchcode: ""
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref (+17 lines)
Lines 196-198 Acquisitions: Link Here
196
                ccode: "collection"
196
                ccode: "collection"
197
                "": "ignore"
197
                "": "ignore"
198
            - " in items."
198
            - " in items."
199
        -
200
            - pref: EdiBlockDuplicateInvoice
201
              choices:
202
                  1: Block
203
                  0: "Don't block"
204
            - processing of EDIFACT invoices when a duplicate invoice number is detected for the same supplier.
205
        -
206
            - pref: EdiBlockDuplicateInvoiceEmailNotice
207
              choices:
208
                  1: Send
209
                  0: "Don't send"
210
            - email notifications when duplicate EDIFACT invoices are detected.
211
        -
212
            - "Send duplicate invoice notifications to these email addresses (comma-separated):"
213
            - pref: EdiBlockDuplicateInvoiceEmailAddresses
214
              type: textarea
215
              class: code
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorContacts.vue (+11 lines)
Lines 44-49 Link Here
44
                                {{ $__("Contact about late orders") }}
44
                                {{ $__("Contact about late orders") }}
45
                            </label>
45
                            </label>
46
                        </li>
46
                        </li>
47
                        <li>
48
                            <label>
49
                                <input
50
                                    type="checkbox"
51
                                    :id="`contact_edi_error_notification_${index}`"
52
                                    class="contact_edi_error_notification"
53
                                    v-model="contact.edi_error_notification"
54
                                />
55
                                {{ $__("Contact about EDI errors") }}
56
                            </label>
57
                        </li>
47
                    </ol>
58
                    </ol>
48
                </fieldset>
59
                </fieldset>
49
            </div>
60
            </div>
(-)a/t/db_dependent/Koha/EDI.t (-2 / +1010 lines)
Lines 21-27 use Modern::Perl; Link Here
21
use FindBin qw( $Bin );
21
use FindBin qw( $Bin );
22
22
23
use Test::NoWarnings;
23
use Test::NoWarnings;
24
use Test::More tests => 8;
24
use Test::More tests => 9;
25
use Test::MockModule;
25
use Test::MockModule;
26
26
27
use t::lib::Mocks;
27
use t::lib::Mocks;
Lines 1907-1909 subtest 'LSL and LSQ field copy to item_hash' => sub { Link Here
1907
1907
1908
    $schema->storage->txn_rollback;
1908
    $schema->storage->txn_rollback;
1909
};
1909
};
1910
- 
1910
1911
subtest 'duplicate_invoice_blocking' => sub {
1912
    plan tests => 7;
1913
1914
    $schema->storage->txn_begin;
1915
1916
    # Get dirname for transport
1917
    my $dirname = ( $Bin =~ /^(.*\/t\/)/ ? $1 . 'edi_testfiles/' : q{} );
1918
1919
    # Test 1: Backward compatibility - duplicate detection disabled
1920
    subtest 'duplicate_detection_disabled' => sub {
1921
        plan tests => 4;
1922
1923
        $schema->storage->txn_begin;
1924
1925
        # Disable duplicate blocking preference
1926
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 0 );
1927
1928
        # Create file transport for local testing
1929
        my $file_transport = $builder->build(
1930
            {
1931
                source => 'FileTransport',
1932
                value  => {
1933
                    name               => 'Test Invoice Transport',
1934
                    transport          => 'local',
1935
                    download_directory => $dirname,
1936
                    upload_directory   => $dirname,
1937
                }
1938
            }
1939
        );
1940
1941
        # Create vendor EDI account
1942
        my $account = $builder->build(
1943
            {
1944
                source => 'VendorEdiAccount',
1945
                value  => {
1946
                    description       => 'test vendor',
1947
                    file_transport_id => $file_transport->{file_transport_id},
1948
                    plugin            => '',
1949
                    san               => '5013546027173',
1950
                }
1951
            }
1952
        );
1953
1954
        # Create test invoice that already exists
1955
        my $existing_invoice = $builder->build(
1956
            {
1957
                source => 'Aqinvoice',
1958
                value  => {
1959
                    invoicenumber => 'INV00003',
1960
                    booksellerid  => $account->{vendor_id},
1961
                    shipmentdate  => '2020-01-01',
1962
                }
1963
            }
1964
        );
1965
1966
        # Create test basket and order
1967
        my $basket = $builder->build_object(
1968
            {
1969
                class => 'Koha::Acquisition::Baskets',
1970
                value => {
1971
                    booksellerid => $account->{vendor_id},
1972
                    basketname   => 'Test Basket',
1973
                }
1974
            }
1975
        );
1976
        my $order = $builder->build_object(
1977
            {
1978
                class => 'Koha::Acquisition::Orders',
1979
                value => {
1980
                    basketno     => $basket->id,
1981
                    orderstatus  => 'new',
1982
                    biblionumber => undef,
1983
                }
1984
            }
1985
        );
1986
        my $ordernumber = $order->ordernumber;
1987
1988
        # Prepare invoice message
1989
        my $filename = 'INVOICE.CEI';
1990
        ok( -e $dirname . $filename, 'File INVOICE.CEI found' );
1991
1992
        my $trans = Koha::Edifact::Transport->new( $account->{id} );
1993
        $trans->working_directory($dirname);
1994
1995
        my $mhash = $trans->message_hash();
1996
        $mhash->{message_type} = 'INVOICE';
1997
        $trans->ingest( $mhash, $filename );
1998
1999
        my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
2000
        my $raw_msg         = $invoice_message->raw_msg;
2001
        $raw_msg =~ s/ORDERNUMBER1/$ordernumber/g;
2002
        $raw_msg =~ s/ORDERNUMBER2/$ordernumber/g;
2003
        $invoice_message->update( { raw_msg => $raw_msg } );
2004
2005
        # Clear logger
2006
        $logger->clear();
2007
2008
        # Process the invoice - should succeed despite duplicate
2009
        my $error;
2010
        eval {
2011
            process_invoice($invoice_message);
2012
            1;
2013
        } or do {
2014
            $error = $@;
2015
        };
2016
        ok( !$error, 'Invoice processing completed without dying when preference disabled' );
2017
2018
        # Verify duplicate was NOT blocked (second invoice created)
2019
        my $duplicate_invoices = $schema->resultset('Aqinvoice')->search(
2020
            {
2021
                invoicenumber => 'INV00003',
2022
                booksellerid  => $account->{vendor_id},
2023
            }
2024
        );
2025
        is( $duplicate_invoices->count, 2, 'Duplicate invoice was allowed when preference disabled' );
2026
2027
        # Verify no duplicate error was logged
2028
        my $errors          = $invoice_message->edifact_errors;
2029
        my $duplicate_error = $errors->search( { details => { 'like', '%Duplicate invoice%' } } )->count;
2030
        is( $duplicate_error, 0, 'No duplicate error logged when preference disabled' );
2031
2032
        $logger->clear();
2033
        $schema->storage->txn_rollback;
2034
    };
2035
2036
    # Test 2: Duplicate blocking enabled
2037
    subtest 'duplicate_blocking_enabled' => sub {
2038
        plan tests => 6;
2039
2040
        $schema->storage->txn_begin;
2041
2042
        # Enable duplicate blocking preference
2043
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice',            1 );
2044
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 0 );
2045
2046
        # Create file transport for local testing
2047
        my $file_transport = $builder->build(
2048
            {
2049
                source => 'FileTransport',
2050
                value  => {
2051
                    name               => 'Test Invoice Transport',
2052
                    transport          => 'local',
2053
                    download_directory => $dirname,
2054
                    upload_directory   => $dirname,
2055
                }
2056
            }
2057
        );
2058
2059
        # Create vendor EDI account
2060
        my $account = $builder->build(
2061
            {
2062
                source => 'VendorEdiAccount',
2063
                value  => {
2064
                    description       => 'test vendor',
2065
                    file_transport_id => $file_transport->{file_transport_id},
2066
                    plugin            => '',
2067
                    san               => '5013546027173',
2068
                }
2069
            }
2070
        );
2071
2072
        # Create test invoice that already exists
2073
        my $existing_invoice = $builder->build(
2074
            {
2075
                source => 'Aqinvoice',
2076
                value  => {
2077
                    invoicenumber => 'INV00003',
2078
                    booksellerid  => $account->{vendor_id},
2079
                    shipmentdate  => '2020-01-01',
2080
                }
2081
            }
2082
        );
2083
2084
        # Create test basket and order
2085
        my $basket = $builder->build_object(
2086
            {
2087
                class => 'Koha::Acquisition::Baskets',
2088
                value => {
2089
                    booksellerid => $account->{vendor_id},
2090
                    basketname   => 'Test Basket',
2091
                }
2092
            }
2093
        );
2094
        my $order = $builder->build_object(
2095
            {
2096
                class => 'Koha::Acquisition::Orders',
2097
                value => {
2098
                    basketno     => $basket->id,
2099
                    orderstatus  => 'new',
2100
                    biblionumber => undef,
2101
                }
2102
            }
2103
        );
2104
        my $ordernumber = $order->ordernumber;
2105
2106
        # Prepare invoice message
2107
        my $filename = 'INVOICE.CEI';
2108
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
2109
        $trans->working_directory($dirname);
2110
2111
        my $mhash = $trans->message_hash();
2112
        $mhash->{message_type} = 'INVOICE';
2113
        $trans->ingest( $mhash, $filename );
2114
2115
        my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
2116
        my $raw_msg         = $invoice_message->raw_msg;
2117
        $raw_msg =~ s/ORDERNUMBER1/$ordernumber/g;
2118
        $raw_msg =~ s/ORDERNUMBER2/$ordernumber/g;
2119
        $invoice_message->update( { raw_msg => $raw_msg } );
2120
2121
        # Clear logger
2122
        $logger->clear();
2123
2124
        # Process the invoice - should block duplicate
2125
        my $error;
2126
        eval {
2127
            process_invoice($invoice_message);
2128
            1;
2129
        } or do {
2130
            $error = $@;
2131
        };
2132
        ok( !$error, 'Invoice processing completed without dying' );
2133
2134
        # Verify duplicate was blocked (only original invoice exists)
2135
        my $duplicate_invoices = $schema->resultset('Aqinvoice')->search(
2136
            {
2137
                invoicenumber => 'INV00003',
2138
                booksellerid  => $account->{vendor_id},
2139
            }
2140
        );
2141
        is( $duplicate_invoices->count, 1, 'Duplicate invoice was blocked' );
2142
2143
        # Verify error was logged
2144
        $logger->error_like(
2145
            qr/Duplicate invoice INV00003 for vendor.*/,
2146
            'Error logged for duplicate invoice'
2147
        );
2148
2149
        # Verify error recorded in edifact_errors table
2150
        my $errors          = $invoice_message->edifact_errors;
2151
        my $duplicate_error = $errors->search( { details => { 'like', '%Duplicate invoice%' } } )->first;
2152
        ok( $duplicate_error, 'Duplicate error recorded in edifact_errors table' );
2153
        like(
2154
            $duplicate_error->details, qr/Duplicate invoice number 'INV00003'/,
2155
            'Error details contain invoice number'
2156
        );
2157
2158
        # Verify message status set to error
2159
        $invoice_message->discard_changes;
2160
        is( $invoice_message->status, 'error', 'Message status set to error' );
2161
2162
        $logger->clear();
2163
        $schema->storage->txn_rollback;
2164
    };
2165
2166
    # Test 3: Same invoice number with different vendor should be allowed
2167
    subtest 'different_vendor_allowed' => sub {
2168
        plan tests => 3;
2169
2170
        $schema->storage->txn_begin;
2171
2172
        # Enable duplicate blocking preference
2173
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice', 1 );
2174
2175
        # Create file transport for local testing
2176
        my $file_transport = $builder->build(
2177
            {
2178
                source => 'FileTransport',
2179
                value  => {
2180
                    name               => 'Test Invoice Transport',
2181
                    transport          => 'local',
2182
                    download_directory => $dirname,
2183
                    upload_directory   => $dirname,
2184
                }
2185
            }
2186
        );
2187
2188
        # Create two different vendors
2189
        my $account1 = $builder->build(
2190
            {
2191
                source => 'VendorEdiAccount',
2192
                value  => {
2193
                    description       => 'test vendor 1',
2194
                    file_transport_id => $file_transport->{file_transport_id},
2195
                    plugin            => '',
2196
                    san               => '5013546027173',
2197
                }
2198
            }
2199
        );
2200
2201
        my $account2 = $builder->build(
2202
            {
2203
                source => 'VendorEdiAccount',
2204
                value  => {
2205
                    description       => 'test vendor 2',
2206
                    file_transport_id => $file_transport->{file_transport_id},
2207
                    plugin            => '',
2208
                    san               => '5013546027999',
2209
                }
2210
            }
2211
        );
2212
2213
        # Create invoice for vendor 1
2214
        my $invoice1 = $builder->build(
2215
            {
2216
                source => 'Aqinvoice',
2217
                value  => {
2218
                    invoicenumber => 'INV00003',
2219
                    booksellerid  => $account1->{vendor_id},
2220
                }
2221
            }
2222
        );
2223
2224
        # Create test basket and order for vendor 2
2225
        my $basket = $builder->build_object(
2226
            {
2227
                class => 'Koha::Acquisition::Baskets',
2228
                value => {
2229
                    booksellerid => $account2->{vendor_id},
2230
                    basketname   => 'Test Basket',
2231
                }
2232
            }
2233
        );
2234
        my $order = $builder->build_object(
2235
            {
2236
                class => 'Koha::Acquisition::Orders',
2237
                value => {
2238
                    basketno     => $basket->id,
2239
                    orderstatus  => 'new',
2240
                    biblionumber => undef,
2241
                }
2242
            }
2243
        );
2244
2245
        # Prepare invoice message for vendor 2 using same file
2246
        my $filename = 'INVOICE.CEI';
2247
        my $trans    = Koha::Edifact::Transport->new( $account2->{id} );
2248
        $trans->working_directory($dirname);
2249
2250
        my $mhash = $trans->message_hash();
2251
        $mhash->{message_type} = 'INVOICE';
2252
        $trans->ingest( $mhash, $filename );
2253
2254
        my $invoice_message = $schema->resultset('EdifactMessage')->search(
2255
            { filename => $filename },
2256
            { order_by => { -desc => 'id' }, rows => 1 }
2257
        )->single;
2258
        my $raw_msg = $invoice_message->raw_msg;
2259
        $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g;
2260
        $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g;
2261
        $raw_msg =~ s/5013546027173/$account2->{san}/g;     # Replace vendor SAN
2262
        $invoice_message->update(
2263
            {
2264
                raw_msg   => $raw_msg,
2265
                vendor_id => $account2->{vendor_id},
2266
                edi_acct  => $account2->{id}
2267
            }
2268
        );
2269
2270
        # Clear logger
2271
        $logger->clear();
2272
2273
        # Process the invoice - should succeed (different vendor)
2274
        my $error;
2275
        eval {
2276
            process_invoice($invoice_message);
2277
            1;
2278
        } or do {
2279
            $error = $@;
2280
        };
2281
        ok( !$error, 'Invoice processing completed without dying' );
2282
2283
        # Verify both invoices exist (one per vendor)
2284
        my $invoices_vendor1 = $schema->resultset('Aqinvoice')->search(
2285
            {
2286
                invoicenumber => 'INV00003',
2287
                booksellerid  => $account1->{vendor_id},
2288
            }
2289
        );
2290
        is( $invoices_vendor1->count, 1, 'Invoice exists for vendor 1' );
2291
2292
        my $invoices_vendor2 = $schema->resultset('Aqinvoice')->search(
2293
            {
2294
                invoicenumber => 'INV00003',
2295
                booksellerid  => $account2->{vendor_id},
2296
            }
2297
        );
2298
        is( $invoices_vendor2->count, 1, 'Invoice allowed for vendor 2 with same invoice number' );
2299
2300
        $logger->clear();
2301
        $schema->storage->txn_rollback;
2302
    };
2303
2304
    # Test 4: Library staff email notification
2305
    subtest 'library_email_notification' => sub {
2306
        plan tests => 5;
2307
2308
        $schema->storage->txn_begin;
2309
2310
        # Enable duplicate blocking and email notifications
2311
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice',               1 );
2312
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice',    1 );
2313
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailAddresses', 'library@example.com' );
2314
2315
        # Create letter templates for notifications (delete first if exist)
2316
        $schema->resultset('Letter')->search(
2317
            {
2318
                module => 'acquisition',
2319
                code   => [ 'EDI_DUP_INV_LIBRARY', 'EDI_DUP_INV_VENDOR' ],
2320
            }
2321
        )->delete;
2322
2323
        $builder->build(
2324
            {
2325
                source => 'Letter',
2326
                value  => {
2327
                    module                 => 'acquisition',
2328
                    code                   => 'EDI_DUP_INV_LIBRARY',
2329
                    branchcode             => '',
2330
                    name                   => 'Test library notification',
2331
                    is_html                => 0,
2332
                    title                  => 'Duplicate Invoice - <<invoicenumber>>',
2333
                    content                => 'Duplicate invoice <<invoicenumber>>. Processing has been blocked.',
2334
                    message_transport_type => 'email',
2335
                    lang                   => 'default',
2336
                }
2337
            }
2338
        );
2339
        $builder->build(
2340
            {
2341
                source => 'Letter',
2342
                value  => {
2343
                    module                 => 'acquisition',
2344
                    code                   => 'EDI_DUP_INV_VENDOR',
2345
                    branchcode             => '',
2346
                    name                   => 'Test vendor notification',
2347
                    is_html                => 0,
2348
                    title                  => 'Duplicate Invoice - <<invoicenumber>>',
2349
                    content                => 'Duplicate invoice <<invoicenumber>>. Please use UNIQUE invoice number.',
2350
                    message_transport_type => 'email',
2351
                    lang                   => 'default',
2352
                }
2353
            }
2354
        );
2355
2356
        # Create file transport for local testing
2357
        my $file_transport = $builder->build(
2358
            {
2359
                source => 'FileTransport',
2360
                value  => {
2361
                    name               => 'Test Invoice Transport',
2362
                    transport          => 'local',
2363
                    download_directory => $dirname,
2364
                    upload_directory   => $dirname,
2365
                }
2366
            }
2367
        );
2368
2369
        # Create vendor EDI account
2370
        my $account = $builder->build(
2371
            {
2372
                source => 'VendorEdiAccount',
2373
                value  => {
2374
                    description       => 'test vendor',
2375
                    file_transport_id => $file_transport->{file_transport_id},
2376
                    plugin            => '',
2377
                    san               => '5013546027173',
2378
                }
2379
            }
2380
        );
2381
2382
        # Create test invoice that already exists
2383
        my $existing_invoice = $builder->build(
2384
            {
2385
                source => 'Aqinvoice',
2386
                value  => {
2387
                    invoicenumber => 'INV00003',
2388
                    booksellerid  => $account->{vendor_id},
2389
                    shipmentdate  => '2020-01-01',
2390
                }
2391
            }
2392
        );
2393
2394
        # Create test basket and order
2395
        my $basket = $builder->build_object(
2396
            {
2397
                class => 'Koha::Acquisition::Baskets',
2398
                value => {
2399
                    booksellerid => $account->{vendor_id},
2400
                    basketname   => 'Test Basket',
2401
                }
2402
            }
2403
        );
2404
        my $order = $builder->build_object(
2405
            {
2406
                class => 'Koha::Acquisition::Orders',
2407
                value => {
2408
                    basketno     => $basket->id,
2409
                    orderstatus  => 'new',
2410
                    biblionumber => undef,
2411
                }
2412
            }
2413
        );
2414
2415
        # Prepare invoice message
2416
        my $filename = 'INVOICE.CEI';
2417
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
2418
        $trans->working_directory($dirname);
2419
2420
        my $mhash = $trans->message_hash();
2421
        $mhash->{message_type} = 'INVOICE';
2422
        $trans->ingest( $mhash, $filename );
2423
2424
        my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
2425
        my $raw_msg         = $invoice_message->raw_msg;
2426
        $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g;
2427
        $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g;
2428
        $invoice_message->update( { raw_msg => $raw_msg } );
2429
2430
        # Process the invoice
2431
        process_invoice($invoice_message);
2432
2433
        # Verify library email was queued in message_queue
2434
        my $library_messages = $schema->resultset('MessageQueue')->search(
2435
            {
2436
                to_address => 'library@example.com',
2437
                status     => [ 'sent', 'pending', 'failed' ],    # Accept any status - queuing is what matters
2438
            }
2439
        );
2440
        is( $library_messages->count, 1, 'Library notification was queued' );
2441
2442
        my $library_message = $library_messages->next;
2443
        like( $library_message->subject, qr/Duplicate Invoice/,           'Library email has correct subject' );
2444
        like( $library_message->content, qr/INV00003/,                    'Library email contains invoice number' );
2445
        like( $library_message->content, qr/Processing has been blocked/, 'Library email contains blocking message' );
2446
2447
        # Verify message was recorded in message_queue for audit trail
2448
        ok( $library_message->message_id, 'Message has ID for audit trail' );
2449
2450
        $schema->storage->txn_rollback;
2451
    };
2452
2453
    # Test 5: Vendor email notification
2454
    subtest 'vendor_email_notification' => sub {
2455
        plan tests => 5;
2456
2457
        $schema->storage->txn_begin;
2458
2459
        # Enable duplicate blocking and email notifications
2460
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice',               1 );
2461
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice',    1 );
2462
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailAddresses', 'library@example.com' );
2463
2464
        # Create letter templates for notifications (delete first if exist)
2465
        $schema->resultset('Letter')->search(
2466
            {
2467
                module => 'acquisition',
2468
                code   => [ 'EDI_DUP_INV_LIBRARY', 'EDI_DUP_INV_VENDOR' ],
2469
            }
2470
        )->delete;
2471
2472
        $builder->build(
2473
            {
2474
                source => 'Letter',
2475
                value  => {
2476
                    module                 => 'acquisition',
2477
                    code                   => 'EDI_DUP_INV_LIBRARY',
2478
                    branchcode             => '',
2479
                    name                   => 'Test library notification',
2480
                    is_html                => 0,
2481
                    title                  => 'Duplicate Invoice - <<invoicenumber>>',
2482
                    content                => 'Duplicate invoice <<invoicenumber>>. Processing has been blocked.',
2483
                    message_transport_type => 'email',
2484
                    lang                   => 'default',
2485
                }
2486
            }
2487
        );
2488
        $builder->build(
2489
            {
2490
                source => 'Letter',
2491
                value  => {
2492
                    module                 => 'acquisition',
2493
                    code                   => 'EDI_DUP_INV_VENDOR',
2494
                    branchcode             => '',
2495
                    name                   => 'Test vendor notification',
2496
                    is_html                => 0,
2497
                    title                  => 'Duplicate Invoice - <<invoicenumber>>',
2498
                    content                => 'Duplicate invoice <<invoicenumber>>. Please use UNIQUE invoice number.',
2499
                    message_transport_type => 'email',
2500
                    lang                   => 'default',
2501
                }
2502
            }
2503
        );
2504
2505
        # Create file transport for local testing
2506
        my $file_transport = $builder->build(
2507
            {
2508
                source => 'FileTransport',
2509
                value  => {
2510
                    name               => 'Test Invoice Transport',
2511
                    transport          => 'local',
2512
                    download_directory => $dirname,
2513
                    upload_directory   => $dirname,
2514
                }
2515
            }
2516
        );
2517
2518
        # Create vendor EDI account
2519
        my $account = $builder->build(
2520
            {
2521
                source => 'VendorEdiAccount',
2522
                value  => {
2523
                    description       => 'test vendor',
2524
                    file_transport_id => $file_transport->{file_transport_id},
2525
                    plugin            => '',
2526
                    san               => '5013546027173',
2527
                }
2528
            }
2529
        );
2530
2531
        # Create vendor contact with EDI error notification enabled
2532
        my $vendor_contact = $builder->build(
2533
            {
2534
                source => 'Aqcontact',
2535
                value  => {
2536
                    name                   => 'Test Vendor Contact',
2537
                    email                  => 'vendor@supplier.com',
2538
                    booksellerid           => $account->{vendor_id},
2539
                    edi_error_notification => 1,
2540
                }
2541
            }
2542
        );
2543
2544
        # Create test invoice that already exists
2545
        my $existing_invoice = $builder->build(
2546
            {
2547
                source => 'Aqinvoice',
2548
                value  => {
2549
                    invoicenumber => 'INV00003',
2550
                    booksellerid  => $account->{vendor_id},
2551
                    shipmentdate  => '2020-01-01',
2552
                }
2553
            }
2554
        );
2555
2556
        # Create test basket and order
2557
        my $basket = $builder->build_object(
2558
            {
2559
                class => 'Koha::Acquisition::Baskets',
2560
                value => {
2561
                    booksellerid => $account->{vendor_id},
2562
                    basketname   => 'Test Basket',
2563
                }
2564
            }
2565
        );
2566
        my $order = $builder->build_object(
2567
            {
2568
                class => 'Koha::Acquisition::Orders',
2569
                value => {
2570
                    basketno     => $basket->id,
2571
                    orderstatus  => 'new',
2572
                    biblionumber => undef,
2573
                }
2574
            }
2575
        );
2576
2577
        # Prepare invoice message
2578
        my $filename = 'INVOICE.CEI';
2579
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
2580
        $trans->working_directory($dirname);
2581
2582
        my $mhash = $trans->message_hash();
2583
        $mhash->{message_type} = 'INVOICE';
2584
        $trans->ingest( $mhash, $filename );
2585
2586
        my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
2587
        my $raw_msg         = $invoice_message->raw_msg;
2588
        $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g;
2589
        $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g;
2590
        $invoice_message->update( { raw_msg => $raw_msg } );
2591
2592
        # Process the invoice
2593
        process_invoice($invoice_message);
2594
2595
        # Verify vendor email was queued in message_queue
2596
        my $vendor_messages = $schema->resultset('MessageQueue')->search(
2597
            {
2598
                to_address => 'vendor@supplier.com',
2599
                status     => [ 'sent', 'pending', 'failed' ],    # Accept any status - queuing is what matters
2600
            }
2601
        );
2602
        is( $vendor_messages->count, 1, 'Vendor notification was queued' );
2603
2604
        my $vendor_message = $vendor_messages->next;
2605
        like( $vendor_message->subject, qr/Duplicate Invoice/,     'Vendor email has correct subject' );
2606
        like( $vendor_message->content, qr/INV00003/,              'Vendor email contains invoice number' );
2607
        like( $vendor_message->content, qr/UNIQUE invoice number/, 'Vendor email contains action required message' );
2608
2609
        # Verify message was recorded for audit trail
2610
        ok( $vendor_message->message_id, 'Message has ID for audit trail' );
2611
2612
        $schema->storage->txn_rollback;
2613
    };
2614
2615
    # Test 6: Multiple email recipients
2616
    subtest 'multiple_email_recipients' => sub {
2617
        plan tests => 3;
2618
2619
        $schema->storage->txn_begin;
2620
2621
        # Enable duplicate blocking and email notifications with multiple addresses
2622
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice',            1 );
2623
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice', 1 );
2624
        t::lib::Mocks::mock_preference(
2625
            'EdiBlockDuplicateInvoiceEmailAddresses',
2626
            'library1@example.com, library2@example.com, library3@example.com'
2627
        );
2628
2629
        # Create letter templates (delete first if exist)
2630
        $schema->resultset('Letter')->search(
2631
            {
2632
                module => 'acquisition',
2633
                code   => 'EDI_DUP_INV_LIBRARY',
2634
            }
2635
        )->delete;
2636
2637
        $builder->build(
2638
            {
2639
                source => 'Letter',
2640
                value  => {
2641
                    module                 => 'acquisition',
2642
                    code                   => 'EDI_DUP_INV_LIBRARY',
2643
                    branchcode             => '',
2644
                    name                   => 'Test library notification',
2645
                    is_html                => 0,
2646
                    title                  => 'Duplicate Invoice',
2647
                    content                => 'Duplicate invoice.',
2648
                    message_transport_type => 'email',
2649
                    lang                   => 'default',
2650
                }
2651
            }
2652
        );
2653
2654
        # Create file transport for local testing
2655
        my $file_transport = $builder->build(
2656
            {
2657
                source => 'FileTransport',
2658
                value  => {
2659
                    name               => 'Test Invoice Transport',
2660
                    transport          => 'local',
2661
                    download_directory => $dirname,
2662
                    upload_directory   => $dirname,
2663
                }
2664
            }
2665
        );
2666
2667
        # Create vendor EDI account
2668
        my $account = $builder->build(
2669
            {
2670
                source => 'VendorEdiAccount',
2671
                value  => {
2672
                    description       => 'test vendor',
2673
                    file_transport_id => $file_transport->{file_transport_id},
2674
                    plugin            => '',
2675
                    san               => '5013546027173',
2676
                }
2677
            }
2678
        );
2679
2680
        # Create test invoice that already exists
2681
        my $existing_invoice = $builder->build(
2682
            {
2683
                source => 'Aqinvoice',
2684
                value  => {
2685
                    invoicenumber => 'INV00003',
2686
                    booksellerid  => $account->{vendor_id},
2687
                }
2688
            }
2689
        );
2690
2691
        # Create test basket and order
2692
        my $basket = $builder->build_object(
2693
            {
2694
                class => 'Koha::Acquisition::Baskets',
2695
                value => {
2696
                    booksellerid => $account->{vendor_id},
2697
                    basketname   => 'Test Basket',
2698
                }
2699
            }
2700
        );
2701
        my $order = $builder->build_object(
2702
            {
2703
                class => 'Koha::Acquisition::Orders',
2704
                value => {
2705
                    basketno     => $basket->id,
2706
                    orderstatus  => 'new',
2707
                    biblionumber => undef,
2708
                }
2709
            }
2710
        );
2711
2712
        # Prepare invoice message
2713
        my $filename = 'INVOICE.CEI';
2714
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
2715
        $trans->working_directory($dirname);
2716
2717
        my $mhash = $trans->message_hash();
2718
        $mhash->{message_type} = 'INVOICE';
2719
        $trans->ingest( $mhash, $filename );
2720
2721
        my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
2722
        my $raw_msg         = $invoice_message->raw_msg;
2723
        $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g;
2724
        $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g;
2725
        $invoice_message->update( { raw_msg => $raw_msg } );
2726
2727
        # Process the invoice
2728
        process_invoice($invoice_message);
2729
2730
        # Verify all three library emails were queued
2731
        my $library_messages = $schema->resultset('MessageQueue')->search(
2732
            {
2733
                to_address => [ 'library1@example.com', 'library2@example.com', 'library3@example.com' ],
2734
                status     => [ 'sent', 'pending', 'failed' ],    # Accept any status - queuing is what matters
2735
            }
2736
        );
2737
        is( $library_messages->count, 3, 'Three library notification emails queued' );
2738
2739
        my @to_addresses = map { $_->to_address } $library_messages->all;
2740
        ok( ( grep { $_ eq 'library1@example.com' } @to_addresses ), 'Email sent to library1' );
2741
        ok( ( grep { $_ eq 'library2@example.com' } @to_addresses ), 'Email sent to library2' );
2742
2743
        $schema->storage->txn_rollback;
2744
    };
2745
2746
    # Test 7: Invalid email handling
2747
    subtest 'invalid_email_handling' => sub {
2748
        plan tests => 3;
2749
2750
        $schema->storage->txn_begin;
2751
2752
        # Enable duplicate blocking and email notifications with invalid addresses
2753
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoice',               1 );
2754
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailNotice',    1 );
2755
        t::lib::Mocks::mock_preference( 'EdiBlockDuplicateInvoiceEmailAddresses', 'invalid-email, valid@example.com' );
2756
2757
        # Create letter templates (delete first if exist)
2758
        $schema->resultset('Letter')->search(
2759
            {
2760
                module => 'acquisition',
2761
                code   => [ 'EDI_DUP_INV_LIBRARY', 'EDI_DUP_INV_VENDOR' ],
2762
            }
2763
        )->delete;
2764
2765
        $builder->build(
2766
            {
2767
                source => 'Letter',
2768
                value  => {
2769
                    module                 => 'acquisition',
2770
                    code                   => 'EDI_DUP_INV_LIBRARY',
2771
                    branchcode             => '',
2772
                    name                   => 'Test library notification',
2773
                    is_html                => 0,
2774
                    title                  => 'Duplicate Invoice',
2775
                    content                => 'Duplicate invoice.',
2776
                    message_transport_type => 'email',
2777
                    lang                   => 'default',
2778
                }
2779
            }
2780
        );
2781
        $builder->build(
2782
            {
2783
                source => 'Letter',
2784
                value  => {
2785
                    module                 => 'acquisition',
2786
                    code                   => 'EDI_DUP_INV_VENDOR',
2787
                    branchcode             => '',
2788
                    name                   => 'Test vendor notification',
2789
                    is_html                => 0,
2790
                    title                  => 'Duplicate Invoice',
2791
                    content                => 'Duplicate invoice.',
2792
                    message_transport_type => 'email',
2793
                    lang                   => 'default',
2794
                }
2795
            }
2796
        );
2797
2798
        # Create file transport for local testing
2799
        my $file_transport = $builder->build(
2800
            {
2801
                source => 'FileTransport',
2802
                value  => {
2803
                    name               => 'Test Invoice Transport',
2804
                    transport          => 'local',
2805
                    download_directory => $dirname,
2806
                    upload_directory   => $dirname,
2807
                }
2808
            }
2809
        );
2810
2811
        # Create vendor EDI account
2812
        my $account = $builder->build(
2813
            {
2814
                source => 'VendorEdiAccount',
2815
                value  => {
2816
                    description       => 'test vendor',
2817
                    file_transport_id => $file_transport->{file_transport_id},
2818
                    plugin            => '',
2819
                    san               => '5013546027173',
2820
                }
2821
            }
2822
        );
2823
2824
        # Create vendor contact with invalid email
2825
        my $vendor_contact = $builder->build(
2826
            {
2827
                source => 'Aqcontact',
2828
                value  => {
2829
                    name                   => 'Test Vendor Contact',
2830
                    email                  => 'not-an-email',
2831
                    booksellerid           => $account->{vendor_id},
2832
                    edi_error_notification => 1,
2833
                }
2834
            }
2835
        );
2836
2837
        # Create test invoice that already exists
2838
        my $existing_invoice = $builder->build(
2839
            {
2840
                source => 'Aqinvoice',
2841
                value  => {
2842
                    invoicenumber => 'INV00003',
2843
                    booksellerid  => $account->{vendor_id},
2844
                }
2845
            }
2846
        );
2847
2848
        # Create test basket and order
2849
        my $basket = $builder->build_object(
2850
            {
2851
                class => 'Koha::Acquisition::Baskets',
2852
                value => {
2853
                    booksellerid => $account->{vendor_id},
2854
                    basketname   => 'Test Basket',
2855
                }
2856
            }
2857
        );
2858
        my $order = $builder->build_object(
2859
            {
2860
                class => 'Koha::Acquisition::Orders',
2861
                value => {
2862
                    basketno     => $basket->id,
2863
                    orderstatus  => 'new',
2864
                    biblionumber => undef,
2865
                }
2866
            }
2867
        );
2868
2869
        # Prepare invoice message
2870
        my $filename = 'INVOICE.CEI';
2871
        my $trans    = Koha::Edifact::Transport->new( $account->{id} );
2872
        $trans->working_directory($dirname);
2873
2874
        my $mhash = $trans->message_hash();
2875
        $mhash->{message_type} = 'INVOICE';
2876
        $trans->ingest( $mhash, $filename );
2877
2878
        my $invoice_message = $schema->resultset('EdifactMessage')->find( { filename => $filename } );
2879
        my $raw_msg         = $invoice_message->raw_msg;
2880
        $raw_msg =~ s/ORDERNUMBER1/$order->ordernumber/g;
2881
        $raw_msg =~ s/ORDERNUMBER2/$order->ordernumber/g;
2882
        $invoice_message->update( { raw_msg => $raw_msg } );
2883
2884
        # Clear logger
2885
        $logger->clear();
2886
2887
        # Process the invoice
2888
        process_invoice($invoice_message);
2889
2890
        # Verify only valid email was queued (invalid email skipped)
2891
        my $valid_messages = $schema->resultset('MessageQueue')->search(
2892
            {
2893
                to_address => 'valid@example.com',
2894
                status     => [ 'sent', 'pending', 'failed' ],    # Accept any status - queuing is what matters
2895
            }
2896
        );
2897
        is( $valid_messages->count, 1, 'Only valid library email queued' );
2898
2899
        # Verify no message for invalid email
2900
        my $invalid_messages = $schema->resultset('MessageQueue')->search(
2901
            {
2902
                to_address => 'invalid-email',
2903
            }
2904
        );
2905
        is( $invalid_messages->count, 0, 'No message queued for invalid email' );
2906
2907
        # Verify invalid vendor contact email was logged
2908
        $logger->warn_like(
2909
            qr/Invalid vendor contact email address/,
2910
            'Warning logged for invalid vendor contact email'
2911
        );
2912
2913
        $logger->clear();
2914
        $schema->storage->txn_rollback;
2915
    };
2916
2917
    $schema->storage->txn_rollback;
2918
};

Return to bug 41297