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