51eed7ae230b72df5667d3bd22533d6f6b67e63b
[koha.git] / Koha / Account / Line.pm
1 package Koha::Account::Line;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Carp;
21 use Data::Dumper;
22
23 use C4::Log qw(logaction);
24 use C4::Overdues qw(GetFine);
25
26 use Koha::Account::CreditType;
27 use Koha::Account::DebitType;
28 use Koha::Account::Offsets;
29 use Koha::Database;
30 use Koha::Exceptions::Account;
31 use Koha::Items;
32
33 use base qw(Koha::Object);
34
35 =encoding utf8
36
37 =head1 NAME
38
39 Koha::Account::Line - Koha accountline Object class
40
41 =head1 API
42
43 =head2 Class methods
44
45 =cut
46
47 =head3 patron
48
49 Return the patron linked to this account line
50
51 =cut
52
53 sub patron {
54     my ( $self ) = @_;
55     my $rs = $self->_result->borrowernumber;
56     return unless $rs;
57     return Koha::Patron->_new_from_dbic( $rs );
58 }
59
60 =head3 item
61
62 Return the item linked to this account line if exists
63
64 =cut
65
66 sub item {
67     my ( $self ) = @_;
68     my $rs = $self->_result->itemnumber;
69     return unless $rs;
70     return Koha::Item->_new_from_dbic( $rs );
71 }
72
73 =head3 checkout
74
75 Return the checkout linked to this account line if exists
76
77 =cut
78
79 sub checkout {
80     my ( $self ) = @_;
81     return unless $self->issue_id ;
82
83     $self->{_checkout} ||= Koha::Checkouts->find( $self->issue_id );
84     $self->{_checkout} ||= Koha::Old::Checkouts->find( $self->issue_id );
85     return $self->{_checkout};
86 }
87
88 =head3 credit_type
89
90 Return the credit_type linked to this account line
91
92 =cut
93
94 sub credit_type {
95     my ( $self ) = @_;
96     my $rs = $self->_result->credit_type_code;
97     return unless $rs;
98     return Koha::Account::CreditType->_new_from_dbic( $rs );
99 }
100
101 =head3 debit_type
102
103 Return the debit_type linked to this account line
104
105 =cut
106
107 sub debit_type {
108     my ( $self ) = @_;
109     my $rs = $self->_result->debit_type_code;
110     return unless $rs;
111     return Koha::Account::DebitType->_new_from_dbic( $rs );
112 }
113
114 =head3 credit_offsets
115
116 Return the credit_offsets linked to this account line if some exist
117
118 =cut
119
120 sub credit_offsets {
121     my ( $self ) = @_;
122     my $rs = $self->_result->account_offsets_credits;
123     return unless $rs;
124     return Koha::Account::Offsets->_new_from_dbic($rs);
125 }
126
127 =head3 debit_offsets
128
129 Return the debit_offsets linked to this account line if some exist
130
131 =cut
132
133 sub debit_offsets {
134     my ( $self ) = @_;
135     my $rs = $self->_result->account_offsets_debits;
136     return unless $rs;
137     return Koha::Account::Offsets->_new_from_dbic($rs);
138 }
139
140
141 =head3 credits
142
143   my $credits = $accountline->credits;
144   my $credits = $accountline->credits( $cond, $attr );
145
146 Return the credits linked to this account line if some exist.
147 Search conditions and attributes may be passed if you wish to filter
148 the resultant resultant resultset.
149
150 =cut
151
152 sub credits {
153     my ( $self, $cond, $attr ) = @_;
154
155     unless ( $self->is_debit ) {
156         Koha::Exceptions::Account::IsNotCredit->throw(
157             error => 'Account line ' . $self->id . ' is not a debit'
158         );
159     }
160
161     my $rs =
162       $self->_result->search_related('account_offsets_debits')
163       ->search_related( 'credit', $cond, $attr );
164     return unless $rs;
165     return Koha::Account::Lines->_new_from_dbic($rs);
166 }
167
168 =head3 debits
169
170   my $debits = $accountline->debits;
171   my $debits = $accountline->debits( $cond, $attr );
172
173 Return the debits linked to this account line if some exist.
174 Search conditions and attributes may be passed if you wish to filter
175 the resultant resultant resultset.
176
177 =cut
178
179 sub debits {
180     my ( $self, $cond, $attr ) = @_;
181
182     unless ( $self->is_credit ) {
183         Koha::Exceptions::Account::IsNotCredit->throw(
184             error => 'Account line ' . $self->id . ' is not a credit'
185         );
186     }
187
188     my $rs =
189       $self->_result->search_related('account_offsets_credits')
190       ->search_related( 'debit', $cond, $attr );
191     return unless $rs;
192     return Koha::Account::Lines->_new_from_dbic($rs);
193 }
194
195 =head3 void
196
197   $payment_accountline->void();
198
199 Used to 'void' (or reverse) a payment/credit. It will roll back any offsets
200 created by the application of this credit upon any debits and mark the credit
201 as 'void' by updating it's status to "VOID".
202
203 =cut
204
205 sub void {
206     my ($self) = @_;
207
208     # Make sure it is a payment we are voiding
209     return unless $self->amount < 0;
210
211     my @account_offsets =
212       Koha::Account::Offsets->search(
213         { credit_id => $self->id, amount => { '<' => 0 }  } );
214
215     $self->_result->result_source->schema->txn_do(
216         sub {
217             foreach my $account_offset (@account_offsets) {
218                 my $fee_paid =
219                   Koha::Account::Lines->find( $account_offset->debit_id );
220
221                 next unless $fee_paid;
222
223                 my $amount_paid = $account_offset->amount * -1; # amount paid is stored as a negative amount
224                 my $new_amount = $fee_paid->amountoutstanding + $amount_paid;
225                 $fee_paid->amountoutstanding($new_amount);
226                 $fee_paid->store();
227
228                 Koha::Account::Offset->new(
229                     {
230                         credit_id => $self->id,
231                         debit_id  => $fee_paid->id,
232                         amount    => $amount_paid,
233                         type      => 'Void Payment',
234                     }
235                 )->store();
236             }
237
238             if ( C4::Context->preference("FinesLog") ) {
239                 logaction(
240                     "FINES", 'VOID',
241                     $self->borrowernumber,
242                     Dumper(
243                         {
244                             action         => 'void_payment',
245                             borrowernumber => $self->borrowernumber,
246                             amount            => $self->amount,
247                             amountoutstanding => $self->amountoutstanding,
248                             description       => $self->description,
249                             credit_type_code  => $self->credit_type_code,
250                             payment_type      => $self->payment_type,
251                             note              => $self->note,
252                             itemnumber        => $self->itemnumber,
253                             manager_id        => $self->manager_id,
254                             offsets =>
255                               [ map { $_->unblessed } @account_offsets ],
256                         }
257                     )
258                 );
259             }
260
261             $self->set(
262                 {
263                     status            => 'VOID',
264                     amountoutstanding => 0,
265                     amount            => 0,
266                 }
267             );
268             $self->store();
269         }
270     );
271
272 }
273
274 =head3 reduce
275
276   $charge_accountline->reduce({
277       reduction_type => $reduction_type
278   });
279
280 Used to 'reduce' a charge/debit by adding a credit to offset against the amount
281 outstanding.
282
283 May be used to apply a discount whilst retaining the original debit amounts or
284 to apply a full or partial refund for example when a lost item is found and
285 returned.
286
287 It will immediately be applied to the given debit unless the debit has already
288 been paid, in which case a 'zero' offset will be added to maintain a link to
289 the debit but the outstanding credit will be left so it may be applied to other
290 debts.
291
292 Reduction type may be one of:
293
294 * REFUND
295 * DISCOUNT
296
297 Returns the reduction accountline (which will be a credit)
298
299 =cut
300
301 sub reduce {
302     my ( $self, $params ) = @_;
303
304     # Make sure it is a charge we are reducing
305     unless ( $self->is_debit ) {
306         Koha::Exceptions::Account::IsNotDebit->throw(
307             error => 'Account line ' . $self->id . 'is not a debit' );
308     }
309     if ( $self->debit_type_code eq 'PAYOUT' ) {
310         Koha::Exceptions::Account::IsNotDebit->throw(
311             error => 'Account line ' . $self->id . 'is a payout' );
312     }
313
314     # Check for mandatory parameters
315     my @mandatory = ( 'interface', 'reduction_type', 'amount' );
316     for my $param (@mandatory) {
317         unless ( defined( $params->{$param} ) ) {
318             Koha::Exceptions::MissingParameter->throw(
319                 error => "The $param parameter is mandatory" );
320         }
321     }
322
323     # More mandatory parameters
324     if ( $params->{interface} eq 'intranet' ) {
325         my @optional = ( 'staff_id', 'branch' );
326         for my $param (@optional) {
327             unless ( defined( $params->{$param} ) ) {
328                 Koha::Exceptions::MissingParameter->throw( error =>
329 "The $param parameter is mandatory when interface is set to 'intranet'"
330                 );
331             }
332         }
333     }
334
335     # Make sure the reduction isn't more than the original
336     my $original = $self->amount;
337     Koha::Exceptions::Account::AmountNotPositive->throw(
338         error => 'Reduce amount passed is not positive' )
339       unless ( $params->{amount} > 0 );
340     Koha::Exceptions::ParameterTooHigh->throw( error =>
341 "Amount to reduce ($params->{amount}) is higher than original amount ($original)"
342     ) unless ( $original >= $params->{amount} );
343     my $reduced =
344       $self->credits( { credit_type_code => [ 'DISCOUNT', 'REFUND' ] } )->total;
345     Koha::Exceptions::ParameterTooHigh->throw( error =>
346 "Combined reduction ($params->{amount} + $reduced) is higher than original amount ("
347           . abs($original)
348           . ")" )
349       unless ( $original >= ( $params->{amount} + abs($reduced) ) );
350
351     my $status = { 'REFUND' => 'REFUNDED', 'DISCOUNT' => 'DISCOUNTED' };
352
353     my $reduction;
354     $self->_result->result_source->schema->txn_do(
355         sub {
356
357             # A 'reduction' is a 'credit'
358             $reduction = Koha::Account::Line->new(
359                 {
360                     date              => \'NOW()',
361                     amount            => 0 - $params->{amount},
362                     credit_type_code  => $params->{reduction_type},
363                     status            => 'ADDED',
364                     amountoutstanding => 0 - $params->{amount},
365                     manager_id        => $params->{staff_id},
366                     borrowernumber    => $self->borrowernumber,
367                     interface         => $params->{interface},
368                     branchcode        => $params->{branch},
369                 }
370             )->store();
371
372             my $reduction_offset = Koha::Account::Offset->new(
373                 {
374                     credit_id => $reduction->accountlines_id,
375                     type      => uc( $params->{reduction_type} ),
376                     amount    => $params->{amount}
377                 }
378             )->store();
379
380             # Link reduction to charge (and apply as required)
381             my $debit_outstanding = $self->amountoutstanding;
382             if ( $debit_outstanding >= $params->{amount} ) {
383
384                 $reduction->apply(
385                     {
386                         debits      => [$self],
387                         offset_type => uc( $params->{reduction_type} )
388                     }
389                 );
390                 $reduction->status('APPLIED')->store();
391             }
392             else {
393
394         # Zero amount offset used to link original 'debit' to reduction 'credit'
395                 my $link_reduction_offset = Koha::Account::Offset->new(
396                     {
397                         credit_id => $reduction->accountlines_id,
398                         debit_id  => $self->accountlines_id,
399                         type      => uc( $params->{reduction_type} ),
400                         amount    => 0
401                     }
402                 )->store();
403             }
404
405             # Update status of original debit
406             $self->status( $status->{ $params->{reduction_type} } )->store;
407         }
408     );
409
410     $reduction->discard_changes;
411     return $reduction;
412 }
413
414 =head3 apply
415
416     my $debits = $account->outstanding_debits;
417     my $outstanding_amount = $credit->apply( { debits => $debits, [ offset_type => $offset_type ] } );
418
419 Applies the credit to a given debits array reference.
420
421 =head4 arguments hashref
422
423 =over 4
424
425 =item debits - Koha::Account::Lines object set of debits
426
427 =item offset_type (optional) - a string indicating the offset type (valid values are those from
428 the 'account_offset_types' table)
429
430 =back
431
432 =cut
433
434 sub apply {
435     my ( $self, $params ) = @_;
436
437     my $debits      = $params->{debits};
438     my $offset_type = $params->{offset_type} // 'Credit Applied';
439
440     unless ( $self->is_credit ) {
441         Koha::Exceptions::Account::IsNotCredit->throw(
442             error => 'Account line ' . $self->id . ' is not a credit'
443         );
444     }
445
446     my $available_credit = $self->amountoutstanding * -1;
447
448     unless ( $available_credit > 0 ) {
449         Koha::Exceptions::Account::NoAvailableCredit->throw(
450             error => 'Outstanding credit is ' . $available_credit . ' and cannot be applied'
451         );
452     }
453
454     my $schema = Koha::Database->new->schema;
455
456     # Item numbers that have had a fine paid where the line has a accounttype
457     # of OVERDUE and a status of UNRETURNED. We might want to try and renew
458     # these items.
459     my $overdue_unreturned = {};
460
461     $schema->txn_do( sub {
462         for my $debit ( @{$debits} ) {
463
464             unless ( $debit->is_debit ) {
465                 Koha::Exceptions::Account::IsNotDebit->throw(
466                     error => 'Account line ' . $debit->id . 'is not a debit'
467                 );
468             }
469             my $amount_to_cancel;
470             my $owed = $debit->amountoutstanding;
471
472             if ( $available_credit >= $owed ) {
473                 $amount_to_cancel = $owed;
474             }
475             else {    # $available_credit < $debit->amountoutstanding
476                 $amount_to_cancel = $available_credit;
477             }
478
479             # record the account offset
480             Koha::Account::Offset->new(
481                 {   credit_id => $self->id,
482                     debit_id  => $debit->id,
483                     amount    => $amount_to_cancel * -1,
484                     type      => $offset_type,
485                 }
486             )->store();
487
488             $available_credit -= $amount_to_cancel;
489
490             $self->amountoutstanding( $available_credit * -1 )->store;
491             $debit->amountoutstanding( $owed - $amount_to_cancel )->store;
492
493             # If we need to make a note of the item associated with this line,
494             # in order that we can potentially renew it, do so.
495             # Same logic existing in Koha::Account::pay
496             if (
497                 $debit->amountoutstanding == 0 &&
498                 $debit->accounttype &&
499                 $debit->accounttype eq 'OVERDUE' &&
500                 $debit->status &&
501                 $debit->status eq 'UNRETURNED'
502             ) {
503                 $overdue_unreturned->{$debit->itemnumber} = $debit;
504             }
505
506             # Same logic exists in Koha::Account::pay
507             if (   $debit->amountoutstanding == 0
508                 && $debit->itemnumber
509                 && $debit->debit_type_code
510                 && $debit->debit_type_code eq 'LOST' )
511             {
512                 C4::Circulation::ReturnLostItem( $self->borrowernumber, $debit->itemnumber );
513             }
514
515         }
516     });
517
518     # If we have overdue unreturned items that have had payments made
519     # against them, check whether the balance on those items is now zero
520     # and, if the syspref is set, renew them
521     # Same logic existing in Koha::Account::pay
522     if (
523         C4::Context->preference('RenewAccruingItemWhenPaid') &&
524         keys %{$overdue_unreturned}
525     ) {
526         foreach my $itemnumber (keys %{$overdue_unreturned}) {
527             # Only do something if this item has no fines left on it
528             my $fine = C4::Overdues::GetFine( $itemnumber, $self->borrowernumber );
529             next if $fine && $fine > 0;
530
531             my ( $renew_ok, $error ) =
532                 C4::Circulation::CanBookBeRenewed(
533                     $self->borrowernumber, $itemnumber
534                 );
535             if ( $renew_ok ) {
536                 C4::Circulation::AddRenewal(
537                     $self->borrowernumber,
538                     $itemnumber,
539                     $overdue_unreturned->{$itemnumber}->{branchcode},
540                     undef,
541                     undef,
542                     1
543                 );
544             }
545         }
546     }
547
548     return $available_credit;
549 }
550
551 =head3 payout
552
553   $credit_accountline->payout(
554     {
555         payout_type => $payout_type,
556         register_id => $register_id,
557         staff_id    => $staff_id,
558         interface   => 'intranet',
559         amount      => $amount
560     }
561   );
562
563 Used to 'pay out' a credit to a user.
564
565 Payout type may be one of any existing payment types
566
567 Returns the payout debit line that is created via this transaction.
568
569 =cut
570
571 sub payout {
572     my ( $self, $params ) = @_;
573
574     # Make sure it is a credit we are paying out
575     unless ( $self->is_credit ) {
576         Koha::Exceptions::Account::IsNotCredit->throw(
577             error => 'Account line ' . $self->id . ' is not a credit' );
578     }
579
580     # Check for mandatory parameters
581     my @mandatory =
582       ( 'interface', 'staff_id', 'branch', 'payout_type', 'amount' );
583     for my $param (@mandatory) {
584         unless ( defined( $params->{$param} ) ) {
585             Koha::Exceptions::MissingParameter->throw(
586                 error => "The $param parameter is mandatory" );
587         }
588     }
589
590     # Make sure there is outstanding credit to pay out
591     my $outstanding = -1 * $self->amountoutstanding;
592     my $amount =
593       $params->{amount} ? $params->{amount} : $outstanding;
594     Koha::Exceptions::Account::AmountNotPositive->throw(
595         error => 'Payout amount passed is not positive' )
596       unless ( $amount > 0 );
597     Koha::Exceptions::ParameterTooHigh->throw(
598         error => "Amount to payout ($amount) is higher than amountoutstanding ($outstanding)" )
599       unless ($outstanding >= $amount );
600
601     # Make sure we record the cash register for cash transactions
602     Koha::Exceptions::Account::RegisterRequired->throw()
603       if ( C4::Context->preference("UseCashRegisters")
604         && defined( $params->{payout_type} )
605         && ( $params->{payout_type} eq 'CASH' )
606         && !defined( $params->{cash_register} ) );
607
608     my $payout;
609     $self->_result->result_source->schema->txn_do(
610         sub {
611
612             # A 'payout' is a 'debit'
613             $payout = Koha::Account::Line->new(
614                 {
615                     date              => \'NOW()',
616                     amount            => $amount,
617                     debit_type_code   => 'PAYOUT',
618                     payment_type      => $params->{payout_type},
619                     amountoutstanding => $amount,
620                     manager_id        => $params->{staff_id},
621                     borrowernumber    => $self->borrowernumber,
622                     interface         => $params->{interface},
623                     branchcode        => $params->{branch},
624                     register_id       => $params->{cash_register}
625                 }
626             )->store();
627
628             my $payout_offset = Koha::Account::Offset->new(
629                 {
630                     debit_id => $payout->accountlines_id,
631                     type     => 'PAYOUT',
632                     amount   => $amount
633                 }
634             )->store();
635
636             $self->apply( { debits => [$payout], offset_type => 'PAYOUT' } );
637             $self->status('PAID')->store;
638         }
639     );
640
641     $payout->discard_changes;
642     return $payout;
643 }
644
645 =head3 adjust
646
647 This method allows updating a debit or credit on a patron's account
648
649     $account_line->adjust(
650         {
651             amount    => $amount,
652             type      => $update_type,
653             interface => $interface
654         }
655     );
656
657 $update_type can be any of:
658   - overdue_update
659
660 Authors Note: The intention here is that this method is only used
661 to adjust accountlines where the final amount is not yet known/fixed.
662 Incrementing fines are the only existing case at the time of writing,
663 all other forms of 'adjustment' should be recorded as distinct credits
664 or debits and applied, via an offset, to the corresponding debit or credit.
665
666 =cut
667
668 sub adjust {
669     my ( $self, $params ) = @_;
670
671     my $amount       = $params->{amount};
672     my $update_type  = $params->{type};
673     my $interface    = $params->{interface};
674
675     unless ( exists($Koha::Account::Line::allowed_update->{$update_type}) ) {
676         Koha::Exceptions::Account::UnrecognisedType->throw(
677             error => 'Update type not recognised'
678         );
679     }
680
681     my $debit_type_code = $self->debit_type_code;
682     my $account_status  = $self->status;
683     unless (
684         (
685             exists(
686                 $Koha::Account::Line::allowed_update->{$update_type}
687                   ->{$debit_type_code}
688             )
689             && ( $Koha::Account::Line::allowed_update->{$update_type}
690                 ->{$debit_type_code} eq $account_status )
691         )
692       )
693     {
694         Koha::Exceptions::Account::UnrecognisedType->throw(
695             error => 'Update type not allowed on this debit_type' );
696     }
697
698     my $schema = Koha::Database->new->schema;
699
700     $schema->txn_do(
701         sub {
702
703             my $amount_before             = $self->amount;
704             my $amount_outstanding_before = $self->amountoutstanding;
705             my $difference                = $amount - $amount_before;
706             my $new_outstanding           = $amount_outstanding_before + $difference;
707
708             my $offset_type = $debit_type_code;
709             $offset_type .= ( $difference > 0 ) ? "_INCREASE" : "_DECREASE";
710
711             # Catch cases that require patron refunds
712             if ( $new_outstanding < 0 ) {
713                 my $account =
714                   Koha::Patrons->find( $self->borrowernumber )->account;
715                 my $credit = $account->add_credit(
716                     {
717                         amount      => $new_outstanding * -1,
718                         description => 'Overpayment refund',
719                         type        => 'CREDIT',
720                         interface   => $interface,
721                         ( $update_type eq 'overdue_update' ? ( item_id => $self->itemnumber ) : ()),
722                     }
723                 );
724                 $new_outstanding = 0;
725             }
726
727             # Update the account line
728             $self->set(
729                 {
730                     date              => \'NOW()',
731                     amount            => $amount,
732                     amountoutstanding => $new_outstanding,
733                 }
734             )->store();
735
736             # Record the account offset
737             my $account_offset = Koha::Account::Offset->new(
738                 {
739                     debit_id => $self->id,
740                     type     => $offset_type,
741                     amount   => $difference
742                 }
743             )->store();
744
745             if ( C4::Context->preference("FinesLog") ) {
746                 logaction(
747                     "FINES", 'UPDATE', #undef becomes UPDATE in UpdateFine
748                     $self->borrowernumber,
749                     Dumper(
750                         {   action            => $update_type,
751                             borrowernumber    => $self->borrowernumber,
752                             amount            => $amount,
753                             description       => undef,
754                             amountoutstanding => $new_outstanding,
755                             debit_type_code   => $self->debit_type_code,
756                             note              => undef,
757                             itemnumber        => $self->itemnumber,
758                             manager_id        => undef,
759                         }
760                     )
761                 ) if ( $update_type eq 'overdue_update' );
762             }
763         }
764     );
765
766     return $self;
767 }
768
769 =head3 is_credit
770
771     my $bool = $line->is_credit;
772
773 =cut
774
775 sub is_credit {
776     my ($self) = @_;
777
778     return ( $self->amount < 0 );
779 }
780
781 =head3 is_debit
782
783     my $bool = $line->is_debit;
784
785 =cut
786
787 sub is_debit {
788     my ($self) = @_;
789
790     return !$self->is_credit;
791 }
792
793 =head3 to_api_mapping
794
795 This method returns the mapping for representing a Koha::Account::Line object
796 on the API.
797
798 =cut
799
800 sub to_api_mapping {
801     return {
802         accountlines_id   => 'account_line_id',
803         credit_type_code  => 'credit_type',
804         debit_type_code   => 'debit_type',
805         amountoutstanding => 'amount_outstanding',
806         borrowernumber    => 'patron_id',
807         branchcode        => 'library_id',
808         issue_id          => 'checkout_id',
809         itemnumber        => 'item_id',
810         manager_id        => 'user_id',
811         note              => 'internal_note',
812     };
813 }
814
815 =head2 Internal methods
816
817 =cut
818
819 =head3 _type
820
821 =cut
822
823 sub _type {
824     return 'Accountline';
825 }
826
827 1;
828
829 =head2 Name mappings
830
831 =head3 $allowed_update
832
833 =cut
834
835 our $allowed_update = { 'overdue_update' => { 'OVERDUE' => 'UNRETURNED' } };
836
837 =head1 AUTHORS
838
839 Kyle M Hall <kyle@bywatersolutions.com >
840 Tomás Cohen Arazi <tomascohen@theke.io>
841 Martin Renvoize <martin.renvoize@ptfs-europe.com>
842
843 =cut