Bug 15702: Add test cases for modified code
[koha.git] / t / db_dependent / UsageStats.t
1 # Copyright 2015 BibLibre
2 #
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, see <http://www.gnu.org/licenses>.
16
17 use Modern::Perl;
18 use Test::More tests => 545;
19 use t::lib::Mocks qw(mock_preference);
20 use POSIX qw(strftime);
21
22 use Koha::Libraries;
23
24 BEGIN {
25     use_ok('C4::UsageStats');
26     use_ok('C4::Context');
27     use_ok('C4::Biblio');
28     use_ok( 'C4::AuthoritiesMarc', qw(AddAuthority) );
29     use_ok('C4::Reserves');
30     use_ok('MARC::Record');
31     use_ok('Koha::Acquisition::Order');
32 }
33
34 can_ok(
35     'C4::UsageStats', qw(
36       NeedUpdate
37       BuildReport
38       ReportToCommunity
39       _count )
40 );
41
42 my $dbh = C4::Context->dbh;
43 $dbh->{AutoCommit} = 0;
44 $dbh->{RaiseError} = 1;
45
46 $dbh->do('DELETE FROM issues');
47 $dbh->do('DELETE FROM biblio');
48 $dbh->do('DELETE FROM items');
49 $dbh->do('DELETE FROM auth_header');
50 $dbh->do('DELETE FROM old_issues');
51 $dbh->do('DELETE FROM old_reserves');
52 $dbh->do('DELETE FROM borrowers');
53 $dbh->do('DELETE FROM aqorders');
54 $dbh->do('DELETE FROM subscription');
55
56 #################################################
57 #             Testing Subs
58 #################################################
59
60 # ---------- Testing NeedUpdate -----------------
61
62 #Mocking C4::Context->preference("UsageStatsLastUpdateTime") to 0
63 my $now = strftime( "%s", localtime );
64 t::lib::Mocks::mock_preference( "UsageStatsLastUpdateTime", 0 );
65
66 my $update = C4::UsageStats->NeedUpdate;
67 is( $update, 1, "There is no last update, update needed" );
68
69 #Mocking C4::Context->preference("UsageStatsLastUpdateTime") to now
70 $now = strftime( "%s", localtime );
71 t::lib::Mocks::mock_preference( "UsageStatsLastUpdateTime", $now );
72
73 $update = C4::UsageStats->NeedUpdate;
74 is( $update, 0, "Last update just be done, no update needed " );
75
76 my $nb_of_libraries = Koha::Libraries->count;
77
78 # ---------- Testing BuildReport ----------------
79
80 #Test report->library -----------------
81 #mock to 0
82 t::lib::Mocks::mock_preference( "UsageStatsID",          0 );
83 t::lib::Mocks::mock_preference( "UsageStatsLibraryName", 0 );
84 t::lib::Mocks::mock_preference( "UsageStatsLibrariesInfo",  0 );
85 t::lib::Mocks::mock_preference( "UsageStatsLibraryType", 0 );
86 t::lib::Mocks::mock_preference( "UsageStatsCountry",     0 );
87 t::lib::Mocks::mock_preference( "UsageStatsLibraryUrl",  0 );
88
89 my $report = C4::UsageStats->BuildReport();
90
91 isa_ok( $report,              'HASH',  '$report is a HASH' );
92 isa_ok( $report->{libraries}, 'ARRAY', '$report->{libraries} is an ARRAY' );
93 is( scalar( @{ $report->{libraries} } ), 0, "There are 0 fields in libraries, libraries info are not shared" );
94 is( $report->{installation}->{koha_id}, 0,  "UsageStatsID          is good" );
95 is( $report->{installation}->{name},    '', "UsageStatsLibraryName is good" );
96 is( $report->{installation}->{url},     '', "UsageStatsLibraryUrl  is good" );
97 is( $report->{installation}->{type},    '', "UsageStatsLibraryType is good" );
98 is( $report->{installation}->{country}, '', "UsageStatsCountry     is good" );
99
100
101 #mock with values
102 t::lib::Mocks::mock_preference( "UsageStatsID",          1 );
103 t::lib::Mocks::mock_preference( "UsageStatsLibraryName", 'NAME' );
104 t::lib::Mocks::mock_preference( "UsageStatsLibraryUrl",  'URL' );
105 t::lib::Mocks::mock_preference( "UsageStatsLibraryType", 'TYPE' );
106 t::lib::Mocks::mock_preference( "UsageStatsCountry",     'COUNTRY' );
107 t::lib::Mocks::mock_preference( "UsageStatsLibrariesInfo", 1 );
108 t::lib::Mocks::mock_preference( "UsageStatsGeolocation", 1 );
109
110
111 $report = C4::UsageStats->BuildReport();
112
113 isa_ok( $report,              'HASH',  '$report is a HASH' );
114 isa_ok( $report->{libraries}, 'ARRAY', '$report->{libraries} is an ARRAY' );
115 is( scalar( @{ $report->{libraries} } ), $nb_of_libraries, "There are 6 fields in $report->{libraries}" );
116 is( $report->{installation}->{koha_id}, 1,     "UsageStatsID          is good" );
117 is( $report->{installation}->{name},   'NAME', "UsageStatsLibraryName is good" );
118 is( $report->{installation}->{url},     'URL', "UsageStatsLibraryUrl  is good" );
119 is( $report->{installation}->{type},   'TYPE', "UsageStatsLibraryType is good" );
120 is( $report->{installation}->{country}, 'COUNTRY', "UsageStatsCountry is good" );
121
122 #Test report->volumetry ---------------
123 #with original values
124 $report = C4::UsageStats->BuildReport();
125
126 isa_ok( $report,              'HASH', '$report is a HASH' );
127 isa_ok( $report->{volumetry}, 'HASH', '$report->{volumetry} is a HASH' );
128 is( scalar( keys %{$report->{volumetry}} ), 8, "There are 8 fields in $report->{volumetry}" );
129 is( $report->{volumetry}->{biblio},         0, "There is no biblio" );
130 is( $report->{volumetry}->{items},          0, "There is no items" );
131 is( $report->{volumetry}->{auth_header},    0, "There is no auth_header" );
132 is( $report->{volumetry}->{old_issues},     0, "There is no old_issues" );
133 is( $report->{volumetry}->{old_reserves},   0, "There is no old_reserves" );
134 is( $report->{volumetry}->{borrowers},      0, "There is no borrowers" );
135 is( $report->{volumetry}->{aqorders},       0, "There is no aqorders" );
136 is( $report->{volumetry}->{subscription},   0, "There is no subscription" );
137
138 #after adding objects
139 construct_objects_needed();
140
141 $report = C4::UsageStats->BuildReport();
142
143 isa_ok( $report,              'HASH', '$report is a HASH' );
144 isa_ok( $report->{volumetry}, 'HASH', '$report->{volumetry} is a HASH' );
145 is( scalar( keys %{$report->{volumetry}} ), 8, "There are 8 fields in $report->{volumetry}" );
146 is( $report->{volumetry}->{biblio},         3, "There are 3 biblio" );
147 is( $report->{volumetry}->{items},          3, "There are 3 items" );
148 is( $report->{volumetry}->{auth_header},    2, "There are 2 auth_header" );
149 is( $report->{volumetry}->{old_issues},     1, "There is  1 old_issues" );
150 is( $report->{volumetry}->{old_reserves},   1, "There is  1 old_reserves" );
151 is( $report->{volumetry}->{borrowers},      3, "There are 3 borrowers" );
152 is( $report->{volumetry}->{aqorders},       1, "There is  1 aqorders" );
153 is( $report->{volumetry}->{subscription},   1, "There is  1 subscription" );
154
155 #Test report->systempreferences -------
156 #mock to 0
157 mocking_systempreferences_to_a_set_value(0);
158
159 $report = C4::UsageStats->BuildReport();
160 isa_ok( $report,                      'HASH', '$report is a HASH' );
161 isa_ok( $report->{systempreferences}, 'HASH', '$report->{systempreferences} is a HASH' );
162 verif_systempreferences_values( $report, 0 );
163
164 #mock with values
165 mocking_systempreferences_to_a_set_value(1);
166
167 $report = C4::UsageStats->BuildReport();
168 isa_ok( $report,                      'HASH', '$report is a HASH' );
169 isa_ok( $report->{systempreferences}, 'HASH', '$report->{systempreferences} is a HASH' );
170 verif_systempreferences_values( $report, 1 );
171
172 #Test if unwanted syspref are not sent
173 is( $report->{systempreferences}->{useDischarge}, undef, 'useDischarge should not be shared');
174 is( $report->{systempreferences}->{OpacUserJS},   undef, 'OpacUserJS   should not be shared');
175
176 # ---------- Testing ReportToCommunity ----------
177
178 # ---------- Testing _count ---------------------
179 my $query = '
180   SELECT count(*)
181   FROM   borrowers
182   ';
183 my $count = $dbh->selectrow_array($query);
184
185 my $nb_fields = C4::UsageStats::_count('borrowers');
186 is( $nb_fields, $count, "_count return the good number of fields" );
187
188 #################################################
189 #             Subs
190 #################################################
191
192 # Adding :
193 # 3 borrowers
194 # 4 biblio
195 # 3 biblio items
196 # 3 items
197 # 2 auth_header
198 # 1 old_issues
199 # 1 old_reserves
200 # 1 subscription
201 # 1 aqorders
202 sub construct_objects_needed {
203
204     # ---------- 3 borrowers  ---------------------
205     my $surname1     = 'Borrower 1';
206     my $surname2     = 'Borrower 2';
207     my $surname3     = 'Borrower 3';
208     my $firstname1   = 'firstname 1';
209     my $firstname2   = 'firstname 2';
210     my $firstname3   = 'firstname 3';
211     my $cardnumber1  = 'test_card1';
212     my $cardnumber2  = 'test_card2';
213     my $cardnumber3  = 'test_card3';
214     my $categorycode = Koha::Database->new()->schema()->resultset('Category')->first()->categorycode();
215     my $branchcode   = Koha::Database->new()->schema()->resultset('Branch')->first()->branchcode();
216
217     my $query = '
218     INSERT INTO borrowers
219       (surname, firstname, cardnumber, branchcode, categorycode)
220     VALUES (?,?,?,?,?)';
221     my $insert_sth = $dbh->prepare($query);
222     $insert_sth->execute( $surname1, $firstname1, $cardnumber1, $branchcode, $categorycode );
223     my $borrowernumber1 = $dbh->last_insert_id( undef, undef, 'borrowers', undef );
224     $insert_sth->execute( $surname2, $firstname2, $cardnumber2, $branchcode, $categorycode );
225     my $borrowernumber2 = $dbh->last_insert_id( undef, undef, 'borrowers', undef );
226     $insert_sth->execute( $surname3, $firstname3, $cardnumber3, $branchcode, $categorycode );
227     my $borrowernumber3 = $dbh->last_insert_id( undef, undef, 'borrowers', undef );
228
229     # ---------- 3 biblios -----------------------
230     my $title1  = 'Title 1';
231     my $title2  = 'Title 2';
232     my $title3  = 'Title 3';
233     my $author1 = 'Author 1';
234     my $author2 = 'Author 2';
235     my $author3 = 'Author 3';
236
237     $query = '
238     INSERT INTO biblio
239       (title, author)
240     VALUES (?,?)';
241     $insert_sth = $dbh->prepare($query);
242     $insert_sth->execute( $title1, $author1 );
243     my $biblionumber1 = $dbh->last_insert_id( undef, undef, 'biblio', undef );
244     $insert_sth->execute( $title2, undef );
245     my $biblionumber2 = $dbh->last_insert_id( undef, undef, 'biblio', undef );
246     $insert_sth->execute( $title3, $author3 );
247     my $biblionumber3 = $dbh->last_insert_id( undef, undef, 'biblio', undef );
248
249     # ---------- 3 biblio items  -------------------------
250     $query = '
251     INSERT INTO biblioitems
252       (biblionumber, itemtype, marcxml)
253     VALUES (?,?,?)';
254     $insert_sth = $dbh->prepare($query);
255     $insert_sth->execute( $biblionumber1, 'Book', '' );
256     my $biblioitemnumber1 = $dbh->last_insert_id( undef, undef, 'biblioitems', undef );
257     $insert_sth->execute( $biblionumber2, 'Music', '' );
258     my $biblioitemnumber2 = $dbh->last_insert_id( undef, undef, 'biblioitems', undef );
259     $insert_sth->execute( $biblionumber3, 'Book', '' );
260     my $biblioitemnumber3 = $dbh->last_insert_id( undef, undef, 'biblioitems', undef );
261
262     # ---------- 3 items  -------------------------
263     my $barcode1 = '111111';
264     my $barcode2 = '222222';
265     my $barcode3 = '333333';
266
267     $query = '
268     INSERT INTO items
269       (biblionumber, biblioitemnumber, barcode, itype)
270     VALUES (?,?,?,?)';
271     $insert_sth = $dbh->prepare($query);
272     $insert_sth->execute( $biblionumber1, $biblioitemnumber1, $barcode1, 'Book' );
273     my $item_number1 = $dbh->last_insert_id( undef, undef, 'items', undef );
274     $insert_sth->execute( $biblionumber2, $biblioitemnumber2, $barcode2, 'Music' );
275     my $item_number2 = $dbh->last_insert_id( undef, undef, 'items', undef );
276     $insert_sth->execute( $biblionumber3, $biblioitemnumber3, $barcode3, 'Book' );
277     my $item_number3 = $dbh->last_insert_id( undef, undef, 'items', undef );
278
279     # ---------- Add 2 auth_header
280     $query = '
281     INSERT INTO auth_header
282       (authtypecode)
283     VALUES (?)';
284     $insert_sth = $dbh->prepare($query);
285     $insert_sth->execute('authtypecode1');
286     my $authid1 = $dbh->last_insert_id( undef, undef, 'auth_header', undef );
287     $insert_sth->execute('authtypecode2');
288     my $authid2 = $dbh->last_insert_id( undef, undef, 'auth_header', undef );
289
290     # ---------- Add 1 old_issues
291     $query = '
292     INSERT INTO old_issues
293       (borrowernumber, branchcode, itemnumber)
294     VALUES (?,?,?)';
295     $insert_sth = $dbh->prepare($query);
296     $insert_sth->execute( $borrowernumber1, $branchcode, $item_number1 );
297     my $issue_id1 = $dbh->last_insert_id( undef, undef, 'old_issues', undef );
298
299     # ---------- Add 1 old_reserves
300     AddReserve( $branchcode, $borrowernumber1, $biblionumber1, '', 1, undef, undef, '', 'Title', undef, undef );
301     my $reserves1   = GetReservesFromBiblionumber( { biblionumber => $biblionumber1 } );
302     my $reserve_id1 = $reserves1->[0]->{reserve_id};
303     my $reserve1    = CancelReserve( { reserve_id => $reserve_id1 } );
304
305     # ---------- Add 1 aqbudgets
306     $query = '
307     INSERT INTO aqbudgets
308       (budget_amount)
309     VALUES (?)';
310     $insert_sth = $dbh->prepare($query);
311     $insert_sth->execute("20.0");
312     my $aqbudgets1 = $dbh->last_insert_id( undef, undef, 'aqbudgets', undef );
313
314     # ---------- Add 1 aqorders
315     $query = '
316     INSERT INTO aqorders
317       (budget_id, basketno, biblionumber, invoiceid, subscriptionid)
318     VALUES (?,?,?,?,?)';
319     $insert_sth = $dbh->prepare($query);
320     $insert_sth->execute( $aqbudgets1, undef, undef, undef, undef );
321     my $aqorders1 = $dbh->last_insert_id( undef, undef, 'aqorders', undef );
322
323     # --------- Add 1 subscription
324     $query = '
325     INSERT INTO subscription
326       (biblionumber)
327     VALUES (?)';
328     $insert_sth = $dbh->prepare($query);
329     $insert_sth->execute($biblionumber1);
330     my $subscription1 = $dbh->last_insert_id( undef, undef, 'subscription', undef );
331
332 }
333
334 #Change systempreferences values to $set_value
335 sub mocking_systempreferences_to_a_set_value {
336     my $set_value = shift;
337
338     foreach (
339         qw/
340         AcqCreateItem
341         AcqWarnOnDuplicateInvoice
342         AcqViewBaskets
343         BasketConfirmations
344         OrderPdfFormat
345         casAuthentication
346         casLogout
347         AllowPKIAuth
348         DebugLevel
349         delimiter
350         noItemTypeImages
351         virtualshelves
352         AutoLocation
353         IndependentBranches
354         SessionStorage
355         Persona
356         AuthDisplayHierarchy
357         AutoCreateAuthorities
358         BiblioAddsAuthorities
359         dontmerge
360         UseAuthoritiesForTracings
361         CatalogModuleRelink
362         hide_marc
363         IntranetBiblioDefaultView
364         LabelMARCView
365         OpacSuppression
366         SeparateHoldings
367         UseControlNumber
368         advancedMARCeditor
369         DefaultClassificationSource
370         EasyAnalyticalRecords
371         autoBarcode
372         item-level_itypes
373         marcflavour
374         PrefillItem
375         z3950NormalizeAuthor
376         SpineLabelAutoPrint
377         SpineLabelShowPrintOnBibDetails
378         BlockReturnOfWithdrawnItems
379         CalculateFinesOnReturn
380         AgeRestrictionOverride
381         AllFinesNeedOverride
382         AllowFineOverride
383         AllowItemsOnHoldCheckout
384         AllowNotForLoanOverride
385         AllowRenewalLimitOverride
386         AllowReturnToBranch
387         AllowTooManyOverride
388         AutomaticItemReturn
389         AutoRemoveOverduesRestrictions
390         CircControl
391         HomeOrHoldingBranch
392         HomeOrHoldingBranchReturn
393         InProcessingToShelvingCart
394         IssueLostItem
395         IssuingInProcess
396         ManInvInNoissuesCharge
397         OverduesBlockCirc
398         RenewalPeriodBase
399         RenewalSendNotice
400         RentalsInNoissuesCharge
401         ReturnBeforeExpiry
402         ReturnToShelvingCart
403         TransfersMaxDaysWarning
404         UseBranchTransferLimits
405         useDaysMode
406         UseTransportCostMatrix
407         UseCourseReserves
408         finesCalendar
409         FinesIncludeGracePeriod
410         finesMode
411         RefundLostItemFeeOnReturn
412         WhenLostChargeReplacementFee
413         WhenLostForgiveFine
414         AllowHoldDateInFuture
415         AllowHoldPolicyOverride
416         AllowHoldsOnDamagedItems
417         AllowHoldsOnPatronsPossessions
418         AutoResumeSuspendedHolds
419         canreservefromotherbranches
420         decreaseLoanHighHolds
421         DisplayMultiPlaceHold
422         emailLibrarianWhenHoldIsPlaced
423         ExpireReservesMaxPickUpDelay
424         OPACAllowHoldDateInFuture
425         OPACAllowUserToChooseBranch
426         ReservesControlBranch
427         ReservesNeedReturns
428         SuspendHoldsIntranet
429         SuspendHoldsOpac
430         TransferWhenCancelAllWaitingHolds
431         AllowAllMessageDeletion
432         AllowOfflineCirculation
433         CircAutocompl
434         CircAutoPrintQuickSlip
435         DisplayClearScreenButton
436         FilterBeforeOverdueReport
437         FineNotifyAtCheckin
438         itemBarcodeFallbackSearch
439         itemBarcodeInputFilter
440         previousIssuesDefaultSortOrder
441         RecordLocalUseOnReturn
442         soundon
443         SpecifyDueDate
444         todaysIssuesDefaultSortOrder
445         UpdateTotalIssuesOnCirc
446         UseTablesortForCirc
447         WaitingNotifyAtCheckin
448         AllowSelfCheckReturns
449         AutoSelfCheckAllowed
450         FRBRizeEditions
451         OPACFRBRizeEditions
452         AmazonCoverImages
453         OPACAmazonCoverImages
454         Babeltheque
455         BakerTaylorEnabled
456         GoogleJackets
457         HTML5MediaEnabled
458         IDreamBooksReadometer
459         IDreamBooksResults
460         IDreamBooksReviews
461         LibraryThingForLibrariesEnabled
462         LocalCoverImages
463         OPACLocalCoverImages
464         NovelistSelectEnabled
465         XISBN
466         OpenLibraryCovers
467         OpenLibrarySearch
468         UseKohaPlugins
469         SyndeticsEnabled
470         TagsEnabled
471         CalendarFirstDayOfWeek
472         opaclanguagesdisplay
473         AuthoritiesLog
474         BorrowersLog
475         CataloguingLog
476         FinesLog
477         IssueLog
478         LetterLog
479         ReturnLog
480         SubscriptionLog
481         BiblioDefaultView
482         COinSinOPACResults
483         DisplayOPACiconsXSLT
484         hidelostitems
485         HighlightOwnItemsOnOPAC
486         OpacAddMastheadLibraryPulldown
487         OPACDisplay856uAsImage
488         OpacHighlightedWords
489         OpacKohaUrl
490         OpacMaintenance
491         OpacPublic
492         OpacSeparateHoldings
493         OPACShowBarcode
494         OPACShowCheckoutName
495         OpacShowFiltersPulldownMobile
496         OPACShowHoldQueueDetails
497         OpacShowLibrariesPulldownMobile
498         OpacShowRecentComments
499         OPACShowUnusedAuthorities
500         OpacStarRatings
501         opacthemes
502         OPACURLOpenInNewWindow
503         OpacAuthorities
504         opacbookbag
505         OpacBrowser
506         OpacBrowseResults
507         OpacCloud
508         OPACFinesTab
509         OpacHoldNotes
510         OpacItemLocation
511         OpacPasswordChange
512         OPACPatronDetails
513         OPACpatronimages
514         OPACPopupAuthorsSearch
515         OpacTopissue
516         opacuserlogin
517         QuoteOfTheDay
518         RequestOnOpac
519         reviewson
520         ShowReviewer
521         ShowReviewerPhoto
522         SocialNetworks
523         suggestion
524         AllowPurchaseSuggestionBranchChoice
525         OpacAllowPublicListCreation
526         OpacAllowSharingPrivateLists
527         OpacRenewalAllowed
528         OpacRenewalBranch
529         OPACViewOthersSuggestions
530         SearchMyLibraryFirst
531         singleBranchMode
532         AnonSuggestions
533         EnableOpacSearchHistory
534         OPACPrivacy
535         opacreadinghistory
536         TrackClicks
537         PatronSelfRegistration
538         OPACShelfBrowser
539         AutoEmailOpacUser
540         AutoEmailPrimaryAddress
541         autoMemberNum
542         BorrowerRenewalPeriodBase
543         checkdigit
544         EnableBorrowerFiles
545         EnhancedMessagingPreferences
546         ExtendedPatronAttributes
547         intranetreadinghistory
548         memberofinstitution
549         patronimages
550         TalkingTechItivaPhoneNotification
551         uppercasesurnames
552         IncludeSeeFromInSearches
553         OpacGroupResults
554         QueryAutoTruncate
555         QueryFuzzy
556         QueryStemming
557         QueryWeightFields
558         TraceCompleteSubfields
559         TraceSubjectSubdivisions
560         UseICU
561         UseQueryParser
562         defaultSortField
563         displayFacetCount
564         OPACdefaultSortField
565         OPACItemsResultsDisplay
566         expandedSearchOption
567         IntranetNumbersPreferPhrase
568         OPACNumbersPreferPhrase
569         opacSerialDefaultTab
570         RenewSerialAddsSuggestion
571         RoutingListAddReserves
572         RoutingSerials
573         SubscriptionHistory
574         Display856uAsImage
575         DisplayIconsXSLT
576         template
577         yuipath
578         HidePatronName
579         intranetbookbag
580         StaffDetailItemSelection
581         viewISBD
582         viewLabeledMARC
583         viewMARC
584         ILS-DI
585         OAI-PMH
586         version
587         AudioAlerts
588         /
589       ) {
590         t::lib::Mocks::mock_preference( $_, $set_value );
591     }
592 }
593
594 #Test if all systempreferences are at $value_to_test
595 sub verif_systempreferences_values {
596     my ( $report, $value_to_test ) = @_;
597
598     foreach my $key ( keys %{$report->{systempreferences}} ) {
599         is( $report->{systempreferences}->{$key}, $value_to_test, "\$report->{systempreferences}->{$key} = $value_to_test" );
600     }
601 }
602
603 $dbh->rollback;