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