minor refactor and better safeguards on term discounts, #15068
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
index 500d2c2..364d4a7 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;
 
@@ -138,7 +138,8 @@ I<session_id> is a session identifier associated with this payment.
 
 I<depend_jobnum> allows payment capture to unlock export jobs
 
-I<discount_term> attempts to take a discount by prepaying for discount_term
+I<discount_term> attempts to take a discount by prepaying for discount_term.
+The payment will fail if I<amount> is incorrect for this discount term.
 
 A direct (Business::OnlinePayment) transaction will return nothing on success,
 or an error message on failure.
@@ -306,6 +307,10 @@ sub _bop_content {
   $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
   $content{phone} = $self->daytime || $self->night;
 
+  my $currency =    $conf->exists('business-onlinepayment-currency')
+                 && $conf->config('business-onlinepayment-currency');
+  $content{currency} = $currency if $currency;
+
   \%content;
 }
 
@@ -401,11 +406,29 @@ 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';
+
+  ###
+  # check for term discount validity
+  ###
+
+  my $discount_term = $options{discount_term};
+  if ( $discount_term ) {
+    my $bill = ($self->cust_bill)[-1]
+      or return "Can't apply a term discount to an unbilled customer";
+    my $plan = FS::discount_plan->new(
+      cust_bill => $bill,
+      months    => $discount_term
+    ) or return "No discount available for term '$discount_term'";
+    
+    if ( $plan->discounted_total != $options{amount} ) {
+      return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
+    }
+  }
 
   ###
   # massage data
@@ -522,23 +545,18 @@ sub realtime_bop {
     'custnum' => $self->custnum,
     'status'  => { op=>'!=', value=>'done' } 
   });
-  # This is a problem.  A self-service third party payment that fails somehow 
-  # can't be retried, EVER, until someone manually clears it.  Totally 
-  # arbitrary fix: if the existing payment is more than two minutes old, 
-  # kill it.  This doesn't limit how long it can take the pending payment 
-  # to complete, only how long it will obstruct new payments.
-  my @still_pending;
-  foreach (@pending) {
-    if ( time - $_->_date > 120 ) {
+
+  #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 stale pending payment ".$_->paypendingnum.": $error"
-        if $error; # not fatal, it will fail anyway
-    }
-    else {
-      push @still_pending, $_;
+      warn "error deleting unfinished third-party payment ".
+          $_->paypendingnum . ": $error\n"
+        if $error;
     }
+    @pending = grep { $_->status ne 'thirdparty' } @pending;
   }
-  @pending = @still_pending;
 
   return "A payment is already being processed for this customer (".
          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
@@ -612,6 +630,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 ) };
 
@@ -1287,7 +1308,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,
@@ -1417,14 +1439,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') )