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