separate B:OP from B:OTPP
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
index caa3ab3..91f22ee 100644 (file)
@@ -4,7 +4,6 @@ use strict;
 use vars qw( $conf $DEBUG $me );
 use vars qw( $realtime_bop_decline_quiet ); #ugh
 use Data::Dumper;
-use Digest::MD5 qw(md5_base64);
 use Business::CreditCard 0.28;
 use FS::UID qw( dbh );
 use FS::Record qw( qsearch qsearchs );
@@ -13,6 +12,7 @@ use FS::payby;
 use FS::cust_pay;
 use FS::cust_pay_pending;
 use FS::cust_refund;
+use FS::banned_pay;
 
 $realtime_bop_decline_quiet = 0;
 
@@ -401,11 +401,11 @@ sub realtime_bop {
   # check for banned credit card/ACH
   ###
 
-  my $ban = qsearchs('banned_pay', {
+  my $ban = FS::banned_pay->ban_search(
     'payby'   => $bop_method2payby{$options{method}},
-    'payinfo' => md5_base64($options{payinfo}),
-  );
-  return "Banned credit card" if $ban;
+    'payinfo' => $options{payinfo},
+  );
+  return "Banned credit card" if $ban && $ban->bantype ne 'warn';
 
   ###
   # massage data
@@ -427,76 +427,87 @@ sub realtime_bop {
 
   my $paydate = '';
   my %content = ();
-  if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
-
-    $content{card_number} = $options{payinfo};
-    $paydate = exists($options{'paydate'})
-                    ? $options{'paydate'}
-                    : $self->paydate;
-    $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
-    $content{expiration} = "$2/$1";
-
-    my $paycvv = exists($options{'paycvv'})
-                   ? $options{'paycvv'}
-                   : $self->paycvv;
-    $content{cvv2} = $paycvv
-      if length($paycvv);
-
-    my $paystart_month = exists($options{'paystart_month'})
-                           ? $options{'paystart_month'}
-                           : $self->paystart_month;
-
-    my $paystart_year  = exists($options{'paystart_year'})
-                           ? $options{'paystart_year'}
-                           : $self->paystart_year;
-
-    $content{card_start} = "$paystart_month/$paystart_year"
-      if $paystart_month && $paystart_year;
-
-    my $payissue       = exists($options{'payissue'})
-                           ? $options{'payissue'}
-                           : $self->payissue;
-    $content{issue_number} = $payissue if $payissue;
-
-    if ( $self->_bop_recurring_billing( 'payinfo'        => $options{'payinfo'},
-                                        'trans_is_recur' => $trans_is_recur,
-                                      )
-       )
-    {
-      $content{recurring_billing} = 'YES';
-      $content{acct_code} = 'rebill'
-        if $conf->exists('credit_card-recurring_billing_acct_code');
-    }
 
-  } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
-    ( $content{account_number}, $content{routing_code} ) =
-      split('@', $options{payinfo});
-    $content{bank_name} = $options{payname};
-    $content{bank_state} = exists($options{'paystate'})
-                             ? $options{'paystate'}
-                             : $self->getfield('paystate');
-    $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
-                               ? uc($options{'paytype'})
-                               : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
-    $content{account_name} = $self->getfield('first'). ' '.
-                             $self->getfield('last');
+  if ( $namespace eq 'Business::OnlinePayment' ) {
+
+    if ( $options{method} eq 'CC' ) {
+
+      $content{card_number} = $options{payinfo};
+      $paydate = exists($options{'paydate'})
+                      ? $options{'paydate'}
+                      : $self->paydate;
+      $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+      $content{expiration} = "$2/$1";
+
+      my $paycvv = exists($options{'paycvv'})
+                     ? $options{'paycvv'}
+                     : $self->paycvv;
+      $content{cvv2} = $paycvv
+        if length($paycvv);
+
+      my $paystart_month = exists($options{'paystart_month'})
+                             ? $options{'paystart_month'}
+                             : $self->paystart_month;
+
+      my $paystart_year  = exists($options{'paystart_year'})
+                             ? $options{'paystart_year'}
+                             : $self->paystart_year;
+
+      $content{card_start} = "$paystart_month/$paystart_year"
+        if $paystart_month && $paystart_year;
+
+      my $payissue       = exists($options{'payissue'})
+                             ? $options{'payissue'}
+                             : $self->payissue;
+      $content{issue_number} = $payissue if $payissue;
+
+      if ( $self->_bop_recurring_billing(
+             'payinfo'        => $options{'payinfo'},
+             'trans_is_recur' => $trans_is_recur,
+           )
+         )
+      {
+        $content{recurring_billing} = 'YES';
+        $content{acct_code} = 'rebill'
+          if $conf->exists('credit_card-recurring_billing_acct_code');
+      }
+
+    } elsif ( $options{method} eq 'ECHECK' ){
+
+      ( $content{account_number}, $content{routing_code} ) =
+        split('@', $options{payinfo});
+      $content{bank_name} = $options{payname};
+      $content{bank_state} = exists($options{'paystate'})
+                               ? $options{'paystate'}
+                               : $self->getfield('paystate');
+      $content{account_type}=
+        (exists($options{'paytype'}) && $options{'paytype'})
+          ? uc($options{'paytype'})
+          : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
+      $content{account_name} = $self->getfield('first'). ' '.
+                               $self->getfield('last');
+
+      $content{customer_org} = $self->company ? 'B' : 'I';
+      $content{state_id}       = exists($options{'stateid'})
+                                   ? $options{'stateid'}
+                                   : $self->getfield('stateid');
+      $content{state_id_state} = exists($options{'stateid_state'})
+                                   ? $options{'stateid_state'}
+                                   : $self->getfield('stateid_state');
+      $content{customer_ssn} = exists($options{'ss'})
+                                 ? $options{'ss'}
+                                 : $self->ss;
+
+    } elsif ( $options{method} eq 'LEC' ) {
+      $content{phone} = $options{payinfo};
+    } else {
+      die "unknown method ". $options{method};
+    }
 
-    $content{customer_org} = $self->company ? 'B' : 'I';
-    $content{state_id}       = exists($options{'stateid'})
-                                 ? $options{'stateid'}
-                                 : $self->getfield('stateid');
-    $content{state_id_state} = exists($options{'stateid_state'})
-                                 ? $options{'stateid_state'}
-                                 : $self->getfield('stateid_state');
-    $content{customer_ssn} = exists($options{'ss'})
-                               ? $options{'ss'}
-                               : $self->ss;
-  } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
-    $content{phone} = $options{payinfo};
   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
     #move along
   } else {
-    #die an evil death
+    die "unknown namespace $namespace";
   }
 
   ###
@@ -522,6 +533,19 @@ sub realtime_bop {
     'custnum' => $self->custnum,
     'status'  => { op=>'!=', value=>'done' } 
   });
+
+  #for third-party payments only, remove pending payments if they're in the 
+  #'thirdparty' (waiting for customer action) state.
+  if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
+    foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
+      my $error = $_->delete;
+      warn "error deleting unfinished third-party payment ".
+          $_->paypendingnum . ": $error\n"
+        if $error;
+    }
+    @pending = grep { $_->status ne 'thirdparty' } @pending;
+  }
+
   return "A payment is already being processed for this customer (".
          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
          "); $options{method} transaction aborted."
@@ -594,6 +618,9 @@ sub realtime_bop {
 
   if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
 
+    $cust_pay_pending->status('thirdparty');
+    my $cpp_err = $cust_pay_pending->replace;
+    return { error => $cpp_err } if $cpp_err;
     return { reference => $cust_pay_pending->paypendingnum,
              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
 
@@ -1269,7 +1296,8 @@ Implementation note: If I<amount> is unspecified or equal to the amount of the
 orignal payment, first an attempt is made to "void" the transaction via
 the gateway (to cancel a not-yet settled transaction) and then if that fails,
 the normal attempt is made to "refund" ("credit") the transaction via the
-gateway is attempted.
+gateway is attempted. No attempt to "void" the transaction is made if the 
+gateway has introspection data and doesn't support void.
 
 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
@@ -1399,14 +1427,28 @@ sub realtime_refund_bop {
   }
 
   #first try void if applicable
+  my $void = new Business::OnlinePayment( $processor, @bop_options );
+
+  my $tryvoid = 1;
+  if ($void->can('info')) {
+      my $paytype = '';
+      $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
+      $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
+      my %supported_actions = $void->info('supported_actions');
+      $tryvoid = 0 
+        if ( %supported_actions && $paytype 
+                && defined($supported_actions{$paytype}) 
+                && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
+  }
+
   if ( $cust_pay && $cust_pay->paid == $amount
     && (
       ( not defined($disable_void_after) )
       || ( time < ($cust_pay->_date + $disable_void_after ) )
     )
+    && $tryvoid
   ) {
     warn "  attempting void\n" if $DEBUG > 1;
-    my $void = new Business::OnlinePayment( $processor, @bop_options );
     if ( $void->can('info') ) {
       if ( $cust_pay->payby eq 'CARD'
            && $void->info('CC_void_requires_card') )