prevent edge-case business-onlinepayment mod_perl leakage in multi-database
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $xaction $E_NoErr );
7 use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
8 use vars qw( $ach_processor $ach_login $ach_password $ach_action @ach_options );
9 use vars qw( $invoice_lines @buf ); #yuck
10 use vars qw( $quiet );
11 use Date::Format;
12 use Mail::Internet 1.44;
13 use Mail::Header;
14 use Text::Template;
15 use FS::UID qw( datasrc );
16 use FS::Record qw( qsearch qsearchs );
17 use FS::cust_main;
18 use FS::cust_bill_pkg;
19 use FS::cust_credit;
20 use FS::cust_pay;
21 use FS::cust_pkg;
22 use FS::cust_credit_bill;
23 use FS::cust_pay_batch;
24 use FS::cust_bill_event;
25
26 @ISA = qw( FS::Record );
27
28 #ask FS::UID to run this stuff for us later
29 $FS::UID::callback{'FS::cust_bill'} = sub { 
30
31   $conf = new FS::Conf;
32
33   $money_char = $conf->config('money_char') || '$';  
34
35   $lpr = $conf->config('lpr');
36   $invoice_from = $conf->config('invoice_from');
37   $smtpmachine = $conf->config('smtpmachine');
38
39   ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
40   @bop_options = ();
41   ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
42   @ach_options = ();
43
44   if ( $conf->exists('business-onlinepayment') ) {
45     ( $bop_processor,
46       $bop_login,
47       $bop_password,
48       $bop_action,
49       @bop_options
50     ) = $conf->config('business-onlinepayment');
51     $bop_action ||= 'normal authorization';
52     ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
53       ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
54     eval "use Business::OnlinePayment";  
55   }
56
57   if ( $conf->exists('business-onlinepayment-ach') ) {
58     ( $ach_processor,
59       $ach_login,
60       $ach_password,
61       $ach_action,
62       @ach_options
63     ) = $conf->config('business-onlinepayment-ach');
64     $ach_action ||= 'normal authorization';
65     eval "use Business::OnlinePayment";  
66   }
67
68 };
69
70 =head1 NAME
71
72 FS::cust_bill - Object methods for cust_bill records
73
74 =head1 SYNOPSIS
75
76   use FS::cust_bill;
77
78   $record = new FS::cust_bill \%hash;
79   $record = new FS::cust_bill { 'column' => 'value' };
80
81   $error = $record->insert;
82
83   $error = $new_record->replace($old_record);
84
85   $error = $record->delete;
86
87   $error = $record->check;
88
89   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
90
91   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
92
93   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
94
95   @cust_pay_objects = $cust_bill->cust_pay;
96
97   $tax_amount = $record->tax;
98
99   @lines = $cust_bill->print_text;
100   @lines = $cust_bill->print_text $time;
101
102 =head1 DESCRIPTION
103
104 An FS::cust_bill object represents an invoice; a declaration that a customer
105 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
106 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
107 following fields are currently supported:
108
109 =over 4
110
111 =item invnum - primary key (assigned automatically for new invoices)
112
113 =item custnum - customer (see L<FS::cust_main>)
114
115 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
116 L<Time::Local> and L<Date::Parse> for conversion functions.
117
118 =item charged - amount of this invoice
119
120 =item printed - deprecated
121
122 =item closed - books closed flag, empty or `Y'
123
124 =back
125
126 =head1 METHODS
127
128 =over 4
129
130 =item new HASHREF
131
132 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
133 Invoices are normally created by calling the bill method of a customer object
134 (see L<FS::cust_main>).
135
136 =cut
137
138 sub table { 'cust_bill'; }
139
140 =item insert
141
142 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
143 returns the error, otherwise returns false.
144
145 =item delete
146
147 Currently unimplemented.  I don't remove invoices because there would then be
148 no record you ever posted this invoice (which is bad, no?)
149
150 =cut
151
152 sub delete {
153   my $self = shift;
154   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
155   $self->SUPER::delete(@_);
156 }
157
158 =item replace OLD_RECORD
159
160 Replaces the OLD_RECORD with this one in the database.  If there is an error,
161 returns the error, otherwise returns false.
162
163 Only printed may be changed.  printed is normally updated by calling the
164 collect method of a customer object (see L<FS::cust_main>).
165
166 =cut
167
168 sub replace {
169   my( $new, $old ) = ( shift, shift );
170   return "Can't change custnum!" unless $old->custnum == $new->custnum;
171   #return "Can't change _date!" unless $old->_date eq $new->_date;
172   return "Can't change _date!" unless $old->_date == $new->_date;
173   return "Can't change charged!" unless $old->charged == $new->charged;
174
175   $new->SUPER::replace($old);
176 }
177
178 =item check
179
180 Checks all fields to make sure this is a valid invoice.  If there is an error,
181 returns the error, otherwise returns false.  Called by the insert and replace
182 methods.
183
184 =cut
185
186 sub check {
187   my $self = shift;
188
189   my $error =
190     $self->ut_numbern('invnum')
191     || $self->ut_number('custnum')
192     || $self->ut_numbern('_date')
193     || $self->ut_money('charged')
194     || $self->ut_numbern('printed')
195     || $self->ut_enum('closed', [ '', 'Y' ])
196   ;
197   return $error if $error;
198
199   return "Unknown customer"
200     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
201
202   $self->_date(time) unless $self->_date;
203
204   $self->printed(0) if $self->printed eq '';
205
206   ''; #no error
207 }
208
209 =item previous
210
211 Returns a list consisting of the total previous balance for this customer, 
212 followed by the previous outstanding invoices (as FS::cust_bill objects also).
213
214 =cut
215
216 sub previous {
217   my $self = shift;
218   my $total = 0;
219   my @cust_bill = sort { $a->_date <=> $b->_date }
220     grep { $_->owed != 0 && $_->_date < $self->_date }
221       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
222   ;
223   foreach ( @cust_bill ) { $total += $_->owed; }
224   $total, @cust_bill;
225 }
226
227 =item cust_bill_pkg
228
229 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
230
231 =cut
232
233 sub cust_bill_pkg {
234   my $self = shift;
235   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
236 }
237
238 =item cust_bill_event
239
240 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
241 invoice.
242
243 =cut
244
245 sub cust_bill_event {
246   my $self = shift;
247   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
248 }
249
250
251 =item cust_main
252
253 Returns the customer (see L<FS::cust_main>) for this invoice.
254
255 =cut
256
257 sub cust_main {
258   my $self = shift;
259   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
260 }
261
262 =item cust_credit
263
264 Depreciated.  See the cust_credited method.
265
266  #Returns a list consisting of the total previous credited (see
267  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
268  #outstanding credits (FS::cust_credit objects).
269
270 =cut
271
272 sub cust_credit {
273   use Carp;
274   croak "FS::cust_bill->cust_credit depreciated; see ".
275         "FS::cust_bill->cust_credit_bill";
276   #my $self = shift;
277   #my $total = 0;
278   #my @cust_credit = sort { $a->_date <=> $b->_date }
279   #  grep { $_->credited != 0 && $_->_date < $self->_date }
280   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
281   #;
282   #foreach (@cust_credit) { $total += $_->credited; }
283   #$total, @cust_credit;
284 }
285
286 =item cust_pay
287
288 Depreciated.  See the cust_bill_pay method.
289
290 #Returns all payments (see L<FS::cust_pay>) for this invoice.
291
292 =cut
293
294 sub cust_pay {
295   use Carp;
296   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
297   #my $self = shift;
298   #sort { $a->_date <=> $b->_date }
299   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
300   #;
301 }
302
303 =item cust_bill_pay
304
305 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
306
307 =cut
308
309 sub cust_bill_pay {
310   my $self = shift;
311   sort { $a->_date <=> $b->_date }
312     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
313 }
314
315 =item cust_credited
316
317 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
318
319 =cut
320
321 sub cust_credited {
322   my $self = shift;
323   sort { $a->_date <=> $b->_date }
324     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
325   ;
326 }
327
328 =item tax
329
330 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
331
332 =cut
333
334 sub tax {
335   my $self = shift;
336   my $total = 0;
337   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
338                                              'pkgnum' => 0 } );
339   foreach (@taxlines) { $total += $_->setup; }
340   $total;
341 }
342
343 =item owed
344
345 Returns the amount owed (still outstanding) on this invoice, which is charged
346 minus all payment applications (see L<FS::cust_bill_pay>) and credit
347 applications (see L<FS::cust_credit_bill>).
348
349 =cut
350
351 sub owed {
352   my $self = shift;
353   my $balance = $self->charged;
354   $balance -= $_->amount foreach ( $self->cust_bill_pay );
355   $balance -= $_->amount foreach ( $self->cust_credited );
356   $balance = sprintf( "%.2f", $balance);
357   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
358   $balance;
359 }
360
361 =item send
362
363 Sends this invoice to the destinations configured for this customer: send
364 emails or print.  See L<FS::cust_main_invoice>.
365
366 =cut
367
368 sub send {
369   my($self,$template) = @_;
370   my @print_text = $self->print_text('', $template);
371   my @invoicing_list = $self->cust_main->invoicing_list;
372
373   if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list  ) { #email
374
375     #better to notify this person than silence
376     @invoicing_list = ($invoice_from) unless @invoicing_list;
377
378     #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
379     #$ENV{SMTPHOSTS} = $smtpmachine;
380     $ENV{MAILADDRESS} = $invoice_from;
381     my $header = new Mail::Header ( [
382       "From: $invoice_from",
383       "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
384       "Sender: $invoice_from",
385       "Reply-To: $invoice_from",
386       "Date: ". time2str("%a, %d %b %Y %X %z", time),
387       "Subject: Invoice",
388     ] );
389     my $message = new Mail::Internet (
390       'Header' => $header,
391       'Body' => [ @print_text ], #( date)
392     );
393     $!=0;
394     $message->smtpsend( Host => $smtpmachine )
395       or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
396         or return "(customer # ". $self->custnum. ") can't send invoice email".
397                   " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
398                   " via server $smtpmachine with SMTP: $!";
399
400   }
401
402   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
403     open(LPR, "|$lpr")
404       or return "Can't open pipe to $lpr: $!";
405     print LPR @print_text;
406     close LPR
407       or return $! ? "Error closing $lpr: $!"
408                    : "Exit status $? from $lpr";
409   }
410
411   '';
412
413 }
414
415 =item send_csv OPTIONS
416
417 Sends invoice as a CSV data-file to a remote host with the specified protocol.
418
419 Options are:
420
421 protocol - currently only "ftp"
422 server
423 username
424 password
425 dir
426
427 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
428 and YYMMDDHHMMSS is a timestamp.
429
430 The fields of the CSV file is as follows:
431
432 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
433
434 =over 4
435
436 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
437
438 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
439 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
440 fields are filled in.
441
442 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
443 first two fields (B<record_type> and B<invnum>) and the last five fields
444 (B<pkg> through B<edate>) are filled in.
445
446 =item invnum - invoice number
447
448 =item custnum - customer number
449
450 =item _date - invoice date
451
452 =item charged - total invoice amount
453
454 =item first - customer first name
455
456 =item last - customer first name
457
458 =item company - company name
459
460 =item address1 - address line 1
461
462 =item address2 - address line 1
463
464 =item city
465
466 =item state
467
468 =item zip
469
470 =item country
471
472 =item pkg - line item description
473
474 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
475
476 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
477
478 =item sdate - start date for recurring fee
479
480 =item edate - end date for recurring fee
481
482 =back
483
484 =cut
485
486 sub send_csv {
487   my($self, %opt) = @_;
488
489   #part one: create file
490
491   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
492   mkdir $spooldir, 0700 unless -d $spooldir;
493
494   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
495
496   open(CSV, ">$file") or die "can't open $file: $!";
497
498   eval "use Text::CSV_XS";
499   die $@ if $@;
500
501   my $csv = Text::CSV_XS->new({'always_quote'=>1});
502
503   my $cust_main = $self->cust_main;
504
505   $csv->combine(
506     'cust_bill',
507     $self->invnum,
508     $self->custnum,
509     time2str("%x", $self->_date),
510     sprintf("%.2f", $self->charged),
511     ( map { $cust_main->getfield($_) }
512         qw( first last company address1 address2 city state zip country ) ),
513     map { '' } (1..5),
514   ) or die "can't create csv";
515   print CSV $csv->string. "\n";
516
517   #new charges (false laziness w/print_text)
518   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
519
520     my($pkg, $setup, $recur, $sdate, $edate);
521     if ( $cust_bill_pkg->pkgnum ) {
522     
523       ($pkg, $setup, $recur, $sdate, $edate) = (
524         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
525         ( $cust_bill_pkg->setup != 0
526           ? sprintf("%.2f", $cust_bill_pkg->setup )
527           : '' ),
528         ( $cust_bill_pkg->recur != 0
529           ? sprintf("%.2f", $cust_bill_pkg->recur )
530           : '' ),
531         time2str("%x", $cust_bill_pkg->sdate),
532         time2str("%x", $cust_bill_pkg->edate),
533       );
534
535     } else { #pkgnum tax
536       next unless $cust_bill_pkg->setup != 0;
537       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
538                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
539                        : 'Tax';
540       ($pkg, $setup, $recur, $sdate, $edate) =
541         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
542     }
543
544     $csv->combine(
545       'cust_bill_pkg',
546       $self->invnum,
547       ( map { '' } (1..11) ),
548       ($pkg, $setup, $recur, $sdate, $edate)
549     ) or die "can't create csv";
550     print CSV $csv->string. "\n";
551
552   }
553
554   close CSV or die "can't close CSV: $!";
555
556   #part two: upload it
557
558   my $net;
559   if ( $opt{protocol} eq 'ftp' ) {
560     eval "use Net::FTP;";
561     die $@ if $@;
562     $net = Net::FTP->new($opt{server}) or die @$;
563   } else {
564     die "unknown protocol: $opt{protocol}";
565   }
566
567   $net->login( $opt{username}, $opt{password} )
568     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
569
570   $net->binary or die "can't set binary mode";
571
572   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
573
574   $net->put($file) or die "can't put $file: $!";
575
576   $net->quit;
577
578   unlink $file;
579
580 }
581
582 =item comp
583
584 Pays this invoice with a compliemntary payment.  If there is an error,
585 returns the error, otherwise returns false.
586
587 =cut
588
589 sub comp {
590   my $self = shift;
591   my $cust_pay = new FS::cust_pay ( {
592     'invnum'   => $self->invnum,
593     'paid'     => $self->owed,
594     '_date'    => '',
595     'payby'    => 'COMP',
596     'payinfo'  => $self->cust_main->payinfo,
597     'paybatch' => '',
598   } );
599   $cust_pay->insert;
600 }
601
602 =item realtime_card
603
604 Attempts to pay this invoice with a credit card payment via a
605 Business::OnlinePayment realtime gateway.  See
606 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
607 for supported processors.
608
609 =cut
610
611 sub realtime_card {
612   my $self = shift;
613   $self->realtime_bop(
614     'CC',
615     $bop_processor,
616     $bop_login,
617     $bop_password,
618     $bop_action,
619     \@bop_options,
620     @_
621   );
622 }
623
624 =item realtime_ach
625
626 Attempts to pay this invoice with an electronic check (ACH) payment via a
627 Business::OnlinePayment realtime gateway.  See
628 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
629 for supported processors.
630
631 =cut
632
633 sub realtime_ach {
634   my $self = shift;
635   $self->realtime_bop(
636     'ECHECK',
637     $ach_processor,
638     $ach_login,
639     $ach_password,
640     $ach_action,
641     \@ach_options,
642     @_
643   );
644 }
645
646 =item realtime_lec
647
648 Attempts to pay this invoice with phone bill (LEC) payment via a
649 Business::OnlinePayment realtime gateway.  See
650 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
651 for supported processors.
652
653 =cut
654
655 sub realtime_lec {
656   my $self = shift;
657   $self->realtime_bop(
658     'LEC',
659     $bop_processor,
660     $bop_login,
661     $bop_password,
662     $bop_action,
663     \@bop_options,
664     @_
665   );
666 }
667
668 sub realtime_bop {
669   my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
670   my $cust_main = $self->cust_main;
671   my $amount = $self->owed;
672
673   my $address = $cust_main->address1;
674   $address .= ", ". $cust_main->address2 if $cust_main->address2;
675
676   my($payname, $payfirst, $paylast);
677   if ( $cust_main->payname && $method ne 'ECHECK' ) {
678     $payname = $cust_main->payname;
679     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
680       or do {
681               #$dbh->rollback if $oldAutoCommit;
682               return "Illegal payname $payname";
683             };
684     ($payfirst, $paylast) = ($1, $2);
685   } else {
686     $payfirst = $cust_main->getfield('first');
687     $paylast = $cust_main->getfield('last');
688     $payname =  "$payfirst $paylast";
689   }
690
691   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
692   if ( $conf->exists('emailinvoiceauto')
693        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
694     push @invoicing_list, $cust_main->all_emails;
695   }
696   my $email = $invoicing_list[0];
697
698   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
699
700   my $description = 'Internet Services';
701   if ( $conf->exists('business-onlinepayment-description') ) {
702     my $dtempl = $conf->config('business-onlinepayment-description');
703
704     my $agent_obj = $cust_main->agent
705       or die "can't retreive agent for $cust_main (agentnum ".
706              $cust_main->agentnum. ")";
707     my $agent = $agent_obj->agent;
708     my $pkgs = join(', ',
709       map { $_->cust_pkg->part_pkg->pkg }
710         grep { $_->pkgnum } $self->cust_bill_pkg
711     );
712     $description = eval qq("$dtempl");
713
714   }
715
716   my %content;
717   if ( $method eq 'CC' ) { 
718     $content{card_number} = $cust_main->payinfo;
719     $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
720     $content{expiration} = "$2/$1";
721   } elsif ( $method eq 'ECHECK' ) {
722     my($account_number,$routing_code) = $cust_main->payinfo;
723     ( $content{account_number}, $content{routing_code} ) =
724       split('@', $cust_main->payinfo);
725     $content{bank_name} = $cust_main->payname;
726   } elsif ( $method eq 'LEC' ) {
727     $content{phone} = $cust_main->payinfo;
728   }
729   
730   my $transaction =
731     new Business::OnlinePayment( $processor, @$options );
732   $transaction->content(
733     'type'           => $method,
734     'login'          => $login,
735     'password'       => $password,
736     'action'         => $action1,
737     'description'    => $description,
738     'amount'         => $amount,
739     'invoice_number' => $self->invnum,
740     'customer_id'    => $self->custnum,
741     'last_name'      => $paylast,
742     'first_name'     => $payfirst,
743     'name'           => $payname,
744     'address'        => $address,
745     'city'           => $cust_main->city,
746     'state'          => $cust_main->state,
747     'zip'            => $cust_main->zip,
748     'country'        => $cust_main->country,
749     'referer'        => 'http://cleanwhisker.420.am/',
750     'email'          => $email,
751     'phone'          => $cust_main->daytime || $cust_main->night,
752     %content, #after
753   );
754   $transaction->submit();
755
756   if ( $transaction->is_success() && $action2 ) {
757     my $auth = $transaction->authorization;
758     my $ordernum = $transaction->can('order_number')
759                    ? $transaction->order_number
760                    : '';
761
762     #warn "********* $auth ***********\n";
763     #warn "********* $ordernum ***********\n";
764     my $capture =
765       new Business::OnlinePayment( $processor, @$options );
766
767     my %capture = (
768       %content,
769       type           => $method,
770       action         => $action2,
771       login          => $login,
772       password       => $password,
773       order_number   => $ordernum,
774       amount         => $amount,
775       authorization  => $auth,
776       description    => $description,
777     );
778
779     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
780                            transaction_sequence_num local_transaction_date    
781                            local_transaction_time AVS_result_code          )) {
782       $capture{$field} = $transaction->$field() if $transaction->can($field);
783     }
784
785     $capture->content( %capture );
786
787     $capture->submit();
788
789     unless ( $capture->is_success ) {
790       my $e = "Authorization sucessful but capture failed, invnum #".
791               $self->invnum. ': '.  $capture->result_code.
792               ": ". $capture->error_message;
793       warn $e;
794       return $e;
795     }
796
797   }
798
799   if ( $transaction->is_success() ) {
800
801     my %method2payby = (
802       'CC'     => 'CARD',
803       'ECHECK' => 'CHEK',
804       'LEC'    => 'LECB',
805     );
806
807     my $cust_pay = new FS::cust_pay ( {
808        'invnum'   => $self->invnum,
809        'paid'     => $amount,
810        '_date'     => '',
811        'payby'    => method2payby{$method},
812        'payinfo'  => $cust_main->payinfo,
813        'paybatch' => "$processor:". $transaction->authorization,
814     } );
815     my $error = $cust_pay->insert;
816     if ( $error ) {
817       # gah, even with transactions.
818       my $e = 'WARNING: Card/ACH debited but database not updated - '.
819               'error applying payment, invnum #' . $self->invnum.
820               " ($processor): $error";
821       warn $e;
822       return $e;
823     } else {
824       return '';
825     }
826   #} elsif ( $options{'report_badcard'} ) {
827   } else {
828
829     my $perror = "$processor error, invnum #". $self->invnum. ': '.
830                  $transaction->result_code. ": ". $transaction->error_message;
831
832     if ( !$quiet && $conf->exists('emaildecline')
833          && grep { $_ ne 'POST' } $cust_main->invoicing_list
834     ) {
835       my @templ = $conf->config('declinetemplate');
836       my $template = new Text::Template (
837         TYPE   => 'ARRAY',
838         SOURCE => [ map "$_\n", @templ ],
839       ) or return "($perror) can't create template: $Text::Template::ERROR";
840       $template->compile()
841         or return "($perror) can't compile template: $Text::Template::ERROR";
842
843       my $templ_hash = { error => $transaction->error_message };
844
845       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
846       $ENV{MAILADDRESS} = $invoice_from;
847       my $header = new Mail::Header ( [
848         "From: $invoice_from",
849         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
850         "Sender: $invoice_from",
851         "Reply-To: $invoice_from",
852         "Date: ". time2str("%a, %d %b %Y %X %z", time),
853         "Subject: Your payment could not be processed",
854       ] );
855       my $message = new Mail::Internet (
856         'Header' => $header,
857         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
858       );
859       $!=0;
860       $message->smtpsend( Host => $smtpmachine )
861         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
862           or return "($perror) (customer # ". $self->custnum.
863             ") can't send card decline email to ".
864             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
865             " via server $smtpmachine with SMTP: $!";
866     }
867   
868     return $perror;
869   }
870
871 }
872
873 =item batch_card
874
875 Adds a payment for this invoice to the pending credit card batch (see
876 L<FS::cust_pay_batch>).
877
878 =cut
879
880 sub batch_card {
881   my $self = shift;
882   my $cust_main = $self->cust_main;
883
884   my $cust_pay_batch = new FS::cust_pay_batch ( {
885     'invnum'   => $self->getfield('invnum'),
886     'custnum'  => $cust_main->getfield('custnum'),
887     'last'     => $cust_main->getfield('last'),
888     'first'    => $cust_main->getfield('first'),
889     'address1' => $cust_main->getfield('address1'),
890     'address2' => $cust_main->getfield('address2'),
891     'city'     => $cust_main->getfield('city'),
892     'state'    => $cust_main->getfield('state'),
893     'zip'      => $cust_main->getfield('zip'),
894     'country'  => $cust_main->getfield('country'),
895     'trancode' => 77,
896     'cardnum'  => $cust_main->getfield('payinfo'),
897     'exp'      => $cust_main->getfield('paydate'),
898     'payname'  => $cust_main->getfield('payname'),
899     'amount'   => $self->owed,
900   } );
901   my $error = $cust_pay_batch->insert;
902   die $error if $error;
903
904   '';
905 }
906
907 =item print_text [TIME];
908
909 Returns an text invoice, as a list of lines.
910
911 TIME an optional value used to control the printing of overdue messages.  The
912 default is now.  It isn't the date of the invoice; that's the `_date' field.
913 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
914 L<Time::Local> and L<Date::Parse> for conversion functions.
915
916 =cut
917
918 sub print_text {
919
920   my( $self, $today, $template ) = @_;
921   $today ||= time;
922 #  my $invnum = $self->invnum;
923   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
924   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
925     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
926
927   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
928 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
929   #my $balance_due = $self->owed + $pr_total - $cr_total;
930   my $balance_due = $self->owed + $pr_total;
931
932   #my @collect = ();
933   #my($description,$amount);
934   @buf = ();
935
936   #previous balance
937   foreach ( @pr_cust_bill ) {
938     push @buf, [
939       "Previous Balance, Invoice #". $_->invnum. 
940                  " (". time2str("%x",$_->_date). ")",
941       $money_char. sprintf("%10.2f",$_->owed)
942     ];
943   }
944   if (@pr_cust_bill) {
945     push @buf,['','-----------'];
946     push @buf,[ 'Total Previous Balance',
947                 $money_char. sprintf("%10.2f",$pr_total ) ];
948     push @buf,['',''];
949   }
950
951   #new charges
952   foreach ( ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
953             ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
954   ) {
955
956     if ( $_->pkgnum ) {
957
958       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
959       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
960       my($pkg)=$part_pkg->pkg;
961
962       if ( $_->setup != 0 ) {
963         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
964         push @buf,
965           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
966       }
967
968       if ( $_->recur != 0 ) {
969         push @buf, [
970           "$pkg (" . time2str("%x",$_->sdate) . " - " .
971                                 time2str("%x",$_->edate) . ")",
972           $money_char. sprintf("%10.2f",$_->recur)
973         ];
974         push @buf,
975           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
976       }
977
978     } else { #pkgnum tax
979       my $itemdesc = defined $_->dbdef_table->column('itemdesc')
980                      ? ( $_->itemdesc || 'Tax' )
981                      : 'Tax';
982       push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ] 
983         if $_->setup != 0;
984     }
985   }
986
987   push @buf,['','-----------'];
988   push @buf,['Total New Charges',
989              $money_char. sprintf("%10.2f",$self->charged) ];
990   push @buf,['',''];
991
992   push @buf,['','-----------'];
993   push @buf,['Total Charges',
994              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
995   push @buf,['',''];
996
997   #credits
998   foreach ( $self->cust_credited ) {
999
1000     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1001
1002     my $reason = substr($_->cust_credit->reason,0,32);
1003     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1004     $reason = " ($reason) " if $reason;
1005     push @buf,[
1006       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1007         $reason,
1008       $money_char. sprintf("%10.2f",$_->amount)
1009     ];
1010   }
1011   #foreach ( @cr_cust_credit ) {
1012   #  push @buf,[
1013   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1014   #    $money_char. sprintf("%10.2f",$_->credited)
1015   #  ];
1016   #}
1017
1018   #get & print payments
1019   foreach ( $self->cust_bill_pay ) {
1020
1021     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1022
1023     push @buf,[
1024       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1025       $money_char. sprintf("%10.2f",$_->amount )
1026     ];
1027   }
1028
1029   #balance due
1030   push @buf,['','-----------'];
1031   push @buf,['Balance Due', $money_char. 
1032     sprintf("%10.2f", $balance_due ) ];
1033
1034   #create the template
1035   my $templatefile = 'invoice_template';
1036   $templatefile .= "_$template" if $template;
1037   my @invoice_template = $conf->config($templatefile)
1038   or die "cannot load config file $templatefile";
1039   $invoice_lines = 0;
1040   my $wasfunc = 0;
1041   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1042     /invoice_lines\((\d*)\)/;
1043     $invoice_lines += $1 || scalar(@buf);
1044     $wasfunc=1;
1045   }
1046   die "no invoice_lines() functions in template?" unless $wasfunc;
1047   my $invoice_template = new Text::Template (
1048     TYPE   => 'ARRAY',
1049     SOURCE => [ map "$_\n", @invoice_template ],
1050   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1051   $invoice_template->compile()
1052     or die "can't compile template: $Text::Template::ERROR";
1053
1054   #setup template variables
1055   package FS::cust_bill::_template; #!
1056   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1057
1058   $invnum = $self->invnum;
1059   $date = $self->_date;
1060   $page = 1;
1061   $agent = $self->cust_main->agent->agent;
1062
1063   if ( $FS::cust_bill::invoice_lines ) {
1064     $total_pages =
1065       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1066     $total_pages++
1067       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1068   } else {
1069     $total_pages = 1;
1070   }
1071
1072   #format address (variable for the template)
1073   my $l = 0;
1074   @address = ( '', '', '', '', '', '' );
1075   package FS::cust_bill; #!
1076   $FS::cust_bill::_template::address[$l++] =
1077     $cust_main->payname.
1078       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1079         ? " (P.O. #". $cust_main->payinfo. ")"
1080         : ''
1081       )
1082   ;
1083   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1084     if $cust_main->company;
1085   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1086   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1087     if $cust_main->address2;
1088   $FS::cust_bill::_template::address[$l++] =
1089     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1090   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1091     unless $cust_main->country eq 'US';
1092
1093         #  #overdue? (variable for the template)
1094         #  $FS::cust_bill::_template::overdue = ( 
1095         #    $balance_due > 0
1096         #    && $today > $self->_date 
1097         ##    && $self->printed > 1
1098         #    && $self->printed > 0
1099         #  );
1100
1101   #and subroutine for the template
1102   sub FS::cust_bill::_template::invoice_lines {
1103     my $lines = shift || scalar(@buf);
1104     map { 
1105       scalar(@buf) ? shift @buf : [ '', '' ];
1106     }
1107     ( 1 .. $lines );
1108   }
1109
1110   #and fill it in
1111   $FS::cust_bill::_template::page = 1;
1112   my $lines;
1113   my @collect;
1114   while (@buf) {
1115     push @collect, split("\n",
1116       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1117     );
1118     $FS::cust_bill::_template::page++;
1119   }
1120
1121   map "$_\n", @collect;
1122
1123 }
1124
1125 =back
1126
1127 =head1 VERSION
1128
1129 $Id: cust_bill.pm,v 1.60 2002-12-28 09:16:49 ivan Exp $
1130
1131 =head1 BUGS
1132
1133 The delete method.
1134
1135 print_text formatting (and some logic :/) is in source, but needs to be
1136 slurped in from a file.  Also number of lines ($=).
1137
1138 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1139 or something similar so the look can be completely customized?)
1140
1141 =head1 SEE ALSO
1142
1143 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1144 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1145 documentation.
1146
1147 =cut
1148
1149 1;
1150