simple payment CSV import
[freeside.git] / FS / FS / cust_pay.pm
1 package FS::cust_pay;
2
3 use strict;
4 use vars qw( @ISA $conf $unsuspendauto $ignore_noapply @encrypted_fields );
5 use Date::Format;
6 use Business::CreditCard;
7 use Text::Template;
8 use FS::Misc qw(send_email);
9 use FS::Record qw( dbh qsearch qsearchs );
10 use FS::payby;
11 use FS::cust_main_Mixin;
12 use FS::payinfo_Mixin;
13 use FS::cust_bill;
14 use FS::cust_bill_pay;
15 use FS::cust_pay_refund;
16 use FS::cust_main;
17 use FS::cust_pay_void;
18
19 @ISA = qw(FS::Record FS::cust_main_Mixin FS::payinfo_Mixin  );
20
21 $ignore_noapply = 0;
22
23 #ask FS::UID to run this stuff for us later
24 FS::UID->install_callback( sub { 
25   $conf = new FS::Conf;
26   $unsuspendauto = $conf->exists('unsuspendauto');
27 } );
28
29 @encrypted_fields = ('payinfo');
30
31 =head1 NAME
32
33 FS::cust_pay - Object methods for cust_pay objects
34
35 =head1 SYNOPSIS
36
37   use FS::cust_pay;
38
39   $record = new FS::cust_pay \%hash;
40   $record = new FS::cust_pay { 'column' => 'value' };
41
42   $error = $record->insert;
43
44   $error = $new_record->replace($old_record);
45
46   $error = $record->delete;
47
48   $error = $record->check;
49
50 =head1 DESCRIPTION
51
52 An FS::cust_pay object represents a payment; the transfer of money from a
53 customer.  FS::cust_pay inherits from FS::Record.  The following fields are
54 currently supported:
55
56 =over 4
57
58 =item paynum - primary key (assigned automatically for new payments)
59
60 =item custnum - customer (see L<FS::cust_main>)
61
62 =item paid - Amount of this payment
63
64 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
65 L<Time::Local> and L<Date::Parse> for conversion functions.
66
67 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
68
69 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
70
71 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
72
73 =item paybatch - text field for tracking card processing or other batch grouping
74
75 =item payunique - Optional unique identifer to prevent duplicate transactions.
76
77 =item closed - books closed flag, empty or `Y'
78
79 =back
80
81 =head1 METHODS
82
83 =over 4 
84
85 =item new HASHREF
86
87 Creates a new payment.  To add the payment to the databse, see L<"insert">.
88
89 =cut
90
91 sub table { 'cust_pay'; }
92 sub cust_linked { $_[0]->cust_main_custnum; } 
93 sub cust_unlinked_msg {
94   my $self = shift;
95   "WARNING: can't find cust_main.custnum ". $self->custnum.
96   ' (cust_pay.paynum '. $self->paynum. ')';
97 }
98
99 =item insert
100
101 Adds this payment to the database.
102
103 For backwards-compatibility and convenience, if the additional field invnum
104 is defined, an FS::cust_bill_pay record for the full amount of the payment
105 will be created.  In this case, custnum is optional.  An hash of optional
106 arguments may be passed.  Currently "manual" is supported.  If true, a
107 payment receipt is sent instead of a statement when 'payment_receipt_email'
108 configuration option is set.
109
110 =cut
111
112 sub insert {
113   my ($self, %options) = @_;
114
115   local $SIG{HUP} = 'IGNORE';
116   local $SIG{INT} = 'IGNORE';
117   local $SIG{QUIT} = 'IGNORE';
118   local $SIG{TERM} = 'IGNORE';
119   local $SIG{TSTP} = 'IGNORE';
120   local $SIG{PIPE} = 'IGNORE';
121
122   my $oldAutoCommit = $FS::UID::AutoCommit;
123   local $FS::UID::AutoCommit = 0;
124   my $dbh = dbh;
125
126   my $cust_bill;
127   if ( $self->invnum ) {
128     $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
129       or do {
130         $dbh->rollback if $oldAutoCommit;
131         return "Unknown cust_bill.invnum: ". $self->invnum;
132       };
133     $self->custnum($cust_bill->custnum );
134   }
135
136
137   my $error = $self->check;
138   return $error if $error;
139
140   my $cust_main = $self->cust_main;
141   my $old_balance = $cust_main->balance;
142
143   $error = $self->SUPER::insert;
144   if ( $error ) {
145     $dbh->rollback if $oldAutoCommit;
146     return "error inserting $self: $error";
147   }
148
149   if ( $self->invnum ) {
150     my $cust_bill_pay = new FS::cust_bill_pay {
151       'invnum' => $self->invnum,
152       'paynum' => $self->paynum,
153       'amount' => $self->paid,
154       '_date'  => $self->_date,
155     };
156     $error = $cust_bill_pay->insert;
157     if ( $error ) {
158       if ( $ignore_noapply ) {
159         warn "warning: error inserting $cust_bill_pay: $error ".
160              "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
161       } else {
162         $dbh->rollback if $oldAutoCommit;
163         return "error inserting $cust_bill_pay: $error";
164       }
165     }
166   }
167
168   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
169
170   #false laziness w/ cust_credit::insert
171   if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
172     my @errors = $cust_main->unsuspend;
173     #return 
174     # side-fx with nested transactions?  upstack rolls back?
175     warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
176          join(' / ', @errors)
177       if @errors;
178   }
179   #eslaf
180
181   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
182
183   #my $cust_main = $self->cust_main;
184   if ( $conf->exists('payment_receipt_email')
185        && grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list
186   ) {
187
188     $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
189
190     my $error;
191     if (    ( exists($options{'manual'}) && $options{'manual'} )
192          || ! $conf->exists('invoice_html_statement')
193          || ! $cust_bill
194        ) {
195
196       my $receipt_template = new Text::Template (
197         TYPE   => 'ARRAY',
198         SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
199       ) or do {
200         warn "can't create payment receipt template: $Text::Template::ERROR";
201         return '';
202       };
203
204       my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
205                              $cust_main->invoicing_list;
206
207       my $payby = $self->payby;
208       my $payinfo = $self->payinfo;
209       $payby =~ s/^BILL$/Check/ if $payinfo;
210       $payinfo = $self->paymask if $payby eq 'CARD' || $payby eq 'CHEK';
211       $payby =~ s/^CHEK$/Electronic check/;
212
213       $error = send_email(
214         'from'    => $conf->config('invoice_from'), #??? well as good as any
215         'to'      => \@invoicing_list,
216         'subject' => 'Payment receipt',
217         'body'    => [ $receipt_template->fill_in( HASH => {
218                        'date'    => time2str("%a %B %o, %Y", $self->_date),
219                        'name'    => $cust_main->name,
220                        'paynum'  => $self->paynum,
221                        'paid'    => sprintf("%.2f", $self->paid),
222                        'payby'   => ucfirst(lc($payby)),
223                        'payinfo' => $payinfo,
224                        'balance' => $cust_main->balance,
225                      } ) ],
226       );
227
228     } else {
229
230       my $queue = new FS::queue {
231          'paynum' => $self->paynum,
232          'job'    => 'FS::cust_bill::queueable_email',
233       };
234       $error = $queue->insert(
235         'invnum' => $cust_bill->invnum,
236         'template' => 'statement',
237       );
238
239     }
240
241     if ( $error ) {
242       warn "can't send payment receipt/statement: $error";
243     }
244
245   }
246
247   '';
248
249 }
250
251 =item void [ REASON ]
252
253 Voids this payment: deletes the payment and all associated applications and
254 adds a record of the voided payment to the FS::cust_pay_void table.
255
256 =cut
257
258 sub void {
259   my $self = shift;
260
261   local $SIG{HUP} = 'IGNORE';
262   local $SIG{INT} = 'IGNORE';
263   local $SIG{QUIT} = 'IGNORE';
264   local $SIG{TERM} = 'IGNORE';
265   local $SIG{TSTP} = 'IGNORE';
266   local $SIG{PIPE} = 'IGNORE';
267
268   my $oldAutoCommit = $FS::UID::AutoCommit;
269   local $FS::UID::AutoCommit = 0;
270   my $dbh = dbh;
271
272   my $cust_pay_void = new FS::cust_pay_void ( {
273     map { $_ => $self->get($_) } $self->fields
274   } );
275   $cust_pay_void->reason(shift) if scalar(@_);
276   my $error = $cust_pay_void->insert;
277   if ( $error ) {
278     $dbh->rollback if $oldAutoCommit;
279     return $error;
280   }
281
282   $error = $self->delete;
283   if ( $error ) {
284     $dbh->rollback if $oldAutoCommit;
285     return $error;
286   }
287
288   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
289
290   '';
291
292 }
293
294 =item delete
295
296 Unless the closed flag is set, deletes this payment and all associated
297 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>).  In most
298 cases, you want to use the void method instead to leave a record of the
299 deleted payment.
300
301 =cut
302
303 # very similar to FS::cust_credit::delete
304 sub delete {
305   my $self = shift;
306   return "Can't delete closed payment" if $self->closed =~ /^Y/i;
307
308   local $SIG{HUP} = 'IGNORE';
309   local $SIG{INT} = 'IGNORE';
310   local $SIG{QUIT} = 'IGNORE';
311   local $SIG{TERM} = 'IGNORE';
312   local $SIG{TSTP} = 'IGNORE';
313   local $SIG{PIPE} = 'IGNORE';
314
315   my $oldAutoCommit = $FS::UID::AutoCommit;
316   local $FS::UID::AutoCommit = 0;
317   my $dbh = dbh;
318
319   foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
320     my $error = $app->delete;
321     if ( $error ) {
322       $dbh->rollback if $oldAutoCommit;
323       return $error;
324     }
325   }
326
327   my $error = $self->SUPER::delete(@_);
328   if ( $error ) {
329     $dbh->rollback if $oldAutoCommit;
330     return $error;
331   }
332
333   if ( $conf->config('deletepayments') ne '' ) {
334
335     my $cust_main = $self->cust_main;
336
337     my $error = send_email(
338       'from'    => $conf->config('invoice_from'), #??? well as good as any
339       'to'      => $conf->config('deletepayments'),
340       'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
341       'body'    => [
342         "This is an automatic message from your Freeside installation\n",
343         "informing you that the following payment has been deleted:\n",
344         "\n",
345         'paynum: '. $self->paynum. "\n",
346         'custnum: '. $self->custnum.
347           " (". $cust_main->last. ", ". $cust_main->first. ")\n",
348         'paid: $'. sprintf("%.2f", $self->paid). "\n",
349         'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
350         'payby: '. $self->payby. "\n",
351         'payinfo: '. $self->paymask. "\n",
352         'paybatch: '. $self->paybatch. "\n",
353       ],
354     );
355
356     if ( $error ) {
357       $dbh->rollback if $oldAutoCommit;
358       return "can't send payment deletion notification: $error";
359     }
360
361   }
362
363   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
364
365   '';
366
367 }
368
369 =item replace OLD_RECORD
370
371 You can, but probably shouldn't modify payments...
372
373 =cut
374
375 sub replace {
376   #return "Can't modify payment!"
377   my $self = shift;
378   return "Can't modify closed payment" if $self->closed =~ /^Y/i;
379   $self->SUPER::replace(@_);
380 }
381
382 =item check
383
384 Checks all fields to make sure this is a valid payment.  If there is an error,
385 returns the error, otherwise returns false.  Called by the insert method.
386
387 =cut
388
389 sub check {
390   my $self = shift;
391
392   my $error =
393     $self->ut_numbern('paynum')
394     || $self->ut_numbern('custnum')
395     || $self->ut_money('paid')
396     || $self->ut_numbern('_date')
397     || $self->ut_textn('paybatch')
398     || $self->ut_textn('payunique')
399     || $self->ut_enum('closed', [ '', 'Y' ])
400     || $self->payinfo_check()
401   ;
402   return $error if $error;
403
404   return "paid must be > 0 " if $self->paid <= 0;
405
406   return "unknown cust_main.custnum: ". $self->custnum
407     unless $self->invnum
408            || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
409
410   $self->_date(time) unless $self->_date;
411
412 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
413 #  # UNIQUE index should catch this too, without race conditions, but this
414 #  # should give a better error message the other 99.9% of the time...
415 #  if ( length($self->payunique)
416 #       && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
417 #    #well, it *could* be a better error message
418 #    return "duplicate transaction".
419 #           " - a payment with unique identifer ". $self->payunique.
420 #           " already exists";
421 #  }
422
423   $self->SUPER::check;
424 }
425
426 =item batch_insert CUST_PAY_OBJECT, ...
427
428 Class method which inserts multiple payments.  Takes a list of FS::cust_pay
429 objects.  Returns a list, each element representing the status of inserting the
430 corresponding payment - empty.  If there is an error inserting any payment, the
431 entire transaction is rolled back, i.e. all payments are inserted or none are.
432
433 For example:
434
435   my @errors = FS::cust_pay->batch_insert(@cust_pay);
436   my $num_errors = scalar(grep $_, @errors);
437   if ( $num_errors == 0 ) {
438     #success; all payments were inserted
439   } else {
440     #failure; no payments were inserted.
441   }
442
443 =cut
444
445 sub batch_insert {
446   my $self = shift; #class method
447
448   local $SIG{HUP} = 'IGNORE';
449   local $SIG{INT} = 'IGNORE';
450   local $SIG{QUIT} = 'IGNORE';
451   local $SIG{TERM} = 'IGNORE';
452   local $SIG{TSTP} = 'IGNORE';
453   local $SIG{PIPE} = 'IGNORE';
454
455   my $oldAutoCommit = $FS::UID::AutoCommit;
456   local $FS::UID::AutoCommit = 0;
457   my $dbh = dbh;
458
459   my $errors = 0;
460   
461   my @errors = map {
462     my $error = $_->insert( 'manual' => 1 );
463     if ( $error ) { 
464       $errors++;
465     } else {
466       $_->cust_main->apply_payments;
467     }
468     $error;
469   } @_;
470
471   if ( $errors ) {
472     $dbh->rollback if $oldAutoCommit;
473   } else {
474     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
475   }
476
477   @errors;
478
479 }
480
481 =item cust_bill_pay
482
483 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
484 payment.
485
486 =cut
487
488 sub cust_bill_pay {
489   my $self = shift;
490   sort {    $a->_date  <=> $b->_date
491          || $a->invnum <=> $b->invnum }
492     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
493   ;
494 }
495
496 =item cust_pay_refund
497
498 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
499 payment.
500
501 =cut
502
503 sub cust_pay_refund {
504   my $self = shift;
505   sort { $a->_date <=> $b->_date }
506     qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
507   ;
508 }
509
510
511 =item unapplied
512
513 Returns the amount of this payment that is still unapplied; which is
514 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
515 applications (see L<FS::cust_pay_refund>).
516
517 =cut
518
519 sub unapplied {
520   my $self = shift;
521   my $amount = $self->paid;
522   $amount -= $_->amount foreach ( $self->cust_bill_pay );
523   $amount -= $_->amount foreach ( $self->cust_pay_refund );
524   sprintf("%.2f", $amount );
525 }
526
527 =item unrefunded
528
529 Returns the amount of this payment that has not been refuned; which is
530 paid minus all  refund applications (see L<FS::cust_pay_refund>).
531
532 =cut
533
534 sub unrefunded {
535   my $self = shift;
536   my $amount = $self->paid;
537   $amount -= $_->amount foreach ( $self->cust_pay_refund );
538   sprintf("%.2f", $amount );
539 }
540
541
542 =item cust_main
543
544 Returns the parent customer object (see L<FS::cust_main>).
545
546 =cut
547
548 sub cust_main {
549   my $self = shift;
550   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
551 }
552
553 =item payby_name
554
555 Returns a name for the payby field.
556
557 =cut
558
559 sub payby_name {
560   my $self = shift;
561   FS::payby->shortname( $self->payby );
562 }
563
564 =item gatewaynum
565
566 Returns a gatewaynum for the processing gateway.
567
568 =item processor
569
570 Returns a name for the processing gateway.
571
572 =item authorization
573
574 Returns a name for the processing gateway.
575
576 =item order_number
577
578 Returns a name for the processing gateway.
579
580 =cut
581
582 sub gatewaynum    { shift->_parse_paybatch->{'gatewaynum'}; }
583 sub processor     { shift->_parse_paybatch->{'processor'}; }
584 sub authorization { shift->_parse_paybatch->{'authorization'}; }
585 sub order_number  { shift->_parse_paybatch->{'order_number'}; }
586
587 #sucks that this stuff is in paybatch like this in the first place,
588 #but at least other code can start to use new field names
589 #(code nicked from FS::cust_main::realtime_refund_bop)
590 sub _parse_paybatch {
591   my $self = shift;
592
593   $self->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
594     or return {};
595               #"Can't parse paybatch for paynum $options{'paynum'}: ".
596               #  $cust_pay->paybatch;
597
598   my( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
599
600   if ( $gatewaynum ) { #gateway for the payment to be refunded
601
602     my $payment_gateway =
603       qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
604
605     die "payment gateway $gatewaynum not found" #?
606       unless $payment_gateway;
607
608     $processor = $payment_gateway->gateway_module;
609
610   }
611
612   {
613     'gatewaynum'    => $gatewaynum,
614     'processor'     => $processor,
615     'authorization' => $auth,
616     'order_number'  => $order_number,
617   };
618
619 }
620
621 =back
622
623 =head1 CLASS METHODS
624
625 =over 4
626
627 =item unapplied_sql
628
629 Returns an SQL fragment to retreive the unapplied amount.
630
631 =cut 
632
633 sub unapplied_sql {
634   #my $class = shift;
635
636   "paid
637         - COALESCE( 
638                     ( SELECT SUM(amount) FROM cust_bill_pay
639                         WHERE cust_pay.paynum = cust_bill_pay.paynum )
640                     ,0
641                   )
642         - COALESCE(
643                     ( SELECT SUM(amount) FROM cust_pay_refund
644                         WHERE cust_pay.paynum = cust_pay_refund.paynum )
645                     ,0
646                   )
647   ";
648
649 }
650
651 =back
652
653 =head1 SUBROUTINES
654
655 =over 4 
656
657 =item batch_import HASHREF
658
659 Inserts new payments.
660
661 =cut
662
663 sub batch_import {
664   my $param = shift;
665
666   my $fh = $param->{filehandle};
667   my $agentnum = $param->{agentnum};
668   my $format = $param->{'format'};
669   my $paybatch = $param->{'paybatch'};
670
671   # here is the agent virtualization
672   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
673
674   my @fields;
675   my $payby;
676   if ( $format eq 'simple' ) {
677     @fields = qw( custnum agent_custid paid payinfo );
678     $payby = 'BILL';
679   } elsif ( $format eq 'extended' ) {
680     die "unimplemented\n";
681     @fields = qw( );
682     $payby = 'BILL';
683   } else {
684     die "unknown format $format";
685   }
686
687   eval "use Text::CSV_XS;";
688   die $@ if $@;
689
690   my $csv = new Text::CSV_XS;
691
692   my $imported = 0;
693
694   local $SIG{HUP} = 'IGNORE';
695   local $SIG{INT} = 'IGNORE';
696   local $SIG{QUIT} = 'IGNORE';
697   local $SIG{TERM} = 'IGNORE';
698   local $SIG{TSTP} = 'IGNORE';
699   local $SIG{PIPE} = 'IGNORE';
700
701   my $oldAutoCommit = $FS::UID::AutoCommit;
702   local $FS::UID::AutoCommit = 0;
703   my $dbh = dbh;
704   
705   my $line;
706   while ( defined($line=<$fh>) ) {
707
708     $csv->parse($line) or do {
709       $dbh->rollback if $oldAutoCommit;
710       return "can't parse: ". $csv->error_input();
711     };
712
713     my @columns = $csv->fields();
714
715     my %cust_pay = (
716       payby    => $payby,
717       paybatch => $paybatch,
718     );
719
720     my $cust_main;
721     foreach my $field ( @fields ) {
722
723       if ( $field eq 'agent_custid'
724         && $agentnum
725         && $columns[0] =~ /\S+/ )
726       {
727
728         my $agent_custid = $columns[0];
729         my %hash = ( 'agent_custid' => $agent_custid,
730                      'agentnum'     => $agentnum,
731                    );
732
733         if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
734           $dbh->rollback if $oldAutoCommit;
735           return "can't specify custnum with agent_custid $agent_custid";
736         }
737
738         $cust_main = qsearchs({
739                                 'table'     => 'cust_main',
740                                 'hashref'   => \%hash,
741                                 'extra_sql' => $extra_sql,
742                              });
743
744         unless ( $cust_main ) {
745           $dbh->rollback if $oldAutoCommit;
746           return "can't find customer with agent_custid $agent_custid";
747         }
748
749         $field = 'custnum';
750         $columns[0] = $cust_main->custnum;
751       }
752
753       $cust_pay{$field} = shift @columns; 
754     }
755
756     my $cust_pay = new FS::cust_pay( \%cust_pay );
757     my $error = $cust_pay->insert;
758
759     if ( $error ) {
760       $dbh->rollback if $oldAutoCommit;
761       return "can't insert payment for $line: $error";
762     }
763
764     if ( $format eq 'simple' ) {
765       # include agentnum for less surprise?
766       $cust_main = qsearchs({
767                              'table'     => 'cust_main',
768                              'hashref'   => { 'custnum' => $cust_pay->custnum },
769                              'extra_sql' => $extra_sql,
770                            })
771         unless $cust_main;
772
773       unless ( $cust_main ) {
774         $dbh->rollback if $oldAutoCommit;
775         return "can't find customer to which payments apply at line: $line";
776       }
777
778       $error = $cust_main->apply_payments_and_credits;
779       if ( $error ) {
780         $dbh->rollback if $oldAutoCommit;
781         return "can't apply payments to customer for $line: $error";
782       }
783
784     }
785
786     $imported++;
787   }
788
789   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
790
791   return "Empty file!" unless $imported;
792
793   ''; #no error
794
795 }
796
797 =back
798
799 =head1 BUGS
800
801 Delete and replace methods.  
802
803 =head1 SEE ALSO
804
805 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
806 schema.html from the base documentation.
807
808 =cut
809
810 1;
811