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