add (unfinished) credit card surcharge, part 1
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
index 4159d04..ea09379 100644 (file)
@@ -3,6 +3,7 @@ package FS::cust_main::Billing_Realtime;
 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 );
@@ -18,7 +19,7 @@ $realtime_bop_decline_quiet = 0;
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
 # 3 is even more information including possibly sensitive data
-$DEBUG = 0;
+$DEBUG = 1;
 $me = '[FS::cust_main::Billing_Realtime]';
 
 install_callback FS::UID sub { 
@@ -88,6 +89,8 @@ I<depend_jobnum> allows payment capture to unlock export jobs
 sub realtime_collect {
   my( $self, %options ) = @_;
 
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
   if ( $DEBUG ) {
     warn "$me realtime_collect:\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
@@ -138,6 +141,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
+
 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
 
 =cut
@@ -173,6 +178,21 @@ sub _bop_recurring_billing {
 sub _payment_gateway {
   my ($self, $options) = @_;
 
+  if ( $options->{'selfservice'} ) {
+    my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
+    if ( $gatewaynum ) {
+      return $options->{payment_gateway} ||= 
+          qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
+    }
+  }
+
+  if ( $options->{'fake_gatewaynum'} ) {
+       $options->{payment_gateway} =
+           qsearchs('payment_gateway',
+                     { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
+                   );
+  }
+
   $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
     unless exists($options->{payment_gateway});
 
@@ -284,6 +304,8 @@ my %bop_method2payby = (
 sub realtime_bop {
   my $self = shift;
 
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
   my %options = ();
   if (ref($_[0]) eq 'HASH') {
     %options = %{$_[0]};
@@ -293,13 +315,39 @@ sub realtime_bop {
     $options{method} = $method;
     $options{amount} = $amount;
   }
+
+
+  ### 
+  # optional credit card surcharge
+  ###
+
+  my $cc_surcharge = 0;
+  my $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage');
   
+  # always add cc surcharge if called from event 
+  if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
+      $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
+      $options{'amount'} += $cc_surcharge;
+      $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
+  }
+  elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a 
+                                 # payment screen), so consider the given 
+                                # amount as post-surcharge
+    $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
+  }
+  if ( $cc_surcharge > 0) {
+      $cc_surcharge = sprintf("%.2f",$cc_surcharge);
+      $options{'cc_surcharge'} = $cc_surcharge;
+  }
+
   if ( $DEBUG ) {
     warn "$me realtime_bop (new): $options{method} $options{amount}\n";
+    warn " cc_surcharge = $cc_surcharge\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
-  return $self->fake_bop(%options) if $options{'fake'};
+  return $self->fake_bop(\%options) if $options{'fake'};
 
   $self->_bop_defaults(\%options);
 
@@ -460,6 +508,24 @@ 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 ) {
+      my $error = $_->delete;
+      warn "error deleting stale pending payment ".$_->paypendingnum.": $error"
+        if $error; # not fatal, it will fail anyway
+    }
+    else {
+      push @still_pending, $_;
+    }
+  }
+  @pending = @still_pending;
+
   return "A payment is already being processed for this customer (".
          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
          "); $options{method} transaction aborted."
@@ -504,6 +570,7 @@ sub realtime_bop {
     'customer_id'    => $self->custnum,
     %$bop_content,
     'reference'      => $cust_pay_pending->paypendingnum, #for now
+    'callback_url'   => $payment_gateway->gateway_callback_url,
     'email'          => $email,
     %content, #after
   );
@@ -675,6 +742,11 @@ sub fake_bop {
   } );
   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
 
+  if ( $DEBUG ) {
+      warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
+      warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
 
   if ( $error ) {
@@ -709,6 +781,9 @@ sub fake_bop {
 
 sub _realtime_bop_result {
   my( $self, $cust_pay_pending, $transaction, %options ) = @_;
+
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
   if ( $DEBUG ) {
     warn "$me _realtime_bop_result: pending transaction ".
       $cust_pay_pending->paypendingnum. "\n";
@@ -746,6 +821,7 @@ sub _realtime_bop_result {
        'paybatch' => $paybatch,
        'paydate'  => $cust_pay_pending->paydate,
        'pkgnum'   => $cust_pay_pending->pkgnum,
+       'discount_term' => $options{'discount_term'},
     } );
     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
     $cust_pay->payunique( $options{payunique} )
@@ -834,6 +910,60 @@ sub _realtime_bop_result {
         }
       }
 
+      # have a CC surcharge portion --> one-time charge
+      if ( $options{'cc_surcharge'} > 0 ) { 
+         my $invnum;
+         $invnum = $options{'invnum'} if $options{'invnum'};
+         unless ( $invnum ) { # probably from a payment screen
+            # do we have any open invoices? pick earliest
+            # uses the fact that cust_main->cust_bill sorts by date ascending
+            my @open = $self->open_cust_bill;
+            $invnum = $open[0]->invnum if scalar(@open);
+         }
+           
+         unless ( $invnum ) {  # still nothing? pick last closed invoice
+            # again uses fact that cust_main->cust_bill sorts by date ascending
+            my @closed = $self->cust_bill;
+            $invnum = $closed[$#closed]->invnum if scalar(@closed);
+         }
+
+         unless ( $invnum ) {
+           # XXX: unlikely case - pre-paying before any invoices generated
+           # what it should do is create a new invoice and pick it
+               warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
+               return '';
+         }
+
+         my $cust_pkg;
+         my $charge_error = $self->charge({
+                                   'amount'    => $options{'cc_surcharge'},
+                                   'pkg'       => 'Credit Card Surcharge',
+                                   'setuptax'  => 'Y',
+                                   'cust_pkg_ref' => \$cust_pkg,
+                               });
+         if($charge_error) {
+               warn 'Unable to add CC surcharge';
+               return '';
+         }
+                                   
+         my $cust_bill_pkg = new FS::cust_bill_pkg({
+           'invnum' => $invnum,
+           'pkgnum' => $cust_pkg->pkgnum,
+           'setup' => $options{'cc_surcharge'},
+         });
+         my $cbp_error = $cust_bill_pkg->insert;
+
+         if ( $cbp_error) {
+               warn 'Cannot add CC surcharge line item to invoice #'.$invnum;
+               return '';
+         } else {
+               my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
+               warn 'invoice for cc surcharge: ' . Dumper($cust_bill) if $DEBUG;
+               $cust_bill->apply_payments_and_credits;
+         }
+
+      }
+
       return ''; #no error
 
     }
@@ -891,10 +1021,10 @@ sub _realtime_bop_result {
     }
 
     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
-         && $conf->exists('emaildecline')
+         && $conf->exists('emaildecline', $self->agentnum)
          && grep { $_ ne 'POST' } $self->invoicing_list
          && ! grep { $transaction->error_message =~ /$_/ }
-                   $conf->config('emaildecline-exclude')
+                   $conf->config('emaildecline-exclude', $self->agentnum)
     ) {
 
       # Send a decline alert to the customer.
@@ -988,6 +1118,9 @@ upon success) and session_id of any associated session.
 
 sub realtime_botpp_capture {
   my( $self, $cust_pay_pending, %options ) = @_;
+
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
   if ( $DEBUG ) {
     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
     warn "  $_ => $options{$_}\n" foreach keys %options;
@@ -1002,9 +1135,10 @@ sub realtime_botpp_capture {
 
   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
 
-  my $payment_gateway = $cust_pay_pending->gatewaynum
-    ? qsearchs( 'payment_gateway',
-                { gatewaynum => $cust_pay_pending->gatewaynum }
+  my $payment_gateway;
+  my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
+  $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
+                { gatewaynum => $gatewaynum }
               )
     : $self->agent->payment_gateway( 'method' => $method,
                                      # 'invnum'  => $cust_pay_pending->invnum,
@@ -1066,7 +1200,14 @@ sub realtime_botpp_capture {
   my $error =
     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
 
-  {
+  if ( $options{'apply'} ) {
+    my $apply_error = $self->apply_payments_and_credits;
+    if ( $apply_error ) {
+      warn "WARNING: error applying payment: $apply_error\n";
+    }
+  }
+
+  return {
     bill_error => $error,
     session_id => $cust_pay_pending->session_id,
   }
@@ -1143,6 +1284,8 @@ gateway is attempted.
 sub realtime_refund_bop {
   my $self = shift;
 
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
   my %options = ();
   if (ref($_[0]) eq 'HASH') {
     %options = %{$_[0]};