Setup hash for CC failed Text::Template
[freeside.git] / FS / FS / cust_bill.pm
index fbccff1..2075585 100644 (file)
@@ -1,9 +1,15 @@
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $conf $invoice_template $money_char );
+use vars qw( @ISA $conf $money_char );
+use vars qw( $lpr $invoice_from $smtpmachine );
+use vars qw( $processor );
+use vars qw( $xaction $E_NoErr );
+use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
 use vars qw( $invoice_lines @buf ); #yuck
 use Date::Format;
+use Mail::Internet 1.44;
+use Mail::Header;
 use Text::Template;
 use FS::Record qw( qsearch qsearchs );
 use FS::cust_main;
@@ -12,6 +18,8 @@ use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
+use FS::cust_pay_batch;
+use FS::cust_bill_event;
 
 @ISA = qw( FS::Record );
 
@@ -22,20 +30,43 @@ $FS::UID::callback{'FS::cust_bill'} = sub {
 
   $money_char = $conf->config('money_char') || '$';  
 
-  my @invoice_template = $conf->config('invoice_template')
-    or die "cannot load config file invoice_template";
-  $invoice_lines = 0;
-  foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
-    /invoice_lines\((\d+)\)/;
-    $invoice_lines += $1;
+  $lpr = $conf->config('lpr');
+  $invoice_from = $conf->config('invoice_from');
+  $smtpmachine = $conf->config('smtpmachine');
+
+  if ( $conf->exists('cybercash3.2') ) {
+    require CCMckLib3_2;
+      #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
+    require CCMckDirectLib3_2;
+      #qw(SendCC2_1Server);
+    require CCMckErrno3_2;
+      #qw(MCKGetErrorMessage $E_NoErr);
+    import CCMckErrno3_2 qw($E_NoErr);
+
+    my $merchant_conf;
+    ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
+    my $status = &CCMckLib3_2::InitConfig($merchant_conf);
+    if ( $status != $E_NoErr ) {
+      warn "CCMckLib3_2::InitConfig error:\n";
+      foreach my $key (keys %CCMckLib3_2::Config) {
+        warn "  $key => $CCMckLib3_2::Config{$key}\n"
+      }
+      my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
+      die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
+    }
+    $processor='cybercash3.2';
+  } elsif ( $conf->exists('business-onlinepayment') ) {
+    ( $bop_processor,
+      $bop_login,
+      $bop_password,
+      $bop_action,
+      @bop_options
+    ) = $conf->config('business-onlinepayment');
+    $bop_action ||= 'normal authorization';
+    eval "use Business::OnlinePayment";  
+    $processor="Business::OnlinePayment::$bop_processor";
   }
-  die "no invoice_lines() functions in template?" unless $invoice_lines;
-  $invoice_template = new Text::Template (
-    TYPE   => 'ARRAY',
-    SOURCE => [ map "$_\n", @invoice_template ],
-  ) or die "can't create new Text::Template object: $Text::Template::ERROR";
-  $invoice_template->compile()
-    or die "can't compile template: $Text::Template::ERROR";
+
 };
 
 =head1 NAME
@@ -88,8 +119,9 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =item charged - amount of this invoice
 
-=item printed - how many times this invoice has been printed automatically
-(see L<FS::cust_main/"collect">).
+=item printed - deprecated
+
+=item closed - books closed flag, empty or `Y'
 
 =back
 
@@ -120,7 +152,9 @@ no record you ever posted this invoice (which is bad, no?)
 =cut
 
 sub delete {
-  return "Can't remove invoice!"
+  my $self = shift;
+  return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
+  $self->SUPER::delete(@_);
 }
 
 =item replace OLD_RECORD
@@ -160,6 +194,7 @@ sub check {
     || $self->ut_numbern('_date')
     || $self->ut_money('charged')
     || $self->ut_numbern('printed')
+    || $self->ut_enum('closed', [ '', 'Y' ])
   ;
   return $error if $error;
 
@@ -202,6 +237,30 @@ sub cust_bill_pkg {
   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
 }
 
+=item cust_bill_event
+
+Returns the completed invoice events (see L<FS::cust_bill_event>) for this
+invoice.
+
+=cut
+
+sub cust_bill_event {
+  my $self = shift;
+  qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
+}
+
+
+=item cust_main
+
+Returns the customer (see L<FS::cust_main>) for this invoice.
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+}
+
 =item cust_credit
 
 Depreciated.  See the cust_credited method.
@@ -301,6 +360,379 @@ sub owed {
   $balance;
 }
 
+=item send
+
+Sends this invoice to the destinations configured for this customer: send
+emails or print.  See L<FS::cust_main_invoice>.
+
+=cut
+
+sub send {
+  my($self,$template) = @_;
+
+  #my @print_text = $cust_bill->print_text; #( date )
+  my @invoicing_list = $self->cust_main->invoicing_list;
+  if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
+    #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
+    #$ENV{SMTPHOSTS} = $smtpmachine;
+    $ENV{MAILADDRESS} = $invoice_from;
+    my $header = new Mail::Header ( [
+      "From: $invoice_from",
+      "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
+      "Sender: $invoice_from",
+      "Reply-To: $invoice_from",
+      "Date: ". time2str("%a, %d %b %Y %X %z", time),
+      "Subject: Invoice",
+    ] );
+    my $message = new Mail::Internet (
+      'Header' => $header,
+      'Body' => [ $self->print_text('', $template) ], #( date)
+    );
+    $!=0;
+    $message->smtpsend( Host => $smtpmachine )
+      or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+        or return "(customer # ". $self->custnum. ") can't send invoice email".
+                  " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
+                  " via server $smtpmachine with SMTP: $!";
+
+  #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
+  } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
+    open(LPR, "|$lpr")
+      or return "Can't open pipe to $lpr: $!";
+    print LPR $self->print_text; #( date )
+    close LPR
+      or return $! ? "Error closing $lpr: $!"
+                   : "Exit status $? from $lpr";
+  }
+
+  '';
+
+}
+
+=item comp
+
+Pays this invoice with a compliemntary payment.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub comp {
+  my $self = shift;
+  my $cust_pay = new FS::cust_pay ( {
+    'invnum'   => $self->invnum,
+    'paid'     => $self->owed,
+    '_date'    => '',
+    'payby'    => 'COMP',
+    'payinfo'  => $self->cust_main->payinfo,
+    'paybatch' => '',
+  } );
+  $cust_pay->insert;
+}
+
+=item realtime_card
+
+Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
+See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supproted processors.
+
+=cut
+
+sub realtime_card {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+  my $amount = $self->owed;
+
+  unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
+    return "Real-time card processing not enabled (processor $processor)";
+  }
+  my $bop_processor = $1; #hmm?
+
+  my $address = $cust_main->address1;
+  $address .= ", ". $cust_main->address2 if $cust_main->address2;
+
+  #fix exp. date
+  #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
+  $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+  my $exp = "$2/$1";
+
+  my($payname, $payfirst, $paylast);
+  if ( $cust_main->payname ) {
+    $payname = $cust_main->payname;
+    $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
+      or do {
+              #$dbh->rollback if $oldAutoCommit;
+              return "Illegal payname $payname";
+            };
+    ($payfirst, $paylast) = ($1, $2);
+  } else {
+    $payfirst = $cust_main->getfield('first');
+    $paylast = $cust_main->getfield('last');
+    $payname =  "$payfirst $paylast";
+  }
+
+  my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
+  if ( $conf->exists('emailinvoiceauto')
+       || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
+    push @invoicing_list, $cust_main->default_invoicing_list;
+  }
+  my $email = $invoicing_list[0];
+
+  my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
+
+  my $description = 'Internet Services';
+  if ( $conf->exists('business-onlinepayment-description') ) {
+    my $dtempl = $conf->config('business-onlinepayment-description');
+
+    my $agent_obj = $cust_main->agent
+      or die "can't retreive agent for $cust_main (agentnum ".
+             $cust_main->agentnum. ")";
+    my $agent = $agent_obj->agent;
+    my $pkgs = join(', ',
+      map { $_->cust_pkg->part_pkg->pkg }
+        grep { $_->pkgnum } $self->cust_bill_pkg
+    );
+    $description = eval qq("$dtempl");
+
+  }
+  
+  my $transaction =
+    new Business::OnlinePayment( $bop_processor, @bop_options );
+  $transaction->content(
+    'type'           => 'CC',
+    'login'          => $bop_login,
+    'password'       => $bop_password,
+    'action'         => $action1,
+    'description'    => $description,
+    'amount'         => $amount,
+    'invoice_number' => $self->invnum,
+    'customer_id'    => $self->custnum,
+    'last_name'      => $paylast,
+    'first_name'     => $payfirst,
+    'name'           => $payname,
+    'address'        => $address,
+    'city'           => $cust_main->city,
+    'state'          => $cust_main->state,
+    'zip'            => $cust_main->zip,
+    'country'        => $cust_main->country,
+    'card_number'    => $cust_main->payinfo,
+    'expiration'     => $exp,
+    'referer'        => 'http://cleanwhisker.420.am/',
+    'email'          => $email,
+    'phone'          => $cust_main->daytime || $cust_main->night,
+  );
+  $transaction->submit();
+
+  if ( $transaction->is_success() && $action2 ) {
+    my $auth = $transaction->authorization;
+    my $ordernum = $transaction->order_number;
+
+    #warn "********* $auth ***********\n";
+    #warn "********* $ordernum ***********\n";
+    my $capture =
+      new Business::OnlinePayment( $bop_processor, @bop_options );
+
+    $capture->content(
+      action         => $action2,
+      login          => $bop_login,
+      password       => $bop_password,
+      order_number   => $ordernum,
+      amount         => $amount,
+      authorization  => $auth,
+      description    => $description,
+    );
+
+    $capture->submit();
+
+    unless ( $capture->is_success ) {
+      my $e = "Authorization sucessful but capture failed, invnum #".
+              $self->invnum. ': '.  $capture->result_code.
+              ": ". $capture->error_message;
+      warn $e;
+      return $e;
+    }
+
+  }
+
+  if ( $transaction->is_success() ) {
+
+    my $cust_pay = new FS::cust_pay ( {
+       'invnum'   => $self->invnum,
+       'paid'     => $amount,
+       '_date'     => '',
+       'payby'    => 'CARD',
+       'payinfo'  => $cust_main->payinfo,
+       'paybatch' => "$processor:". $transaction->authorization,
+    } );
+    my $error = $cust_pay->insert;
+    if ( $error ) {
+      # gah, even with transactions.
+      my $e = 'WARNING: Card debited but database not updated - '.
+              'error applying payment, invnum #' . $self->invnum.
+              " ($processor): $error";
+      warn $e;
+      return $e;
+    } else {
+      return '';
+    }
+  #} elsif ( $options{'report_badcard'} ) {
+  } else {
+
+    my $perror = "$processor error, invnum #". $self->invnum. ': '.
+                 $transaction->result_code. ": ". $transaction->error_message;
+
+    if ( $conf->exists('emaildecline')
+         && grep { $_ ne 'POST' } $cust_main->invoicing_list
+    ) {
+      my @templ = $conf->config('declinetemplate');
+      my $template = new Text::Template (
+        TYPE   => 'ARRAY',
+        SOURCE => [ map "$_\n", @templ ],
+      ) or return "($perror) can't create template: $Text::Template::ERROR";
+      $template->compile()
+        or return "($perror) can't compile template: $Text::Template::ERROR";
+
+      my $templ_hash = { error => $transaction->error_message };
+
+      #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
+      $ENV{MAILADDRESS} = $invoice_from;
+      my $header = new Mail::Header ( [
+        "From: $invoice_from",
+        "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
+        "Sender: $invoice_from",
+        "Reply-To: $invoice_from",
+        "Date: ". time2str("%a, %d %b %Y %X %z", time),
+        "Subject: Your credit card could not be processed",
+      ] );
+      my $message = new Mail::Internet (
+        'Header' => $header,
+        'Body' => [ $template->fill_in(HASH => $templ_hash) ],
+      );
+      $!=0;
+      $message->smtpsend( Host => $smtpmachine )
+        or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+          or return "($perror) (customer # ". $self->custnum.
+            ") can't send card decline email to ".
+            join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
+            " via server $smtpmachine with SMTP: $!";
+    }
+  
+    return $perror;
+  }
+
+}
+
+=item realtime_card_cybercash
+
+Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
+
+=cut
+
+sub realtime_card_cybercash {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+  my $amount = $self->owed;
+
+  return "CyberCash CashRegister real-time card processing not enabled!"
+    unless $processor eq 'cybercash3.2';
+
+  my $address = $cust_main->address1;
+  $address .= ", ". $cust_main->address2 if $cust_main->address2;
+
+  #fix exp. date
+  #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
+  $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+  my $exp = "$2/$1";
+
+  #
+
+  my $paybatch = $self->invnum. 
+                  '-' . time2str("%y%m%d%H%M%S", time);
+
+  my $payname = $cust_main->payname ||
+                $cust_main->getfield('first').' '.$cust_main->getfield('last');
+
+  my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
+
+  my @full_xaction = ( $xaction,
+    'Order-ID'     => $paybatch,
+    'Amount'       => "usd $amount",
+    'Card-Number'  => $cust_main->getfield('payinfo'),
+    'Card-Name'    => $payname,
+    'Card-Address' => $address,
+    'Card-City'    => $cust_main->getfield('city'),
+    'Card-State'   => $cust_main->getfield('state'),
+    'Card-Zip'     => $cust_main->getfield('zip'),
+    'Card-Country' => $country,
+    'Card-Exp'     => $exp,
+  );
+
+  my %result;
+  %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
+  
+  if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
+    my $cust_pay = new FS::cust_pay ( {
+       'invnum'   => $self->invnum,
+       'paid'     => $amount,
+       '_date'     => '',
+       'payby'    => 'CARD',
+       'payinfo'  => $cust_main->payinfo,
+       'paybatch' => "$processor:$paybatch",
+    } );
+    my $error = $cust_pay->insert;
+    if ( $error ) {
+      # gah, even with transactions.
+      my $e = 'WARNING: Card debited but database not updated - '.
+              'error applying payment, invnum #' . $self->invnum.
+              " (CyberCash Order-ID $paybatch): $error";
+      warn $e;
+      return $e;
+    } else {
+      return '';
+    }
+#  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
+#            || $options{'report_badcard'}
+#          ) {
+  } else {
+     return 'Cybercash error, invnum #' . 
+       $self->invnum. ':'. $result{'MErrMsg'};
+  }
+
+}
+
+=item batch_card
+
+Adds a payment for this invoice to the pending credit card batch (see
+L<FS::cust_pay_batch>).
+
+=cut
+
+sub batch_card {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+
+  my $cust_pay_batch = new FS::cust_pay_batch ( {
+    'invnum'   => $self->getfield('invnum'),
+    'custnum'  => $cust_main->getfield('custnum'),
+    'last'     => $cust_main->getfield('last'),
+    'first'    => $cust_main->getfield('first'),
+    'address1' => $cust_main->getfield('address1'),
+    'address2' => $cust_main->getfield('address2'),
+    'city'     => $cust_main->getfield('city'),
+    'state'    => $cust_main->getfield('state'),
+    'zip'      => $cust_main->getfield('zip'),
+    'country'  => $cust_main->getfield('country'),
+    'trancode' => 77,
+    'cardnum'  => $cust_main->getfield('payinfo'),
+    'exp'      => $cust_main->getfield('paydate'),
+    'payname'  => $cust_main->getfield('payname'),
+    'amount'   => $self->owed,
+  } );
+  my $error = $cust_pay_batch->insert;
+  die $error if $error;
+
+  '';
+}
+
 =item print_text [TIME];
 
 Returns an text invoice, as a list of lines.
@@ -314,7 +746,7 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 sub print_text {
 
-  my( $self, $today ) = ( shift, shift );
+  my( $self, $today, $template ) = @_;
   $today ||= time;
 #  my $invnum = $self->invnum;
   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
@@ -423,8 +855,27 @@ sub print_text {
   push @buf,['Balance Due', $money_char. 
     sprintf("%10.2f", $balance_due ) ];
 
+  #create the template
+  my $templatefile = 'invoice_template';
+  $templatefile .= "_$template" if $template;
+  my @invoice_template = $conf->config($templatefile)
+  or die "cannot load config file $templatefile";
+  $invoice_lines = 0;
+  my $wasfunc = 0;
+  foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
+    /invoice_lines\((\d+)\)/;
+    $invoice_lines += $1;
+    $wasfunc=1;
+  }
+  die "no invoice_lines() functions in template?" unless $wasfunc;
+  my $invoice_template = new Text::Template (
+    TYPE   => 'ARRAY',
+    SOURCE => [ map "$_\n", @invoice_template ],
+  ) or die "can't create new Text::Template object: $Text::Template::ERROR";
+  $invoice_template->compile()
+    or die "can't compile template: $Text::Template::ERROR";
+
   #setup template variables
-  
   package FS::cust_bill::_template; #!
   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
 
@@ -432,11 +883,14 @@ sub print_text {
   $date = $self->_date;
   $page = 1;
 
-  $total_pages =
-    int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
-  $total_pages++
-    if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
-
+  if ( $FS::cust_bill::invoice_lines ) {
+    $total_pages =
+      int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
+    $total_pages++
+      if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
+  } else {
+    $total_pages = 1;
+  }
 
   #format address (variable for the template)
   my $l = 0;
@@ -459,24 +913,26 @@ sub print_text {
   $FS::cust_bill::_template::address[$l++] = $cust_main->country
     unless $cust_main->country eq 'US';
 
-  #overdue? (variable for the template)
-  $FS::cust_bill::_template::overdue = ( 
-    $balance_due > 0
-    && $today > $self->_date 
-#    && $self->printed > 1
-    && $self->printed > 0
-  );
+       #  #overdue? (variable for the template)
+       #  $FS::cust_bill::_template::overdue = ( 
+       #    $balance_due > 0
+       #    && $today > $self->_date 
+       ##    && $self->printed > 1
+       #    && $self->printed > 0
+       #  );
 
   #and subroutine for the template
 
   sub FS::cust_bill::_template::invoice_lines {
-    my $lines = shift;
+    my $lines = shift or return @buf;
     map { 
       scalar(@buf) ? shift @buf : [ '', '' ];
     }
     ( 1 .. $lines );
   }
-    
+
+
+  #and fill it in
   $FS::cust_bill::_template::page = 1;
   my $lines;
   my @collect;
@@ -495,7 +951,7 @@ sub print_text {
 
 =head1 VERSION
 
-$Id: cust_bill.pm,v 1.14 2001-12-21 21:40:24 ivan Exp $
+$Id: cust_bill.pm,v 1.37 2002-06-07 20:33:27 khoff Exp $
 
 =head1 BUGS
 
@@ -509,7 +965,7 @@ or something similar so the look can be completely customized?)
 
 =head1 SEE ALSO
 
-L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS:;cust_pay>,
+L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
 documentation.