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