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