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