Bug 34825: Merge Letters.t into t/db/Letters.t
[koha.git] / t / db_dependent / Letters.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Copyright (C) 2013 Equinox Software, Inc.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21 use Test::More tests => 89;
22 use Test::MockModule;
23 use Test::Warn;
24 use Test::Exception;
25
26 use Email::Sender::Failure;
27
28 use MARC::Record;
29
30 use utf8;
31
32 my ( $email_object, $sendmail_params );
33
34 my $email_sender_module = Test::MockModule->new('Email::Stuffer');
35 $email_sender_module->mock(
36     'send_or_die',
37     sub {
38         ( $email_object, $sendmail_params ) = @_;
39         my $str = $email_object->email->as_string;
40         unlike $str, qr/I =C3=A2=C2=99=C2=A5 Koha=/, "Content is not double encoded";
41         warn "Fake send_or_die";
42     }
43 );
44
45 use_ok('C4::Context');
46 use_ok('C4::Members');
47 use_ok('C4::Acquisition', qw( NewBasket ));
48 use_ok('C4::Biblio', qw( AddBiblio GetBiblioData ));
49 use_ok('C4::Letters', qw( GetMessageTransportTypes GetMessage EnqueueLetter GetQueuedMessages SendQueuedMessages ResendMessage GetLetters GetPreparedLetter SendAlerts ));
50 use t::lib::Mocks;
51 use t::lib::TestBuilder;
52 use Koha::Database;
53 use Koha::DateUtils qw( dt_from_string output_pref );
54 use Koha::Acquisition::Booksellers;
55 use Koha::Acquisition::Bookseller::Contacts;
56 use Koha::Acquisition::Orders;
57 use Koha::Libraries;
58 use Koha::Notice::Templates;
59 use Koha::Patrons;
60 use Koha::Subscriptions;
61 my $schema = Koha::Database->schema;
62 $schema->storage->txn_begin();
63
64 my $builder = t::lib::TestBuilder->new;
65 my $dbh = C4::Context->dbh;
66
67 $dbh->do(q|DELETE FROM letter|);
68 $dbh->do(q|DELETE FROM message_queue|);
69 $dbh->do(q|DELETE FROM message_transport_types|);
70
71 my $library = $builder->build({
72     source => 'Branch',
73     value  => {
74         branchemail      => 'branchemail@address.com',
75         branchreplyto    => 'branchreplyto@address.com',
76         branchreturnpath => 'branchreturnpath@address.com',
77     }
78 });
79 my $patron_category = $builder->build({ source => 'Category' })->{categorycode};
80 my $date = dt_from_string;
81 my $borrowernumber = Koha::Patron->new({
82     firstname    => 'Jane',
83     surname      => 'Smith',
84     categorycode => $patron_category,
85     branchcode   => $library->{branchcode},
86     dateofbirth  => $date,
87     smsalertnumber => undef,
88 })->store->borrowernumber;
89
90 my $marc_record = MARC::Record->new;
91 my( $biblionumber, $biblioitemnumber ) = AddBiblio( $marc_record, '' );
92
93
94
95 # GetMessageTransportTypes
96 my $mtts = C4::Letters::GetMessageTransportTypes();
97 is( @$mtts, 0, 'GetMessageTransportTypes returns the correct number of message types' );
98
99 $dbh->do(q|
100     INSERT INTO message_transport_types( message_transport_type ) VALUES ('email'), ('phone'), ('print'), ('sms')
101 |);
102 $mtts = C4::Letters::GetMessageTransportTypes();
103 is_deeply( $mtts, ['email', 'phone', 'print', 'sms'], 'GetMessageTransportTypes returns all values' );
104
105
106 # EnqueueLetter
107 is( C4::Letters::EnqueueLetter(), undef, 'EnqueueLetter without argument returns undef' );
108
109 my $my_message = {
110     borrowernumber         => $borrowernumber,
111     message_transport_type => 'sms',
112     to_address             => undef,
113     from_address           => 'from@example.com',
114 };
115 my $message_id = C4::Letters::EnqueueLetter($my_message);
116 is( $message_id, undef, 'EnqueueLetter without the letter argument returns undef' );
117
118 delete $my_message->{message_transport_type};
119 $my_message->{letter} = {
120     content      => 'I ♥ Koha',
121     title        => '啤酒 is great',
122     metadata     => 'metadata',
123     code         => 'TEST_MESSAGE',
124     content_type => 'text/plain',
125 };
126
127 $message_id = C4::Letters::EnqueueLetter($my_message);
128 is( $message_id, undef, 'EnqueueLetter without the message type argument argument returns undef' );
129
130 $my_message->{message_transport_type} = 'sms';
131 $message_id = C4::Letters::EnqueueLetter($my_message);
132 ok(defined $message_id && $message_id > 0, 'new message successfully queued');
133
134
135 # GetQueuedMessages
136 my $messages = C4::Letters::GetQueuedMessages();
137 is( @$messages, 1, 'GetQueuedMessages without argument returns all the entries' );
138
139 $messages = C4::Letters::GetQueuedMessages({ borrowernumber => $borrowernumber });
140 is( @$messages, 1, 'one message stored for the borrower' );
141 is( $messages->[0]->{message_id}, $message_id, 'EnqueueLetter returns the message id correctly' );
142 is( $messages->[0]->{borrowernumber}, $borrowernumber, 'EnqueueLetter stores the borrower number correctly' );
143 is( $messages->[0]->{subject}, $my_message->{letter}->{title}, 'EnqueueLetter stores the subject correctly' );
144 is( $messages->[0]->{content}, $my_message->{letter}->{content}, 'EnqueueLetter stores the content correctly' );
145 is( $messages->[0]->{message_transport_type}, $my_message->{message_transport_type}, 'EnqueueLetter stores the message type correctly' );
146 is( $messages->[0]->{status}, 'pending', 'EnqueueLetter stores the status pending correctly' );
147 isnt( $messages->[0]->{time_queued}, undef, 'Time queued inserted by default in message_queue table' );
148 is( $messages->[0]->{updated_on}, $messages->[0]->{time_queued}, 'Time status changed equals time queued when created in message_queue table' );
149 is( $messages->[0]->{failure_code}, '', 'Failure code for successful message correctly empty');
150
151 # Setting time_queued to something else than now
152 my $yesterday = dt_from_string->subtract( days => 1 );
153 Koha::Notice::Messages->find($messages->[0]->{message_id})->time_queued($yesterday)->store;
154
155 # SendQueuedMessages
156 my $messages_processed = C4::Letters::SendQueuedMessages( { type => 'email' });
157 is($messages_processed, 0, 'No queued messages processed if type limit passed with unused type');
158 $messages_processed = C4::Letters::SendQueuedMessages( { type => 'sms' });
159 is($messages_processed, 1, 'All queued messages processed, found correct number of messages with type limit');
160 $messages = C4::Letters::GetQueuedMessages({ borrowernumber => $borrowernumber });
161 is(
162     $messages->[0]->{status},
163     'failed',
164     'message marked failed if tried to send SMS message for borrower with no smsalertnumber set (bug 11208)'
165 );
166 is(
167     $messages->[0]->{failure_code},
168     'MISSING_SMS',
169     'Correct failure code set for borrower with no smsalertnumber set'
170 );
171 isnt($messages->[0]->{updated_on}, $messages->[0]->{time_queued}, 'Time status changed differs from time queued when status changes' );
172 is(dt_from_string($messages->[0]->{time_queued}), $yesterday, 'Time queued remaines inmutable' );
173
174 # ResendMessage
175 my $resent = C4::Letters::ResendMessage($messages->[0]->{message_id});
176 my $message = C4::Letters::GetMessage( $messages->[0]->{message_id});
177 is( $resent, 1, 'The message should have been resent' );
178 is($message->{status},'pending', 'ResendMessage sets status to pending correctly (bug 12426)');
179 $resent = C4::Letters::ResendMessage($messages->[0]->{message_id});
180 is( $resent, 0, 'The message should not have been resent again' );
181 $resent = C4::Letters::ResendMessage();
182 is( $resent, undef, 'ResendMessage should return undef if not message_id given' );
183
184 # Delivery notes
185 is( $messages->[0]->{failure_code}, 'MISSING_SMS', 'Failure code set correctly for no smsalertnumber correctly set' );
186
187 # GetLetters
188 my $letters = C4::Letters::GetLetters();
189 is( @$letters, 0, 'GetLetters returns the correct number of letters' );
190
191 my $title = q|<<branches.branchname>> - <<status>>|;
192 my $content = q{Dear <<borrowers.firstname>> <<borrowers.surname>>,
193 According to our current records, you have items that are overdue.Your library does not charge late fines, but please return or renew them at the branch below as soon as possible.
194
195 <<branches.branchname>>
196 <<branches.branchaddress1>>
197 URL: <<OPACBaseURL>>
198
199 The following item(s) is/are currently <<status>>:
200
201 <item> <<count>>. <<items.itemcallnumber>>, Barcode: <<items.barcode>> </item>
202
203 Thank-you for your prompt attention to this matter.
204 Don't forget your date of birth: <<borrowers.dateofbirth>>.
205 Look at this wonderful biblio timestamp: <<biblio.timestamp>>.
206 };
207
208 $dbh->do( q|INSERT INTO letter(branchcode,module,code,name,is_html,title,content,message_transport_type) VALUES (?,'my module','my code','my name',1,?,?,'email')|, undef, $library->{branchcode}, $title, $content );
209 $letters = C4::Letters::GetLetters();
210 is( @$letters, 1, 'GetLetters returns the correct number of letters' );
211 is( $letters->[0]->{module}, 'my module', 'GetLetters gets the module correctly' );
212 is( $letters->[0]->{code}, 'my code', 'GetLetters gets the code correctly' );
213 is( $letters->[0]->{name}, 'my name', 'GetLetters gets the name correctly' );
214
215 my $data_1 = { module => 'blah', code => 'ISBN', name => 'book' };
216 my $data_2 = { module => 'blah', code => 'ISSN' };
217 $builder->build_object( { class => 'Koha::Notice::Templates', value => $data_1 } );
218 $builder->build_object( { class => 'Koha::Notice::Templates', value => $data_2 } );
219
220 $letters = GetLetters( { module => 'blah' } );
221 is( scalar(@$letters), 2, 'GetLetters returns the 2 inserted letters' );
222
223 my ($ISBN_letter) = grep { $_->{code} eq 'ISBN' } @$letters;
224 is( $ISBN_letter->{name}, 'book', 'letter name for "ISBN" letter is book' );
225
226 # GetPreparedLetter
227 t::lib::Mocks::mock_preference('OPACBaseURL', 'http://thisisatest.com');
228 t::lib::Mocks::mock_preference( 'SendAllEmailsTo', '' );
229
230 my $sms_content = 'This is a SMS for an <<status>>';
231 $dbh->do( q|INSERT INTO letter(branchcode,module,code,name,is_html,title,content,message_transport_type) VALUES (?,'my module','my code','my name',1,'my title',?,'sms')|, undef, $library->{branchcode}, $sms_content );
232
233 my $tables = {
234     borrowers => $borrowernumber,
235     branches => $library->{branchcode},
236     biblio => $biblionumber,
237 };
238 my $substitute = {
239     status => 'overdue',
240 };
241 my $repeat = [
242     {
243         itemcallnumber => 'my callnumber1',
244         barcode        => '1234',
245     },
246     {
247         itemcallnumber => 'my callnumber2',
248         barcode        => '5678',
249     },
250 ];
251 my $prepared_letter = GetPreparedLetter((
252     module      => 'my module',
253     branchcode  => $library->{branchcode},
254     letter_code => 'my code',
255     tables      => $tables,
256     substitute  => $substitute,
257     repeat      => $repeat,
258 ));
259 my $retrieved_library = Koha::Libraries->find($library->{branchcode});
260 my $my_title_letter = $retrieved_library->branchname . qq| - $substitute->{status}|;
261 my $biblio_timestamp = dt_from_string( GetBiblioData($biblionumber)->{timestamp} );
262 my $my_content_letter = qq|Dear Jane Smith,
263 According to our current records, you have items that are overdue.Your library does not charge late fines, but please return or renew them at the branch below as soon as possible.
264
265 |.$retrieved_library->branchname.qq|
266 |.$retrieved_library->branchaddress1.qq|
267 URL: http://thisisatest.com
268
269 The following item(s) is/are currently $substitute->{status}:
270
271 <item> 1. $repeat->[0]->{itemcallnumber}, Barcode: $repeat->[0]->{barcode} </item>
272 <item> 2. $repeat->[1]->{itemcallnumber}, Barcode: $repeat->[1]->{barcode} </item>
273
274 Thank-you for your prompt attention to this matter.
275 Don't forget your date of birth: | . output_pref({ dt => $date, dateonly => 1 }) . q|.
276 Look at this wonderful biblio timestamp: | . output_pref({ dt => $biblio_timestamp })  . ".\n";
277
278 is( $prepared_letter->{title}, $my_title_letter, 'GetPreparedLetter returns the title correctly' );
279 is( $prepared_letter->{content}, $my_content_letter, 'GetPreparedLetter returns the content correctly' );
280
281 $prepared_letter = GetPreparedLetter((
282     module                 => 'my module',
283     branchcode             => $library->{branchcode},
284     letter_code            => 'my code',
285     tables                 => $tables,
286     substitute             => $substitute,
287     repeat                 => $repeat,
288     message_transport_type => 'sms',
289 ));
290 $my_content_letter = qq|This is a SMS for an $substitute->{status}|;
291 is( $prepared_letter->{content}, $my_content_letter, 'GetPreparedLetter returns the content correctly' );
292
293 warning_is {
294     $prepared_letter = GetPreparedLetter((
295         module                 => 'my module',
296         branchcode             => $library->{branchcode},
297         letter_code            => 'my code',
298         tables                 => $tables,
299         substitute             => { status => undef },
300         repeat                 => $repeat,
301         message_transport_type => 'sms',
302     ));
303 }
304 undef, "No warning if GetPreparedLetter called with substitute containing undefined value";
305 is( $prepared_letter->{content}, q|This is a SMS for an |, 'GetPreparedLetter returns the content correctly when substitute contains undefined value' );
306
307 $dbh->do(q{INSERT INTO letter (module, code, name, title, content) VALUES ('test_date','TEST_DATE','Test dates','A title with a timestamp: <<biblio.timestamp>>','This one only contains the date: <<biblio.timestamp | dateonly>>.');});
308 $prepared_letter = GetPreparedLetter((
309     module                 => 'test_date',
310     branchcode             => '',
311     letter_code            => 'test_date',
312     tables                 => $tables,
313     substitute             => $substitute,
314     repeat                 => $repeat,
315 ));
316 is( $prepared_letter->{content}, q|This one only contains the date: | . output_pref({ dt => $date, dateonly => 1 }) . q|.|, 'dateonly test 1' );
317
318 $dbh->do(q{UPDATE letter SET content = 'And also this one:<<timestamp | dateonly>>.' WHERE code = 'test_date';});
319 $prepared_letter = GetPreparedLetter((
320     module                 => 'test_date',
321     branchcode             => '',
322     letter_code            => 'test_date',
323     tables                 => $tables,
324     substitute             => $substitute,
325     repeat                 => $repeat,
326 ));
327 is( $prepared_letter->{content}, q|And also this one:| . output_pref({ dt => $date, dateonly => 1 }) . q|.|, 'dateonly test 2' );
328
329 $dbh->do(q{UPDATE letter SET content = 'And also this one:<<timestamp|dateonly >>.' WHERE code = 'test_date';});
330 $prepared_letter = GetPreparedLetter((
331     module                 => 'test_date',
332     branchcode             => '',
333     letter_code            => 'test_date',
334     tables                 => $tables,
335     substitute             => $substitute,
336     repeat                 => $repeat,
337 ));
338 is( $prepared_letter->{content}, q|And also this one:| . output_pref({ dt => $date, dateonly => 1 }) . q|.|, 'dateonly test 3' );
339
340 t::lib::Mocks::mock_preference( 'TimeFormat', '12hr' );
341 my $yesterday_night = $date->clone->add( days => -1 )->set_hour(22);
342 $dbh->do(q|UPDATE biblio SET timestamp = ? WHERE biblionumber = ?|, undef, $yesterday_night, $biblionumber );
343 $dbh->do(q{UPDATE letter SET content = 'And also this one:<<timestamp>>.' WHERE code = 'test_date';});
344 $prepared_letter = GetPreparedLetter((
345     module                 => 'test_date',
346     branchcode             => '',
347     letter_code            => 'test_date',
348     tables                 => $tables,
349     substitute             => $substitute,
350     repeat                 => $repeat,
351 ));
352 is( $prepared_letter->{content}, q|And also this one:| . output_pref({ dt => $yesterday_night }) . q|.|, 'dateonly test 3' );
353
354 $dbh->do(q{INSERT INTO letter (module, code, name, title, content) VALUES ('claimacquisition','TESTACQCLAIM','Acquisition Claim','Item Not Received','<<aqbooksellers.name>>|<<aqcontacts.name>>|<order>Ordernumber <<aqorders.ordernumber>> (<<biblio.title>>) (<<aqorders.quantity>> ordered)</order>');});
355 $dbh->do(q{INSERT INTO letter (module, code, name, title, content) VALUES ('orderacquisition','TESTACQORDER','Acquisition Order','Order','<<aqbooksellers.name>>|<<aqcontacts.name>>|<order>Ordernumber <<aqorders.ordernumber>> (<<biblio.title>>) (<<aqorders.quantity>> ordered)</order> Basket name: [% basket.basketname %]');});
356
357 my $testacqorder2_content = <<EOF;
358 [%- USE Price -%]
359 [% bookseller.name %]
360 [% FOREACH order IN orders %]
361 Ordernumber [% order.ordernumber %] [% order.quantity %] [% order.listprice | \$Price %]
362 [% END %]
363 EOF
364
365 $dbh->do("INSERT INTO letter (module, code, name, title, content) VALUES ('orderacquisition','TESTACQORDER2','Acquisition Order','Order','$testacqorder2_content');");
366
367 my $popito = $builder->build({
368     source => 'Aqbookseller',
369     value  => { name => 'Popito' }
370 });
371
372 my $order_1 = $builder->build({
373     source => 'Aqorder',
374     value  => {
375        quantity => 2,
376        listprice => '12.00'
377     }
378 });
379
380 my $order_2 = $builder->build({
381     source => 'Aqorder',
382     value  => {
383        quantity => 1,
384        listprice => '23.50'
385     }
386 });
387
388 $prepared_letter = GetPreparedLetter((
389     module                 => 'orderacquisition',
390     branchcode             => '',
391     letter_code            => 'TESTACQORDER2',
392     tables                 => { 'aqbooksellers' => $popito->{id} },
393     loops                  => {
394         aqorders => [ $order_1->{ordernumber}, $order_2->{ordernumber} ]
395     }
396 ));
397
398 my $testacqorder2_expected = qq|Popito
399
400 Ordernumber | . $order_1->{ordernumber} . qq| 2 12.00
401
402 Ordernumber | . $order_2->{ordernumber} . qq| 1 23.50
403
404 |;
405
406 is($prepared_letter->{content}, $testacqorder2_expected);
407
408 # Test that _parseletter doesn't modify its parameters bug 15429
409 {
410     my $values = { dateexpiry => '2015-12-13', };
411     C4::Letters::_parseletter($prepared_letter, 'borrowers', $values);
412     is( $values->{dateexpiry}, '2015-12-13', "_parseletter doesn't modify its parameters" );
413 }
414
415 # Correctly format dateexpiry
416 {
417     my $values = { dateexpiry => '2015-12-13', };
418
419     t::lib::Mocks::mock_preference('dateformat', 'metric');
420     t::lib::Mocks::mock_preference('timeformat', '24hr');
421     my $letter = C4::Letters::_parseletter({ content => "expiry on <<borrowers.dateexpiry>>"}, 'borrowers', $values);
422     is( $letter->{content}, 'expiry on 13/12/2015' );
423
424     t::lib::Mocks::mock_preference('dateformat', 'metric');
425     t::lib::Mocks::mock_preference('timeformat', '12hr');
426     $letter = C4::Letters::_parseletter({ content => "expiry on <<borrowers.dateexpiry>>"}, 'borrowers', $values);
427     is( $letter->{content}, 'expiry on 13/12/2015' );
428 }
429
430 my $bookseller = Koha::Acquisition::Bookseller->new(
431     {
432         name => "my vendor",
433         address1 => "bookseller's address",
434         phone => "0123456",
435         active => 1,
436         deliverytime => 5,
437     }
438 )->store;
439 my $booksellerid = $bookseller->id;
440
441 Koha::Acquisition::Bookseller::Contact->new( { name => 'John Smith',  phone => '0123456x1', claimacquisition => 1, orderacquisition => 1, booksellerid => $booksellerid } )->store;
442 Koha::Acquisition::Bookseller::Contact->new( { name => 'Leo Tolstoy', phone => '0123456x2', claimissues      => 1, booksellerid => $booksellerid } )->store;
443 my $basketno = NewBasket($booksellerid, 1, 'The basket name');
444
445 my $budgetid = C4::Budgets::AddBudget({
446     budget_code => "budget_code_test_letters",
447     budget_name => "budget_name_test_letters",
448 });
449
450 my $bib = MARC::Record->new();
451 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
452     $bib->append_fields(
453         MARC::Field->new('200', ' ', ' ', a => 'Silence in the library'),
454     );
455 } else {
456     $bib->append_fields(
457         MARC::Field->new('245', ' ', ' ', a => 'Silence in the library'),
458     );
459 }
460
461 my $logged_in_user = $builder->build_object(
462     {
463         class => 'Koha::Patrons',
464         value => {
465             branchcode => $library->{branchcode},
466             email      => 'some@email.com'
467         }
468     }
469 );
470
471 t::lib::Mocks::mock_userenv({ patron => $logged_in_user });
472
473 ($biblionumber, $biblioitemnumber) = AddBiblio($bib, '');
474 my $order = Koha::Acquisition::Order->new(
475     {
476         basketno => $basketno,
477         quantity => 1,
478         biblionumber => $biblionumber,
479         budget_id => $budgetid,
480     }
481 )->store;
482 my $ordernumber = $order->ordernumber;
483
484 Koha::Acquisition::Baskets->find( $basketno )->close;
485 my $err;
486 warning_like {
487     $err = SendAlerts( 'claimacquisition', [ $ordernumber ], 'TESTACQCLAIM' ) }
488     qr/^Bookseller .* without emails at/,
489     "SendAlerts prints a warning";
490 is($err->{'error'}, 'no_email', "Trying to send an alert when there's no e-mail results in an error");
491
492 $bookseller = Koha::Acquisition::Booksellers->find( $booksellerid );
493 $bookseller->contacts->next->email('testemail@mydomain.com')->store;
494
495 # Ensure that the preference 'ClaimsLog' is set to logging
496 t::lib::Mocks::mock_preference( 'ClaimsLog', 'on' );
497
498 # SendAlerts needs branchemail or KohaAdminEmailAddress as sender
499 t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'library@domain.com' );
500
501 {
502 warning_like {
503     $err = SendAlerts( 'orderacquisition', $basketno , 'TESTACQORDER' ) }
504     qr|Fake send_or_die|,
505     "SendAlerts is using the mocked send_or_die routine (orderacquisition)";
506 is($err, 1, "Successfully sent order.");
507 is($email_object->email->header('To'), 'testemail@mydomain.com', "mailto correct in sent order");
508 is($email_object->email->body, 'my vendor|John Smith|Ordernumber ' . $ordernumber . ' (Silence in the library) (1 ordered) Basket name: The basket name', 'Order notice text constructed successfully');
509
510 my $mocked_koha_email = Test::MockModule->new('Koha::Email');
511 $mocked_koha_email->mock( 'send_or_die', sub {
512     Email::Sender::Failure->throw('something went wrong');
513 });
514
515 warning_like {
516     $err = SendAlerts( 'orderacquisition', $basketno , 'TESTACQORDER' ); }
517     qr{something went wrong},
518     'Warning is printed';
519
520 is($err->{error}, 'something went wrong', "Send exception, error message returned");
521
522 $dbh->do(q{DELETE FROM letter WHERE code = 'TESTACQORDER';});
523 warning_like {
524     $err = SendAlerts( 'orderacquisition', $basketno , 'TESTACQORDER' ) }
525     qr/No orderacquisition TESTACQORDER letter transported by email/,
526     "GetPreparedLetter warns about missing notice template";
527 is($err->{'error'}, 'no_letter', "No TESTACQORDER letter was defined.");
528 }
529
530 {
531 warning_like {
532     $err = SendAlerts( 'claimacquisition', [ $ordernumber ], 'TESTACQCLAIM' ) }
533     qr|Fake send_or_die|,
534     "SendAlerts is using the mocked send_or_die routine";
535
536 is($err, 1, "Successfully sent claim");
537 is($email_object->email->header('To'), 'testemail@mydomain.com', "mailto correct in sent claim");
538 is($email_object->email->body, 'my vendor|John Smith|Ordernumber ' . $ordernumber . ' (Silence in the library) (1 ordered)', 'Claim notice text constructed successfully');
539
540 my $mocked_koha_email = Test::MockModule->new('Koha::Email');
541 $mocked_koha_email->mock( 'send_or_die', sub {
542     Email::Sender::Failure->throw('something went wrong');
543 });
544
545 warning_like {
546     $err = SendAlerts( 'claimacquisition', [ $ordernumber ] , 'TESTACQCLAIM' ); }
547     qr{something went wrong},
548     'Warning is printed';
549
550 is($err->{error}, 'something went wrong', "Send exception, error message returned");
551 }
552
553 {
554 use C4::Serials qw( NewSubscription GetSerials findSerialsByStatus ModSerialStatus );
555
556 my $notes = 'notes';
557 my $internalnotes = 'intnotes';
558 my $number_pattern = $builder->build_object(
559     {
560         class => 'Koha::Subscription::Numberpatterns',
561         value => { numberingmethod => 'No. {X}' }
562     }
563 );
564 my $subscriptionid = NewSubscription(
565      undef,      "",     undef, undef, undef, $biblionumber,
566     '2013-01-01', 1, undef, undef,  undef,
567     undef,      undef,  undef, undef, undef, undef,
568     1,          $notes,undef, '2013-01-01', undef, $number_pattern->id,
569     undef,       undef,  0,    $internalnotes,  0,
570     undef, undef, 0,          undef,         '2013-12-31', 0
571 );
572 $dbh->do(q{INSERT INTO letter (module, code, name, title, content) VALUES ('serial','RLIST','Serial issue notification','Serial issue notification','<<biblio.title>>,<<subscription.subscriptionid>>,<<serial.serialseq>>');});
573 my ($serials_count, @serials) = GetSerials($subscriptionid);
574 my $serial = $serials[0];
575
576 my $patron = Koha::Patron->new({
577     firstname    => 'John',
578     surname      => 'Smith',
579     categorycode => $patron_category,
580     branchcode   => $library->{branchcode},
581     dateofbirth  => $date,
582     email        => 'john.smith@test.de',
583 })->store;
584 my $borrowernumber = $patron->borrowernumber;
585 my $subscription = Koha::Subscriptions->find( $subscriptionid );
586 $subscription->add_subscriber( $patron );
587
588 t::lib::Mocks::mock_userenv({ branch => $library->{branchcode} });
589 my $err2;
590 warning_like {
591 $err2 = SendAlerts( 'issue', $serial->{serialid}, 'RLIST' ) }
592     qr|Fake send_or_die|,
593     "SendAlerts is using the mocked send_or_die routine";
594
595 is($err2, 1, "Successfully sent serial notification");
596 is($email_object->email->header('To'), 'john.smith@test.de', "mailto correct in sent serial notification");
597 is($email_object->email->body, 'Silence in the library,'.$subscriptionid.',No. 0', 'Serial notification text constructed successfully');
598
599 t::lib::Mocks::mock_preference( 'SendAllEmailsTo', 'robert.tables@mail.com' );
600
601 my $err3;
602 warning_like {
603 $err3 = SendAlerts( 'issue', $serial->{serialid}, 'RLIST' ) }
604     qr|Fake send_or_die|,
605     "SendAlerts is using the mocked send_or_die routine";
606 is($email_object->email->header('To'), 'robert.tables@mail.com', "mailto address overwritten by SendAllMailsTo preference");
607
608 my $mocked_koha_email = Test::MockModule->new('Koha::Email');
609 $mocked_koha_email->mock( 'send_or_die', sub {
610     Email::Sender::Failure->throw('something went wrong');
611 });
612
613 warning_like {
614     $err = SendAlerts( 'issue', $serial->{serialid} , 'RLIST' ); }
615     qr{something went wrong},
616     'Warning is printed';
617
618 is($err->{error}, 'something went wrong', "Send exception, error message returned");
619
620 }
621 t::lib::Mocks::mock_preference( 'SendAllEmailsTo', '' );
622
623 subtest '_parseletter' => sub {
624     plan tests => 2;
625
626     # Regression test for bug 10843
627     # $dt->add takes a scalar, not undef
628     my $letter;
629     t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', undef );
630     $letter = C4::Letters::_parseletter( undef, 'reserves', { waitingdate => "2013-01-01" } );
631     is( ref($letter), 'HASH' );
632     t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 1 );
633     $letter = C4::Letters::_parseletter( undef, 'reserves', { waitingdate => "2013-01-01" } );
634     is( ref($letter), 'HASH' );
635 };
636
637 subtest 'SendAlerts - claimissue' => sub {
638     plan tests => 13;
639
640     use C4::Serials;
641
642     $dbh->do(q{INSERT INTO letter (module, code, name, title, content) VALUES ('claimissues','TESTSERIALCLAIM','Serial claim test','Serial claim test','<<serial.serialid>>|<<subscription.startdate>>|<<biblio.title>>|<<biblioitems.issn>>');});
643
644     my $bookseller = Koha::Acquisition::Bookseller->new(
645         {
646             name => "my vendor",
647             address1 => "bookseller's address",
648             phone => "0123456",
649             active => 1,
650             deliverytime => 5,
651         }
652     )->store;
653     my $booksellerid = $bookseller->id;
654
655     Koha::Acquisition::Bookseller::Contact->new( { name => 'Leo Tolstoy', phone => '0123456x2', claimissues => 1, booksellerid => $booksellerid } )->store;
656
657     my $bib = MARC::Record->new();
658     if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
659         $bib->append_fields(
660             MARC::Field->new('011', ' ', ' ', a => 'xxxx-yyyy'),
661             MARC::Field->new('200', ' ', ' ', a => 'Silence in the library'),
662         );
663     } else {
664         $bib->append_fields(
665             MARC::Field->new('022', ' ', ' ', a => 'xxxx-yyyy'),
666             MARC::Field->new('245', ' ', ' ', a => 'Silence in the library'),
667         );
668     }
669     my ($biblionumber) = AddBiblio($bib, '');
670
671     my $number_pattern = $builder->build_object(
672         {
673             class => 'Koha::Subscription::Numberpatterns',
674             value => { numberingmethod => 'No. {X}' }
675         }
676     );
677
678     my $subscriptionid = NewSubscription(
679          undef, "", $booksellerid, undef, undef, $biblionumber,
680         '2013-01-01', 1, undef, undef,  undef,
681         undef,  undef,  undef, undef, undef, undef,
682         1, 'public',undef, '2013-01-01', undef, $number_pattern->id,
683         undef, undef,  0, 'internal',  0,
684         undef, undef, 0,  undef, '2013-12-31', 0
685     );
686
687     my ($serials_count, @serials) = GetSerials($subscriptionid);
688     my  @serialids = ($serials[0]->{serialid});
689
690     my $err;
691     warning_like {
692         $err = SendAlerts( 'claimissues', \@serialids, 'TESTSERIALCLAIM' ) }
693         qr/^Bookseller .* without emails at/,
694         "Warn on vendor without email address";
695
696     $bookseller = Koha::Acquisition::Booksellers->find( $booksellerid );
697     $bookseller->contacts->next->email('testemail@mydomain.com')->store;
698
699     # Ensure that the preference 'ClaimsLog' is set to logging
700     t::lib::Mocks::mock_preference( 'ClaimsLog', 'on' );
701
702     # SendAlerts needs branchemail or KohaAdminEmailAddress as sender
703     t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
704
705     t::lib::Mocks::mock_preference( 'KohaAdminEmailAddress', 'library@domain.com' );
706
707     {
708     warning_like {
709         $err = SendAlerts( 'claimissues', \@serialids , 'TESTSERIALCLAIM' ) }
710         qr|Fake send_or_die|,
711         "SendAlerts is using the mocked send_or_die routine (claimissues)";
712     is( $err, 1, "Successfully sent claim" );
713     is( $email_object->email->header('To'),
714         'testemail@mydomain.com', "mailto correct in sent claim" );
715     is(
716         $email_object->email->body,
717         "$serialids[0]|2013-01-01|Silence in the library|xxxx-yyyy",
718         'Serial claim letter for 1 issue constructed successfully'
719     );
720
721     my $mocked_koha_email = Test::MockModule->new('Koha::Email');
722     $mocked_koha_email->mock( 'send_or_die', sub {
723             Email::Sender::Failure->throw('something went wrong');
724     });
725
726     warning_like {
727         $err = SendAlerts( 'claimissues', \@serialids , 'TESTSERIALCLAIM' ); }
728         qr{something went wrong},
729         'Warning is printed';
730
731     is($err->{error}, 'something went wrong', "Send exception, error message returned");
732     }
733
734     {
735     my $publisheddate = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
736     my $serialexpected = ( C4::Serials::findSerialsByStatus( 1, $subscriptionid ) )[0];
737     ModSerialStatus( $serials[0]->{serialid}, "No. 1", $publisheddate, $publisheddate, $publisheddate, '3', 'a note' );
738     ($serials_count, @serials) = GetSerials($subscriptionid);
739     push @serialids, ($serials[1]->{serialid});
740
741     warning_like { $err = SendAlerts( 'claimissues', \@serialids, 'TESTSERIALCLAIM' ); }
742         qr|Fake send_or_die|,
743         "SendAlerts is using the mocked send_or_die routine (claimissues)";
744
745     is(
746         $email_object->email->body,
747         "$serialids[0]|2013-01-01|Silence in the library|xxxx-yyyy"
748           . $email_object->email->crlf
749           . "$serialids[1]|2013-01-01|Silence in the library|xxxx-yyyy",
750         "Serial claim letter for 2 issues constructed successfully"
751     );
752
753     $dbh->do(q{DELETE FROM letter WHERE code = 'TESTSERIALCLAIM';});
754     warning_like {
755         $err = SendAlerts( 'orderacquisition', $basketno , 'TESTSERIALCLAIM' ) }
756         qr/No orderacquisition TESTSERIALCLAIM letter transported by email/,
757         "GetPreparedLetter warns about missing notice template";
758     is($err->{'error'}, 'no_letter', "No TESTSERIALCLAIM letter was defined");
759     }
760
761 };
762
763 subtest 'GetPreparedLetter' => sub {
764     plan tests => 4;
765
766     Koha::Notice::Template->new(
767         {
768             module                 => 'test',
769             code                   => 'test',
770             branchcode             => '',
771             message_transport_type => 'email'
772         }
773     )->store;
774     my $letter;
775     warning_like {
776         $letter = C4::Letters::GetPreparedLetter(
777             module      => 'test',
778             letter_code => 'test',
779         );
780     }
781     qr{^ERROR: nothing to substitute},
782 'GetPreparedLetter should warn if tables, substiture and repeat are not set';
783     is( $letter, undef,
784 'No letter should be returned by GetPreparedLetter if something went wrong'
785     );
786
787     warning_like {
788         $letter = C4::Letters::GetPreparedLetter(
789             module      => 'test',
790             letter_code => 'test',
791             substitute  => {}
792         );
793     }
794     qr{^ERROR: nothing to substitute},
795 'GetPreparedLetter should warn if tables, substiture and repeat are not set, even if the key is passed';
796     is( $letter, undef,
797 'No letter should be returned by GetPreparedLetter if something went wrong'
798     );
799
800 };
801
802
803
804 subtest 'TranslateNotices' => sub {
805     plan tests => 4;
806
807     t::lib::Mocks::mock_preference( 'TranslateNotices', '1' );
808
809     $dbh->do(
810         q|
811         INSERT INTO letter (module, code, branchcode, name, title, content, message_transport_type, lang) VALUES
812         ('test', 'code', '', 'test', 'a test', 'just a test', 'email', 'default'),
813         ('test', 'code', '', 'test', 'una prueba', 'solo una prueba', 'email', 'es-ES');
814     | );
815     my $substitute = {};
816     my $letter = C4::Letters::GetPreparedLetter(
817             module                 => 'test',
818             tables                 => $tables,
819             letter_code            => 'code',
820             message_transport_type => 'email',
821             substitute             => $substitute,
822     );
823     is(
824         $letter->{title},
825         'a test',
826         'GetPreparedLetter should return the default one if the lang parameter is not provided'
827     );
828
829     $letter = C4::Letters::GetPreparedLetter(
830             module                 => 'test',
831             tables                 => $tables,
832             letter_code            => 'code',
833             message_transport_type => 'email',
834             substitute             => $substitute,
835             lang                   => 'es-ES',
836     );
837     is( $letter->{title}, 'una prueba',
838         'GetPreparedLetter should return the required notice if it exists' );
839
840     $letter = C4::Letters::GetPreparedLetter(
841             module                 => 'test',
842             tables                 => $tables,
843             letter_code            => 'code',
844             message_transport_type => 'email',
845             substitute             => $substitute,
846             lang                   => 'fr-FR',
847     );
848     is(
849         $letter->{title},
850         'a test',
851         'GetPreparedLetter should return the default notice if the one required does not exist'
852     );
853
854     t::lib::Mocks::mock_preference( 'TranslateNotices', '' );
855
856     $letter = C4::Letters::GetPreparedLetter(
857             module                 => 'test',
858             tables                 => $tables,
859             letter_code            => 'code',
860             message_transport_type => 'email',
861             substitute             => $substitute,
862             lang                   => 'es-ES',
863     );
864     is( $letter->{title}, 'a test',
865         'GetPreparedLetter should return the default notice if pref disabled but additional language exists' );
866
867 };
868
869 subtest 'Test SMS handling in SendQueuedMessages' => sub {
870
871     plan tests => 14;
872
873     t::lib::Mocks::mock_preference( 'SMSSendDriver', 'Email' );
874     t::lib::Mocks::mock_preference('EmailSMSSendDriverFromAddress', '');
875
876     my $patron = Koha::Patrons->find($borrowernumber);
877     $dbh->do(q|
878         INSERT INTO message_queue(borrowernumber, subject, content, message_transport_type, status, letter_code)
879         VALUES (?, 'subject', 'content', 'sms', 'pending', 'just_a_code')
880         |, undef, $borrowernumber
881     );
882     eval { C4::Letters::SendQueuedMessages(); };
883     is( $@, '', 'SendQueuedMessages should not explode if the patron does not have a sms provider set' );
884
885     my $sms_pro = $builder->build_object({ class => 'Koha::SMS::Providers', value => { domain => 'kidclamp.rocks' } });
886     $patron->set( { smsalertnumber => '5555555555', sms_provider_id => $sms_pro->id() } )->store;
887     $message_id = C4::Letters::EnqueueLetter($my_message); #using datas set around line 95 and forward
888
889     warning_like { C4::Letters::SendQueuedMessages(); }
890         qr|Fake send_or_die|,
891         "SendAlerts is using the mocked send_or_die routine (claimissues)";
892
893     my $message = $schema->resultset('MessageQueue')->search({
894         borrowernumber => $borrowernumber,
895         status => 'sent'
896     })->next();
897
898     is( $message->letter_id, $messages->[0]->{id}, "Message letter_id is set correctly" );
899     is( $message->to_address(), '5555555555@kidclamp.rocks', 'SendQueuedMessages populates the to address correctly for SMS by email when to_address not set' );
900     is(
901         $message->from_address(),
902         'from@example.com',
903         'SendQueuedMessages uses message queue item \"from address\" for SMS by email when EmailSMSSendDriverFromAddress system preference is not set'
904     );
905
906     $schema->resultset('MessageQueue')->search({borrowernumber => $borrowernumber, status => 'sent'})->delete(); #clear borrower queue
907
908     t::lib::Mocks::mock_preference('EmailSMSSendDriverFromAddress', 'override@example.com');
909
910     $message_id = C4::Letters::EnqueueLetter($my_message);
911     warning_like { C4::Letters::SendQueuedMessages(); }
912         qr|Fake send_or_die|,
913         "SendAlerts is using the mocked send_or_die routine (claimissues)";
914
915     $message = $schema->resultset('MessageQueue')->search({
916         borrowernumber => $borrowernumber,
917         status => 'sent'
918     })->next();
919
920     is(
921         $message->from_address(),
922         'override@example.com',
923         'SendQueuedMessages uses EmailSMSSendDriverFromAddress value for SMS by email when EmailSMSSendDriverFromAddress is set'
924     );
925
926     $schema->resultset('MessageQueue')->search({borrowernumber => $borrowernumber,status => 'sent'})->delete(); #clear borrower queue
927     $my_message->{to_address} = 'fixme@kidclamp.iswrong';
928     $message_id = C4::Letters::EnqueueLetter($my_message);
929
930     my $number_attempted = C4::Letters::SendQueuedMessages({
931         borrowernumber => -1, # -1 still triggers the borrowernumber condition
932         letter_code    => 'PASSWORD_RESET',
933     });
934     is ( $number_attempted, 0, 'There were no password reset messages for SendQueuedMessages to attempt.' );
935
936     warning_like { C4::Letters::SendQueuedMessages(); }
937         qr|Fake send_or_die|,
938         "SendAlerts is using the mocked send_or_die routine (claimissues)";
939
940     my $sms_message_address = $schema->resultset('MessageQueue')->search({
941         borrowernumber => $borrowernumber,
942         status => 'sent'
943     })->next()->to_address();
944     is( $sms_message_address, '5555555555@kidclamp.rocks', 'SendQueuedMessages populates the to address correctly for SMS by email when to_address is set incorrectly' );
945
946     # Test using SMS::Send::Test driver that's bundled with SMS::Send
947     t::lib::Mocks::mock_preference('SMSSendDriver', "AU::Test");
948
949     $schema->resultset('MessageQueue')->search({borrowernumber => $borrowernumber, status => 'sent'})->delete(); #clear borrower queue
950     C4::Letters::EnqueueLetter($my_message);
951     C4::Letters::SendQueuedMessages();
952
953     $sms_message_address = $schema->resultset('MessageQueue')->search({
954         borrowernumber => $borrowernumber,
955         status => 'sent'
956     })->next()->to_address();
957     is( $sms_message_address, '5555555555', 'SendQueuedMessages populates the to address correctly for SMS by SMS::Send driver to smsalertnumber when to_address is set incorrectly' );
958
959 };
960
961 subtest 'get_item_content' => sub {
962     plan tests => 2;
963
964     t::lib::Mocks::mock_preference('dateformat', 'metric');
965     t::lib::Mocks::mock_preference('timeformat', '24hr');
966     my @items = (
967         {date_due => '2041-01-01 12:34', title => 'a first title', barcode => 'a_first_barcode', author => 'a_first_author', itemnumber => 1 },
968         {date_due => '2042-01-02 23:45', title => 'a second title', barcode => 'a_second_barcode', author => 'a_second_author', itemnumber => 2 },
969     );
970     my @item_content_fields = qw( date_due title barcode author itemnumber );
971
972     my $items_content;
973     for my $item ( @items ) {
974         $items_content .= C4::Letters::get_item_content( { item => $item, item_content_fields => \@item_content_fields } );
975     }
976
977     my $expected_items_content = <<EOF;
978 01/01/2041 12:34\ta first title\ta_first_barcode\ta_first_author\t1
979 02/01/2042 23:45\ta second title\ta_second_barcode\ta_second_author\t2
980 EOF
981     is( $items_content, $expected_items_content, 'get_item_content should return correct items info with time (default)' );
982
983
984     $items_content = q||;
985     for my $item ( @items ) {
986         $items_content .= C4::Letters::get_item_content( { item => $item, item_content_fields => \@item_content_fields, dateonly => 1, } );
987     }
988
989     $expected_items_content = <<EOF;
990 01/01/2041\ta first title\ta_first_barcode\ta_first_author\t1
991 02/01/2042\ta second title\ta_second_barcode\ta_second_author\t2
992 EOF
993     is( $items_content, $expected_items_content, 'get_item_content should return correct items info without time (if dateonly => 1)' );
994 };
995
996 subtest 'Test limit parameter for SendQueuedMessages' => sub {
997     plan tests => 3;
998
999     my $dbh = C4::Context->dbh;
1000
1001     my $borrowernumber = Koha::Patron->new({
1002         firstname    => 'Jane',
1003         surname      => 'Smith',
1004         categorycode => $patron_category,
1005         branchcode   => $library->{branchcode},
1006         dateofbirth  => $date,
1007         smsalertnumber => undef,
1008     })->store->borrowernumber;
1009
1010     $dbh->do(q|DELETE FROM message_queue|);
1011     $my_message = {
1012         'letter' => {
1013             'content'      => 'a message',
1014             'metadata'     => 'metadata',
1015             'code'         => 'TEST_MESSAGE',
1016             'content_type' => 'text/plain',
1017             'title'        => 'message title'
1018         },
1019         'borrowernumber'         => $borrowernumber,
1020         'to_address'             => undef,
1021         'message_transport_type' => 'sms',
1022         'from_address'           => 'from@example.com'
1023     };
1024     C4::Letters::EnqueueLetter($my_message);
1025     C4::Letters::EnqueueLetter($my_message);
1026     C4::Letters::EnqueueLetter($my_message);
1027     C4::Letters::EnqueueLetter($my_message);
1028     C4::Letters::EnqueueLetter($my_message);
1029     my $messages_processed = C4::Letters::SendQueuedMessages( { limit => 1 } );
1030     is( $messages_processed, 1,
1031         'Processed 1 message with limit of 1 and 5 unprocessed messages' );
1032     $messages_processed = C4::Letters::SendQueuedMessages( { limit => 2 } );
1033     is( $messages_processed, 2,
1034         'Processed 2 message with limit of 2 and 4 unprocessed messages' );
1035     $messages_processed = C4::Letters::SendQueuedMessages( { limit => 3 } );
1036     is( $messages_processed, 2,
1037         'Processed 2 message with limit of 3 and 2 unprocessed messages' );
1038 };
1039
1040 subtest 'Test message_id parameter for SendQueuedMessages' => sub {
1041
1042     plan tests => 7;
1043
1044     my $dbh = C4::Context->dbh;
1045
1046     my $borrowernumber = Koha::Patron->new({
1047         firstname    => 'Jane',
1048         surname      => 'Smith',
1049         categorycode => $patron_category,
1050         branchcode   => $library->{branchcode},
1051         dateofbirth  => $date,
1052         smsalertnumber => undef,
1053     })->store->borrowernumber;
1054
1055     $dbh->do(q|DELETE FROM message_queue|);
1056     $my_message = {
1057         'letter' => {
1058             'content'      => 'a message',
1059             'metadata'     => 'metadata',
1060             'code'         => 'TEST_MESSAGE',
1061             'content_type' => 'text/plain',
1062             'title'        => 'message title'
1063         },
1064         'borrowernumber'         => $borrowernumber,
1065         'to_address'             => 'to@example.org',
1066         'message_transport_type' => 'email',
1067         'from_address'           => '@example.com' # invalid from_address
1068     };
1069     my $message_id = C4::Letters::EnqueueLetter($my_message);
1070     my $processed = C4::Letters::SendQueuedMessages( { message_id => $message_id } );
1071     is( $processed, 1, 'Processed 1 message when one message_id passed' );
1072     my $message_1 = C4::Letters::GetMessage($message_id);
1073     is( $message_1->{status}, 'failed', 'Invalid from_address => status failed' );
1074     is( $message_1->{failure_code}, 'INVALID_EMAIL:from', 'Failure code set correctly for invalid email parameter');
1075
1076     $my_message->{from_address} = 'root@example.org'; # valid from_address
1077     $message_id = C4::Letters::EnqueueLetter($my_message);
1078     warning_like { C4::Letters::SendQueuedMessages( { message_id => $message_id } ); }
1079         qr|Fake send_or_die|,
1080         "SendQueuedMessages is using the mocked send_or_die routine";
1081     $message_1 = C4::Letters::GetMessage($message_1->{message_id});
1082     my $message_2 = C4::Letters::GetMessage($message_id);
1083     is( $message_1->{status}, 'failed', 'Message 1 status is unchanged' );
1084     is( $message_2->{status}, 'sent', 'Valid from_address => status sent' );
1085 };