pop off an extra blank line in business-onlinepayment options
[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
671   #trim an extraneous blank line
672   pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
673
674   my $cust_main = $self->cust_main;
675   my $amount = $self->owed;
676
677   my $address = $cust_main->address1;
678   $address .= ", ". $cust_main->address2 if $cust_main->address2;
679
680   my($payname, $payfirst, $paylast);
681   if ( $cust_main->payname && $method ne 'ECHECK' ) {
682     $payname = $cust_main->payname;
683     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
684       or do {
685               #$dbh->rollback if $oldAutoCommit;
686               return "Illegal payname $payname";
687             };
688     ($payfirst, $paylast) = ($1, $2);
689   } else {
690     $payfirst = $cust_main->getfield('first');
691     $paylast = $cust_main->getfield('last');
692     $payname =  "$payfirst $paylast";
693   }
694
695   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
696   if ( $conf->exists('emailinvoiceauto')
697        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
698     push @invoicing_list, $cust_main->all_emails;
699   }
700   my $email = $invoicing_list[0];
701
702   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
703
704   my $description = 'Internet Services';
705   if ( $conf->exists('business-onlinepayment-description') ) {
706     my $dtempl = $conf->config('business-onlinepayment-description');
707
708     my $agent_obj = $cust_main->agent
709       or die "can't retreive agent for $cust_main (agentnum ".
710              $cust_main->agentnum. ")";
711     my $agent = $agent_obj->agent;
712     my $pkgs = join(', ',
713       map { $_->cust_pkg->part_pkg->pkg }
714         grep { $_->pkgnum } $self->cust_bill_pkg
715     );
716     $description = eval qq("$dtempl");
717
718   }
719
720   my %content;
721   if ( $method eq 'CC' ) { 
722     $content{card_number} = $cust_main->payinfo;
723     $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
724     $content{expiration} = "$2/$1";
725   } elsif ( $method eq 'ECHECK' ) {
726     my($account_number,$routing_code) = $cust_main->payinfo;
727     ( $content{account_number}, $content{routing_code} ) =
728       split('@', $cust_main->payinfo);
729     $content{bank_name} = $cust_main->payname;
730   } elsif ( $method eq 'LEC' ) {
731     $content{phone} = $cust_main->payinfo;
732   }
733   
734   my $transaction =
735     new Business::OnlinePayment( $processor, @$options );
736   $transaction->content(
737     'type'           => $method,
738     'login'          => $login,
739     'password'       => $password,
740     'action'         => $action1,
741     'description'    => $description,
742     'amount'         => $amount,
743     'invoice_number' => $self->invnum,
744     'customer_id'    => $self->custnum,
745     'last_name'      => $paylast,
746     'first_name'     => $payfirst,
747     'name'           => $payname,
748     'address'        => $address,
749     'city'           => $cust_main->city,
750     'state'          => $cust_main->state,
751     'zip'            => $cust_main->zip,
752     'country'        => $cust_main->country,
753     'referer'        => 'http://cleanwhisker.420.am/',
754     'email'          => $email,
755     'phone'          => $cust_main->daytime || $cust_main->night,
756     %content, #after
757   );
758   $transaction->submit();
759
760   if ( $transaction->is_success() && $action2 ) {
761     my $auth = $transaction->authorization;
762     my $ordernum = $transaction->can('order_number')
763                    ? $transaction->order_number
764                    : '';
765
766     #warn "********* $auth ***********\n";
767     #warn "********* $ordernum ***********\n";
768     my $capture =
769       new Business::OnlinePayment( $processor, @$options );
770
771     my %capture = (
772       %content,
773       type           => $method,
774       action         => $action2,
775       login          => $login,
776       password       => $password,
777       order_number   => $ordernum,
778       amount         => $amount,
779       authorization  => $auth,
780       description    => $description,
781     );
782
783     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
784                            transaction_sequence_num local_transaction_date    
785                            local_transaction_time AVS_result_code          )) {
786       $capture{$field} = $transaction->$field() if $transaction->can($field);
787     }
788
789     $capture->content( %capture );
790
791     $capture->submit();
792
793     unless ( $capture->is_success ) {
794       my $e = "Authorization sucessful but capture failed, invnum #".
795               $self->invnum. ': '.  $capture->result_code.
796               ": ". $capture->error_message;
797       warn $e;
798       return $e;
799     }
800
801   }
802
803   if ( $transaction->is_success() ) {
804
805     my %method2payby = (
806       'CC'     => 'CARD',
807       'ECHECK' => 'CHEK',
808       'LEC'    => 'LECB',
809     );
810
811     my $cust_pay = new FS::cust_pay ( {
812        'invnum'   => $self->invnum,
813        'paid'     => $amount,
814        '_date'     => '',
815        'payby'    => method2payby{$method},
816        'payinfo'  => $cust_main->payinfo,
817        'paybatch' => "$processor:". $transaction->authorization,
818     } );
819     my $error = $cust_pay->insert;
820     if ( $error ) {
821       # gah, even with transactions.
822       my $e = 'WARNING: Card/ACH debited but database not updated - '.
823               'error applying payment, invnum #' . $self->invnum.
824               " ($processor): $error";
825       warn $e;
826       return $e;
827     } else {
828       return '';
829     }
830   #} elsif ( $options{'report_badcard'} ) {
831   } else {
832
833     my $perror = "$processor error, invnum #". $self->invnum. ': '.
834                  $transaction->result_code. ": ". $transaction->error_message;
835
836     if ( !$quiet && $conf->exists('emaildecline')
837          && grep { $_ ne 'POST' } $cust_main->invoicing_list
838     ) {
839       my @templ = $conf->config('declinetemplate');
840       my $template = new Text::Template (
841         TYPE   => 'ARRAY',
842         SOURCE => [ map "$_\n", @templ ],
843       ) or return "($perror) can't create template: $Text::Template::ERROR";
844       $template->compile()
845         or return "($perror) can't compile template: $Text::Template::ERROR";
846
847       my $templ_hash = { error => $transaction->error_message };
848
849       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
850       $ENV{MAILADDRESS} = $invoice_from;
851       my $header = new Mail::Header ( [
852         "From: $invoice_from",
853         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
854         "Sender: $invoice_from",
855         "Reply-To: $invoice_from",
856         "Date: ". time2str("%a, %d %b %Y %X %z", time),
857         "Subject: Your payment could not be processed",
858       ] );
859       my $message = new Mail::Internet (
860         'Header' => $header,
861         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
862       );
863       $!=0;
864       $message->smtpsend( Host => $smtpmachine )
865         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
866           or return "($perror) (customer # ". $self->custnum.
867             ") can't send card decline email to ".
868             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
869             " via server $smtpmachine with SMTP: $!";
870     }
871   
872     return $perror;
873   }
874
875 }
876
877 =item batch_card
878
879 Adds a payment for this invoice to the pending credit card batch (see
880 L<FS::cust_pay_batch>).
881
882 =cut
883
884 sub batch_card {
885   my $self = shift;
886   my $cust_main = $self->cust_main;
887
888   my $cust_pay_batch = new FS::cust_pay_batch ( {
889     'invnum'   => $self->getfield('invnum'),
890     'custnum'  => $cust_main->getfield('custnum'),
891     'last'     => $cust_main->getfield('last'),
892     'first'    => $cust_main->getfield('first'),
893     'address1' => $cust_main->getfield('address1'),
894     'address2' => $cust_main->getfield('address2'),
895     'city'     => $cust_main->getfield('city'),
896     'state'    => $cust_main->getfield('state'),
897     'zip'      => $cust_main->getfield('zip'),
898     'country'  => $cust_main->getfield('country'),
899     'trancode' => 77,
900     'cardnum'  => $cust_main->getfield('payinfo'),
901     'exp'      => $cust_main->getfield('paydate'),
902     'payname'  => $cust_main->getfield('payname'),
903     'amount'   => $self->owed,
904   } );
905   my $error = $cust_pay_batch->insert;
906   die $error if $error;
907
908   '';
909 }
910
911 =item print_text [TIME];
912
913 Returns an text invoice, as a list of lines.
914
915 TIME an optional value used to control the printing of overdue messages.  The
916 default is now.  It isn't the date of the invoice; that's the `_date' field.
917 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
918 L<Time::Local> and L<Date::Parse> for conversion functions.
919
920 =cut
921
922 sub print_text {
923
924   my( $self, $today, $template ) = @_;
925   $today ||= time;
926 #  my $invnum = $self->invnum;
927   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
928   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
929     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
930
931   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
932 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
933   #my $balance_due = $self->owed + $pr_total - $cr_total;
934   my $balance_due = $self->owed + $pr_total;
935
936   #my @collect = ();
937   #my($description,$amount);
938   @buf = ();
939
940   #previous balance
941   foreach ( @pr_cust_bill ) {
942     push @buf, [
943       "Previous Balance, Invoice #". $_->invnum. 
944                  " (". time2str("%x",$_->_date). ")",
945       $money_char. sprintf("%10.2f",$_->owed)
946     ];
947   }
948   if (@pr_cust_bill) {
949     push @buf,['','-----------'];
950     push @buf,[ 'Total Previous Balance',
951                 $money_char. sprintf("%10.2f",$pr_total ) ];
952     push @buf,['',''];
953   }
954
955   #new charges
956   foreach ( ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
957             ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
958   ) {
959
960     if ( $_->pkgnum ) {
961
962       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
963       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
964       my($pkg)=$part_pkg->pkg;
965
966       if ( $_->setup != 0 ) {
967         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
968         push @buf,
969           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
970       }
971
972       if ( $_->recur != 0 ) {
973         push @buf, [
974           "$pkg (" . time2str("%x",$_->sdate) . " - " .
975                                 time2str("%x",$_->edate) . ")",
976           $money_char. sprintf("%10.2f",$_->recur)
977         ];
978         push @buf,
979           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
980       }
981
982     } else { #pkgnum tax
983       my $itemdesc = defined $_->dbdef_table->column('itemdesc')
984                      ? ( $_->itemdesc || 'Tax' )
985                      : 'Tax';
986       push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ] 
987         if $_->setup != 0;
988     }
989   }
990
991   push @buf,['','-----------'];
992   push @buf,['Total New Charges',
993              $money_char. sprintf("%10.2f",$self->charged) ];
994   push @buf,['',''];
995
996   push @buf,['','-----------'];
997   push @buf,['Total Charges',
998              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
999   push @buf,['',''];
1000
1001   #credits
1002   foreach ( $self->cust_credited ) {
1003
1004     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1005
1006     my $reason = substr($_->cust_credit->reason,0,32);
1007     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1008     $reason = " ($reason) " if $reason;
1009     push @buf,[
1010       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1011         $reason,
1012       $money_char. sprintf("%10.2f",$_->amount)
1013     ];
1014   }
1015   #foreach ( @cr_cust_credit ) {
1016   #  push @buf,[
1017   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1018   #    $money_char. sprintf("%10.2f",$_->credited)
1019   #  ];
1020   #}
1021
1022   #get & print payments
1023   foreach ( $self->cust_bill_pay ) {
1024
1025     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1026
1027     push @buf,[
1028       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1029       $money_char. sprintf("%10.2f",$_->amount )
1030     ];
1031   }
1032
1033   #balance due
1034   push @buf,['','-----------'];
1035   push @buf,['Balance Due', $money_char. 
1036     sprintf("%10.2f", $balance_due ) ];
1037
1038   #create the template
1039   my $templatefile = 'invoice_template';
1040   $templatefile .= "_$template" if $template;
1041   my @invoice_template = $conf->config($templatefile)
1042   or die "cannot load config file $templatefile";
1043   $invoice_lines = 0;
1044   my $wasfunc = 0;
1045   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1046     /invoice_lines\((\d*)\)/;
1047     $invoice_lines += $1 || scalar(@buf);
1048     $wasfunc=1;
1049   }
1050   die "no invoice_lines() functions in template?" unless $wasfunc;
1051   my $invoice_template = new Text::Template (
1052     TYPE   => 'ARRAY',
1053     SOURCE => [ map "$_\n", @invoice_template ],
1054   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1055   $invoice_template->compile()
1056     or die "can't compile template: $Text::Template::ERROR";
1057
1058   #setup template variables
1059   package FS::cust_bill::_template; #!
1060   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1061
1062   $invnum = $self->invnum;
1063   $date = $self->_date;
1064   $page = 1;
1065   $agent = $self->cust_main->agent->agent;
1066
1067   if ( $FS::cust_bill::invoice_lines ) {
1068     $total_pages =
1069       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1070     $total_pages++
1071       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1072   } else {
1073     $total_pages = 1;
1074   }
1075
1076   #format address (variable for the template)
1077   my $l = 0;
1078   @address = ( '', '', '', '', '', '' );
1079   package FS::cust_bill; #!
1080   $FS::cust_bill::_template::address[$l++] =
1081     $cust_main->payname.
1082       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1083         ? " (P.O. #". $cust_main->payinfo. ")"
1084         : ''
1085       )
1086   ;
1087   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1088     if $cust_main->company;
1089   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1090   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1091     if $cust_main->address2;
1092   $FS::cust_bill::_template::address[$l++] =
1093     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1094   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1095     unless $cust_main->country eq 'US';
1096
1097         #  #overdue? (variable for the template)
1098         #  $FS::cust_bill::_template::overdue = ( 
1099         #    $balance_due > 0
1100         #    && $today > $self->_date 
1101         ##    && $self->printed > 1
1102         #    && $self->printed > 0
1103         #  );
1104
1105   #and subroutine for the template
1106   sub FS::cust_bill::_template::invoice_lines {
1107     my $lines = shift || scalar(@buf);
1108     map { 
1109       scalar(@buf) ? shift @buf : [ '', '' ];
1110     }
1111     ( 1 .. $lines );
1112   }
1113
1114   #and fill it in
1115   $FS::cust_bill::_template::page = 1;
1116   my $lines;
1117   my @collect;
1118   while (@buf) {
1119     push @collect, split("\n",
1120       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1121     );
1122     $FS::cust_bill::_template::page++;
1123   }
1124
1125   map "$_\n", @collect;
1126
1127 }
1128
1129 =back
1130
1131 =head1 VERSION
1132
1133 $Id: cust_bill.pm,v 1.61 2003-01-10 07:41:05 ivan Exp $
1134
1135 =head1 BUGS
1136
1137 The delete method.
1138
1139 print_text formatting (and some logic :/) is in source, but needs to be
1140 slurped in from a file.  Also number of lines ($=).
1141
1142 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1143 or something similar so the look can be completely customized?)
1144
1145 =head1 SEE ALSO
1146
1147 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1148 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1149 documentation.
1150
1151 =cut
1152
1153 1;
1154