bug fix in new ACH code
[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 or !@invoicing_list ) { #email
377
378     #better to notify this person than silence
379     @invoicing_list = ($invoice_from) unless @invoicing_list;
380
381     #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
382     #$ENV{SMTPHOSTS} = $smtpmachine;
383     $ENV{MAILADDRESS} = $invoice_from;
384     my $header = new Mail::Header ( [
385       "From: $invoice_from",
386       "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
387       "Sender: $invoice_from",
388       "Reply-To: $invoice_from",
389       "Date: ". time2str("%a, %d %b %Y %X %z", time),
390       "Subject: Invoice",
391     ] );
392     my $message = new Mail::Internet (
393       'Header' => $header,
394       'Body' => [ @print_text ], #( date)
395     );
396     $!=0;
397     $message->smtpsend( Host => $smtpmachine )
398       or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
399         or return "(customer # ". $self->custnum. ") can't send invoice email".
400                   " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
401                   " via server $smtpmachine with SMTP: $!";
402
403   }
404
405   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
406     open(LPR, "|$lpr")
407       or return "Can't open pipe to $lpr: $!";
408     print LPR @print_text;
409     close LPR
410       or return $! ? "Error closing $lpr: $!"
411                    : "Exit status $? from $lpr";
412   }
413
414   '';
415
416 }
417
418 =item send_csv OPTIONS
419
420 Sends invoice as a CSV data-file to a remote host with the specified protocol.
421
422 Options are:
423
424 protocol - currently only "ftp"
425 server
426 username
427 password
428 dir
429
430 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
431 and YYMMDDHHMMSS is a timestamp.
432
433 The fields of the CSV file is as follows:
434
435 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
436
437 =over 4
438
439 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
440
441 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
442 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
443 fields are filled in.
444
445 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
446 first two fields (B<record_type> and B<invnum>) and the last five fields
447 (B<pkg> through B<edate>) are filled in.
448
449 =item invnum - invoice number
450
451 =item custnum - customer number
452
453 =item _date - invoice date
454
455 =item charged - total invoice amount
456
457 =item first - customer first name
458
459 =item last - customer first name
460
461 =item company - company name
462
463 =item address1 - address line 1
464
465 =item address2 - address line 1
466
467 =item city
468
469 =item state
470
471 =item zip
472
473 =item country
474
475 =item pkg - line item description
476
477 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
478
479 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
480
481 =item sdate - start date for recurring fee
482
483 =item edate - end date for recurring fee
484
485 =back
486
487 =cut
488
489 sub send_csv {
490   my($self, %opt) = @_;
491
492   #part one: create file
493
494   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
495   mkdir $spooldir, 0700 unless -d $spooldir;
496
497   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
498
499   open(CSV, ">$file") or die "can't open $file: $!";
500
501   eval "use Text::CSV_XS";
502   die $@ if $@;
503
504   my $csv = Text::CSV_XS->new({'always_quote'=>1});
505
506   my $cust_main = $self->cust_main;
507
508   $csv->combine(
509     'cust_bill',
510     $self->invnum,
511     $self->custnum,
512     time2str("%x", $self->_date),
513     sprintf("%.2f", $self->charged),
514     ( map { $cust_main->getfield($_) }
515         qw( first last company address1 address2 city state zip country ) ),
516     map { '' } (1..5),
517   ) or die "can't create csv";
518   print CSV $csv->string. "\n";
519
520   #new charges (false laziness w/print_text)
521   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
522
523     my($pkg, $setup, $recur, $sdate, $edate);
524     if ( $cust_bill_pkg->pkgnum ) {
525     
526       ($pkg, $setup, $recur, $sdate, $edate) = (
527         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
528         ( $cust_bill_pkg->setup != 0
529           ? sprintf("%.2f", $cust_bill_pkg->setup )
530           : '' ),
531         ( $cust_bill_pkg->recur != 0
532           ? sprintf("%.2f", $cust_bill_pkg->recur )
533           : '' ),
534         time2str("%x", $cust_bill_pkg->sdate),
535         time2str("%x", $cust_bill_pkg->edate),
536       );
537
538     } else { #pkgnum Tax
539       next unless $cust_bill_pkg->setup != 0;
540       ($pkg, $setup, $recur, $sdate, $edate) =
541         ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
542     }
543
544     $csv->combine(
545       'cust_bill_pkg',
546       $self->invnum,
547       ( map { '' } (1..11) ),
548       ($pkg, $setup, $recur, $sdate, $edate)
549     ) or die "can't create csv";
550     print CSV $csv->string. "\n";
551
552   }
553
554   close CSV or die "can't close CSV: $!";
555
556   #part two: upload it
557
558   my $net;
559   if ( $opt{protocol} eq 'ftp' ) {
560     eval "use Net::FTP;";
561     die $@ if $@;
562     $net = Net::FTP->new($opt{server}) or die @$;
563   } else {
564     die "unknown protocol: $opt{protocol}";
565   }
566
567   $net->login( $opt{username}, $opt{password} )
568     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
569
570   $net->binary or die "can't set binary mode";
571
572   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
573
574   $net->put($file) or die "can't put $file: $!";
575
576   $net->quit;
577
578   unlink $file;
579
580 }
581
582 =item comp
583
584 Pays this invoice with a compliemntary payment.  If there is an error,
585 returns the error, otherwise returns false.
586
587 =cut
588
589 sub comp {
590   my $self = shift;
591   my $cust_pay = new FS::cust_pay ( {
592     'invnum'   => $self->invnum,
593     'paid'     => $self->owed,
594     '_date'    => '',
595     'payby'    => 'COMP',
596     'payinfo'  => $self->cust_main->payinfo,
597     'paybatch' => '',
598   } );
599   $cust_pay->insert;
600 }
601
602 =item realtime_card
603
604 Attempts to pay this invoice with a credit card payment via a
605 Business::OnlinePayment realtime gateway.  See
606 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
607 for supported processors.
608
609 =cut
610
611 sub realtime_card {
612   my $self = shift;
613   $self->realtime_bop('CC', @_);
614 }
615
616 =item realtime_ach
617
618 Attempts to pay this invoice with an electronic check (ACH) payment via a
619 Business::OnlinePayment realtime gateway.  See
620 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
621 for supported processors.
622
623 =cut
624
625 sub realtime_ach {
626   my $self = shift;
627   $self->realtime_bop('ECHECK', @_);
628 }
629
630 sub realtime_bop {
631   my $self = shift;
632   my $method = shift;
633   my $cust_main = $self->cust_main;
634   my $amount = $self->owed;
635
636   unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
637     return "Real-time card/ACH processing not enabled (processor $processor)";
638   }
639   my $bop_processor = $1; #hmm?
640
641   my $address = $cust_main->address1;
642   $address .= ", ". $cust_main->address2 if $cust_main->address2;
643
644   my($payname, $payfirst, $paylast);
645   if ( $cust_main->payname && $method ne 'ECHECK' ) {
646     $payname = $cust_main->payname;
647     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
648       or do {
649               #$dbh->rollback if $oldAutoCommit;
650               return "Illegal payname $payname";
651             };
652     ($payfirst, $paylast) = ($1, $2);
653   } else {
654     $payfirst = $cust_main->getfield('first');
655     $paylast = $cust_main->getfield('last');
656     $payname =  "$payfirst $paylast";
657   }
658
659   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
660   if ( $conf->exists('emailinvoiceauto')
661        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
662     push @invoicing_list, $cust_main->all_emails;
663   }
664   my $email = $invoicing_list[0];
665
666   my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
667
668   my $description = 'Internet Services';
669   if ( $conf->exists('business-onlinepayment-description') ) {
670     my $dtempl = $conf->config('business-onlinepayment-description');
671
672     my $agent_obj = $cust_main->agent
673       or die "can't retreive agent for $cust_main (agentnum ".
674              $cust_main->agentnum. ")";
675     my $agent = $agent_obj->agent;
676     my $pkgs = join(', ',
677       map { $_->cust_pkg->part_pkg->pkg }
678         grep { $_->pkgnum } $self->cust_bill_pkg
679     );
680     $description = eval qq("$dtempl");
681
682   }
683
684   my %content;
685   if ( $method eq 'CC' ) { 
686     $content{card_number} = $cust_main->payinfo;
687     $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
688     $content{expiration} = "$2/$1";
689   } elsif ( $method eq 'ECHECK' ) {
690     my($account_number,$routing_code) = $cust_main->payinfo;
691     ( $content{account_number}, $content{routing_code} ) =
692       split('@', $cust_main->payinfo);
693     $content{bank_name} = $cust_main->payname;
694   }
695   
696   my $transaction =
697     new Business::OnlinePayment( $bop_processor, @bop_options );
698   $transaction->content(
699     %content,
700     'type'           => $method,
701     'login'          => $bop_login,
702     'password'       => $bop_password,
703     'action'         => $action1,
704     'description'    => $description,
705     'amount'         => $amount,
706     'invoice_number' => $self->invnum,
707     'customer_id'    => $self->custnum,
708     'last_name'      => $paylast,
709     'first_name'     => $payfirst,
710     'name'           => $payname,
711     'address'        => $address,
712     'city'           => $cust_main->city,
713     'state'          => $cust_main->state,
714     'zip'            => $cust_main->zip,
715     'country'        => $cust_main->country,
716     'referer'        => 'http://cleanwhisker.420.am/',
717     'email'          => $email,
718     'phone'          => $cust_main->daytime || $cust_main->night,
719   );
720   $transaction->submit();
721
722   if ( $transaction->is_success() && $action2 ) {
723     my $auth = $transaction->authorization;
724     my $ordernum = $transaction->can('order_number')
725                    ? $transaction->order_number
726                    : '';
727
728     #warn "********* $auth ***********\n";
729     #warn "********* $ordernum ***********\n";
730     my $capture =
731       new Business::OnlinePayment( $bop_processor, @bop_options );
732
733     my %capture = (
734       %content,
735       type           => $method,
736       action         => $action2,
737       login          => $bop_login,
738       password       => $bop_password,
739       order_number   => $ordernum,
740       amount         => $amount,
741       authorization  => $auth,
742       description    => $description,
743     );
744
745     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
746                            transaction_sequence_num local_transaction_date    
747                            local_transaction_time AVS_result_code          )) {
748       $capture{$field} = $transaction->$field() if $transaction->can($field);
749     }
750
751     $capture->content( %capture );
752
753     $capture->submit();
754
755     unless ( $capture->is_success ) {
756       my $e = "Authorization sucessful but capture failed, invnum #".
757               $self->invnum. ': '.  $capture->result_code.
758               ": ". $capture->error_message;
759       warn $e;
760       return $e;
761     }
762
763   }
764
765   if ( $transaction->is_success() ) {
766
767     my %method2payby = (
768       'CC'     => 'CARD',
769       'ECHECK' => 'CHEK',
770     );
771
772     my $cust_pay = new FS::cust_pay ( {
773        'invnum'   => $self->invnum,
774        'paid'     => $amount,
775        '_date'     => '',
776        'payby'    => $method2payby{$method},
777        'payinfo'  => $cust_main->payinfo,
778        'paybatch' => "$processor:". $transaction->authorization,
779     } );
780     my $error = $cust_pay->insert;
781     if ( $error ) {
782       # gah, even with transactions.
783       my $e = 'WARNING: Card/ACH debited but database not updated - '.
784               'error applying payment, invnum #' . $self->invnum.
785               " ($processor): $error";
786       warn $e;
787       return $e;
788     } else {
789       return '';
790     }
791   #} elsif ( $options{'report_badcard'} ) {
792   } else {
793
794     my $perror = "$processor error, invnum #". $self->invnum. ': '.
795                  $transaction->result_code. ": ". $transaction->error_message;
796
797     if ( $conf->exists('emaildecline')
798          && grep { $_ ne 'POST' } $cust_main->invoicing_list
799     ) {
800       my @templ = $conf->config('declinetemplate');
801       my $template = new Text::Template (
802         TYPE   => 'ARRAY',
803         SOURCE => [ map "$_\n", @templ ],
804       ) or return "($perror) can't create template: $Text::Template::ERROR";
805       $template->compile()
806         or return "($perror) can't compile template: $Text::Template::ERROR";
807
808       my $templ_hash = { error => $transaction->error_message };
809
810       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
811       $ENV{MAILADDRESS} = $invoice_from;
812       my $header = new Mail::Header ( [
813         "From: $invoice_from",
814         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
815         "Sender: $invoice_from",
816         "Reply-To: $invoice_from",
817         "Date: ". time2str("%a, %d %b %Y %X %z", time),
818         "Subject: Your payment could not be processed",
819       ] );
820       my $message = new Mail::Internet (
821         'Header' => $header,
822         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
823       );
824       $!=0;
825       $message->smtpsend( Host => $smtpmachine )
826         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
827           or return "($perror) (customer # ". $self->custnum.
828             ") can't send card decline email to ".
829             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
830             " via server $smtpmachine with SMTP: $!";
831     }
832   
833     return $perror;
834   }
835
836 }
837
838 =item realtime_card_cybercash
839
840 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
841
842 =cut
843
844 sub realtime_card_cybercash {
845   my $self = shift;
846   my $cust_main = $self->cust_main;
847   my $amount = $self->owed;
848
849   return "CyberCash CashRegister real-time card processing not enabled!"
850     unless $processor eq 'cybercash3.2';
851
852   my $address = $cust_main->address1;
853   $address .= ", ". $cust_main->address2 if $cust_main->address2;
854
855   #fix exp. date
856   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
857   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
858   my $exp = "$2/$1";
859
860   #
861
862   my $paybatch = $self->invnum. 
863                   '-' . time2str("%y%m%d%H%M%S", time);
864
865   my $payname = $cust_main->payname ||
866                 $cust_main->getfield('first').' '.$cust_main->getfield('last');
867
868   my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
869
870   my @full_xaction = ( $xaction,
871     'Order-ID'     => $paybatch,
872     'Amount'       => "usd $amount",
873     'Card-Number'  => $cust_main->getfield('payinfo'),
874     'Card-Name'    => $payname,
875     'Card-Address' => $address,
876     'Card-City'    => $cust_main->getfield('city'),
877     'Card-State'   => $cust_main->getfield('state'),
878     'Card-Zip'     => $cust_main->getfield('zip'),
879     'Card-Country' => $country,
880     'Card-Exp'     => $exp,
881   );
882
883   my %result;
884   %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
885   
886   if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
887     my $cust_pay = new FS::cust_pay ( {
888        'invnum'   => $self->invnum,
889        'paid'     => $amount,
890        '_date'     => '',
891        'payby'    => 'CARD',
892        'payinfo'  => $cust_main->payinfo,
893        'paybatch' => "$processor:$paybatch",
894     } );
895     my $error = $cust_pay->insert;
896     if ( $error ) {
897       # gah, even with transactions.
898       my $e = 'WARNING: Card debited but database not updated - '.
899               'error applying payment, invnum #' . $self->invnum.
900               " (CyberCash Order-ID $paybatch): $error";
901       warn $e;
902       return $e;
903     } else {
904       return '';
905     }
906 #  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
907 #            || $options{'report_badcard'}
908 #          ) {
909   } else {
910      return 'Cybercash error, invnum #' . 
911        $self->invnum. ':'. $result{'MErrMsg'};
912   }
913
914 }
915
916 =item batch_card
917
918 Adds a payment for this invoice to the pending credit card batch (see
919 L<FS::cust_pay_batch>).
920
921 =cut
922
923 sub batch_card {
924   my $self = shift;
925   my $cust_main = $self->cust_main;
926
927   my $cust_pay_batch = new FS::cust_pay_batch ( {
928     'invnum'   => $self->getfield('invnum'),
929     'custnum'  => $cust_main->getfield('custnum'),
930     'last'     => $cust_main->getfield('last'),
931     'first'    => $cust_main->getfield('first'),
932     'address1' => $cust_main->getfield('address1'),
933     'address2' => $cust_main->getfield('address2'),
934     'city'     => $cust_main->getfield('city'),
935     'state'    => $cust_main->getfield('state'),
936     'zip'      => $cust_main->getfield('zip'),
937     'country'  => $cust_main->getfield('country'),
938     'trancode' => 77,
939     'cardnum'  => $cust_main->getfield('payinfo'),
940     'exp'      => $cust_main->getfield('paydate'),
941     'payname'  => $cust_main->getfield('payname'),
942     'amount'   => $self->owed,
943   } );
944   my $error = $cust_pay_batch->insert;
945   die $error if $error;
946
947   '';
948 }
949
950 =item print_text [TIME];
951
952 Returns an text invoice, as a list of lines.
953
954 TIME an optional value used to control the printing of overdue messages.  The
955 default is now.  It isn't the date of the invoice; that's the `_date' field.
956 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
957 L<Time::Local> and L<Date::Parse> for conversion functions.
958
959 =cut
960
961 sub print_text {
962
963   my( $self, $today, $template ) = @_;
964   $today ||= time;
965 #  my $invnum = $self->invnum;
966   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
967   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
968     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
969
970   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
971 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
972   #my $balance_due = $self->owed + $pr_total - $cr_total;
973   my $balance_due = $self->owed + $pr_total;
974
975   #my @collect = ();
976   #my($description,$amount);
977   @buf = ();
978
979   #previous balance
980   foreach ( @pr_cust_bill ) {
981     push @buf, [
982       "Previous Balance, Invoice #". $_->invnum. 
983                  " (". time2str("%x",$_->_date). ")",
984       $money_char. sprintf("%10.2f",$_->owed)
985     ];
986   }
987   if (@pr_cust_bill) {
988     push @buf,['','-----------'];
989     push @buf,[ 'Total Previous Balance',
990                 $money_char. sprintf("%10.2f",$pr_total ) ];
991     push @buf,['',''];
992   }
993
994   #new charges
995   foreach ( $self->cust_bill_pkg ) {
996
997     if ( $_->pkgnum ) {
998
999       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
1000       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
1001       my($pkg)=$part_pkg->pkg;
1002
1003       if ( $_->setup != 0 ) {
1004         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
1005         push @buf,
1006           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1007       }
1008
1009       if ( $_->recur != 0 ) {
1010         push @buf, [
1011           "$pkg (" . time2str("%x",$_->sdate) . " - " .
1012                                 time2str("%x",$_->edate) . ")",
1013           $money_char. sprintf("%10.2f",$_->recur)
1014         ];
1015         push @buf,
1016           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1017       }
1018
1019     } else { #pkgnum Tax
1020       push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
1021         if $_->setup != 0;
1022     }
1023   }
1024
1025   push @buf,['','-----------'];
1026   push @buf,['Total New Charges',
1027              $money_char. sprintf("%10.2f",$self->charged) ];
1028   push @buf,['',''];
1029
1030   push @buf,['','-----------'];
1031   push @buf,['Total Charges',
1032              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1033   push @buf,['',''];
1034
1035   #credits
1036   foreach ( $self->cust_credited ) {
1037
1038     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1039
1040     my $reason = substr($_->cust_credit->reason,0,32);
1041     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1042     $reason = " ($reason) " if $reason;
1043     push @buf,[
1044       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1045         $reason,
1046       $money_char. sprintf("%10.2f",$_->amount)
1047     ];
1048   }
1049   #foreach ( @cr_cust_credit ) {
1050   #  push @buf,[
1051   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1052   #    $money_char. sprintf("%10.2f",$_->credited)
1053   #  ];
1054   #}
1055
1056   #get & print payments
1057   foreach ( $self->cust_bill_pay ) {
1058
1059     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1060
1061     push @buf,[
1062       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1063       $money_char. sprintf("%10.2f",$_->amount )
1064     ];
1065   }
1066
1067   #balance due
1068   push @buf,['','-----------'];
1069   push @buf,['Balance Due', $money_char. 
1070     sprintf("%10.2f", $balance_due ) ];
1071
1072   #create the template
1073   my $templatefile = 'invoice_template';
1074   $templatefile .= "_$template" if $template;
1075   my @invoice_template = $conf->config($templatefile)
1076   or die "cannot load config file $templatefile";
1077   $invoice_lines = 0;
1078   my $wasfunc = 0;
1079   foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
1080     /invoice_lines\((\d+)\)/;
1081     $invoice_lines += $1;
1082     $wasfunc=1;
1083   }
1084   die "no invoice_lines() functions in template?" unless $wasfunc;
1085   my $invoice_template = new Text::Template (
1086     TYPE   => 'ARRAY',
1087     SOURCE => [ map "$_\n", @invoice_template ],
1088   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1089   $invoice_template->compile()
1090     or die "can't compile template: $Text::Template::ERROR";
1091
1092   #setup template variables
1093   package FS::cust_bill::_template; #!
1094   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1095
1096   $invnum = $self->invnum;
1097   $date = $self->_date;
1098   $page = 1;
1099
1100   if ( $FS::cust_bill::invoice_lines ) {
1101     $total_pages =
1102       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1103     $total_pages++
1104       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1105   } else {
1106     $total_pages = 1;
1107   }
1108
1109   #format address (variable for the template)
1110   my $l = 0;
1111   @address = ( '', '', '', '', '', '' );
1112   package FS::cust_bill; #!
1113   $FS::cust_bill::_template::address[$l++] =
1114     $cust_main->payname.
1115       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1116         ? " (P.O. #". $cust_main->payinfo. ")"
1117         : ''
1118       )
1119   ;
1120   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1121     if $cust_main->company;
1122   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1123   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1124     if $cust_main->address2;
1125   $FS::cust_bill::_template::address[$l++] =
1126     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1127   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1128     unless $cust_main->country eq 'US';
1129
1130         #  #overdue? (variable for the template)
1131         #  $FS::cust_bill::_template::overdue = ( 
1132         #    $balance_due > 0
1133         #    && $today > $self->_date 
1134         ##    && $self->printed > 1
1135         #    && $self->printed > 0
1136         #  );
1137
1138   #and subroutine for the template
1139
1140   sub FS::cust_bill::_template::invoice_lines {
1141     my $lines = shift or return @buf;
1142     map { 
1143       scalar(@buf) ? shift @buf : [ '', '' ];
1144     }
1145     ( 1 .. $lines );
1146   }
1147
1148
1149   #and fill it in
1150   $FS::cust_bill::_template::page = 1;
1151   my $lines;
1152   my @collect;
1153   while (@buf) {
1154     push @collect, split("\n",
1155       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1156     );
1157     $FS::cust_bill::_template::page++;
1158   }
1159
1160   map "$_\n", @collect;
1161
1162 }
1163
1164 =back
1165
1166 =head1 VERSION
1167
1168 $Id: cust_bill.pm,v 1.41.2.6 2002-10-13 01:05:27 ivan Exp $
1169
1170 =head1 BUGS
1171
1172 The delete method.
1173
1174 print_text formatting (and some logic :/) is in source, but needs to be
1175 slurped in from a file.  Also number of lines ($=).
1176
1177 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1178 or something similar so the look can be completely customized?)
1179
1180 =head1 SEE ALSO
1181
1182 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1183 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1184 documentation.
1185
1186 =cut
1187
1188 1;
1189