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