don't run $0 transactions for self-service orders of $0 packages, RT#39078
[freeside.git] / FS / FS / cust_main / Billing_Realtime.pm
index e5e5291..1cd2aba 100644 (file)
@@ -3,14 +3,15 @@ package FS::cust_main::Billing_Realtime;
 use strict;
 use vars qw( $conf $DEBUG $me );
 use vars qw( $realtime_bop_decline_quiet ); #ugh
+use Carp;
 use Data::Dumper;
 use Business::CreditCard 0.28;
 use FS::UID qw( dbh );
 use FS::Record qw( qsearch qsearchs );
-use FS::Misc qw( send_email );
 use FS::payby;
 use FS::cust_pay;
 use FS::cust_pay_pending;
+use FS::cust_bill_pay;
 use FS::cust_refund;
 use FS::banned_pay;
 
@@ -44,6 +45,31 @@ These methods are available on FS::cust_main objects.
 
 =over 4
 
+=item realtime_cust_payby
+
+=cut
+
+sub realtime_cust_payby {
+  my( $self, %options ) = @_;
+
+  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
+  $options{amount} = $self->balance unless exists( $options{amount} );
+
+  my @cust_payby = $self->cust_payby('CARD','CHEK');
+                                                   
+  my $error;
+  foreach my $cust_payby (@cust_payby) {
+    $error = $cust_payby->realtime_bop( %options, );
+    last unless $error;
+  }
+
+  #XXX what about the earlier errors?
+
+  $error;
+
+}
+
 =item realtime_collect [ OPTION => VALUE ... ]
 
 Attempt to collect the customer's current balance with a realtime credit 
@@ -96,6 +122,8 @@ sub realtime_collect {
   }
 
   $options{amount} = $self->balance unless exists( $options{amount} );
+  return '' unless $options{amount} > 0;
+
   $options{method} = FS::payby->payby2bop($self->payby)
     unless exists( $options{method} );
 
@@ -128,7 +156,13 @@ specified invoice.  If the customer has exactly one open invoice, that
 invoice number will be assumed.  If you don't specify an I<invnum> you might 
 want to call the B<apply_payments> method or set the I<apply> option.
 
-I<apply> can be set to true to apply a resulting payment.
+I<no_invnum> can be set to true to prevent that default invnum from being set.
+
+I<apply> can be set to true to run B<apply_payments_and_credits> on success.
+
+I<no_auto_apply> can be set to true to set that flag on the resulting payment
+(prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
+but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
 
 I<quiet> can be set true to surpress email decline notices.
 
@@ -159,6 +193,15 @@ A third-party transaction will return a hashref containing:
 =cut
 
 # some helper routines
+#
+# _bop_recurring_billing: Checks whether this payment should have the 
+# recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
+# vSecure, etc.). This works in two different modes:
+# - actual_oncard (default): treat the payment as recurring if the customer
+#   has made a payment using this card before.
+# - transaction_is_recur: treat the payment as recurring if the invoice
+#   being paid has any recurring package charges.
+
 sub _bop_recurring_billing {
   my( $self, %opt ) = @_;
 
@@ -236,10 +279,13 @@ sub _bop_defaults {
     }
   }
 
-  $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
+  unless ( exists( $options->{'payinfo'} ) ) {
+    $options->{'payinfo'} = $self->payinfo;
+    $options->{'paymask'} = $self->paymask;
+  }
 
   # Default invoice number if the customer has exactly one open invoice.
-  if( ! $options->{'invnum'} ) {
+  unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
     $options->{'invnum'} = '';
     my @open = $self->open_cust_bill;
     $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
@@ -300,10 +346,6 @@ sub _bop_content {
                         ? $options->{country}
                         : $self->country;
 
-  #3.0 is a good a time as any to get rid of this... add a config to pass it
-  # if anyone still needs it
-  #$content{referer} = 'http://cleanwhisker.420.am/';
-
   $content{phone} = $self->daytime || $self->night;
 
   my $currency =    $conf->exists('business-onlinepayment-currency')
@@ -323,6 +365,10 @@ my %bop_method2payby = (
 sub realtime_bop {
   my $self = shift;
 
+  confess "Can't call realtime_bop within another transaction ".
+          '($FS::UID::AutoCommit is false)'
+    unless $FS::UID::AutoCommit;
+
   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
  
   my %options = ();
@@ -343,8 +389,9 @@ sub realtime_bop {
   my $cc_surcharge = 0;
   my $cc_surcharge_pct = 0;
   $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage') 
-    if $conf->config('credit-card-surcharge-percentage');
-  
+    if $conf->config('credit-card-surcharge-percentage')
+    && $options{method} eq 'CC';
+
   # 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;
@@ -364,6 +411,8 @@ sub realtime_bop {
   if ( $DEBUG ) {
     warn "$me realtime_bop (new): $options{method} $options{amount}\n";
     warn " cc_surcharge = $cc_surcharge\n";
+  }
+  if ( $DEBUG > 2 ) {
     warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
@@ -546,7 +595,9 @@ sub realtime_bop {
                   ? $options{'balance'}
                   : $self->balance;
 
+  warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
   $self->select_for_update; #mutex ... just until we get our pending record in
+  warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
 
   #the checks here are intended to catch concurrent payments
   #double-form-submission prevention is taken care of in cust_pay_pending::check
@@ -587,6 +638,7 @@ sub realtime_bop {
     '_date'             => '',
     'payby'             => $bop_method2payby{$options{method}},
     'payinfo'           => $options{payinfo},
+    'paymask'           => $options{paymask},
     'paydate'           => $paydate,
     'recurring_billing' => $content{recurring_billing},
     'pkgnum'            => $options{'pkgnum'},
@@ -597,9 +649,16 @@ sub realtime_bop {
   };
   $cust_pay_pending->payunique( $options{payunique} )
     if defined($options{payunique}) && length($options{payunique});
+
+  warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
+    if $DEBUG > 1;
   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
   return $cpp_new_err if $cpp_new_err;
 
+  warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
+    if $DEBUG > 1;
+  warn Dumper($cust_pay_pending) if $DEBUG > 2;
+
   my( $action1, $action2 ) =
     split( /\s*\,\s*/, $payment_gateway->gateway_action );
 
@@ -705,8 +764,7 @@ sub realtime_bop {
   # remove paycvv after initial transaction
   ###
 
-  #false laziness w/misc/process/payment.cgi - check both to make sure working
-  # correctly
+  # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
   if ( length($self->paycvv)
        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
   ) {
@@ -723,8 +781,6 @@ sub realtime_bop {
 
   if ( $transaction->can('card_token') && $transaction->card_token ) {
 
-    $self->card_token($transaction->card_token);
-
     if ( $options{'payinfo'} eq $self->payinfo ) {
       $self->payinfo($transaction->card_token);
       my $error = $self->replace;
@@ -847,6 +903,7 @@ sub _realtime_bop_result {
        '_date'    => '',
        'payby'    => $cust_pay_pending->payby,
        'payinfo'  => $options{'payinfo'},
+       'paymask'  => $options{'paymask'} || $cust_pay_pending->paymask,
        'paydate'  => $cust_pay_pending->paydate,
        'pkgnum'   => $cust_pay_pending->pkgnum,
        'discount_term'  => $options{'discount_term'},
@@ -854,7 +911,7 @@ sub _realtime_bop_result {
        'processor'      => $payment_gateway->gateway_module,
        'auth'           => $transaction->authorization,
        'order_number'   => $order_number || '',
-
+       'no_auto_apply'  => $options{'no_auto_apply'} ? 'Y' : '',
     } );
     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
     $cust_pay->payunique( $options{payunique} )
@@ -913,6 +970,8 @@ sub _realtime_bop_result {
          return $e;
        }
 
+       $cust_pay_pending->set('jobnum','');
+
     }
     
     if ( $options{'paynum_ref'} ) {
@@ -1021,8 +1080,9 @@ sub _realtime_bop_result {
        if ( $placeholder ) {
          my $error = $placeholder->depended_delete;
          $error ||= $placeholder->delete;
+         $cust_pay_pending->set('jobnum','');
          warn "error removing provisioning jobs after declined paypendingnum ".
-           $cust_pay_pending->paypendingnum. ": $error\n";
+           $cust_pay_pending->paypendingnum. ": $error\n" if $error;
        } else {
          my $e = "error finding job $jobnum for declined paypendingnum ".
               $cust_pay_pending->paypendingnum. "\n";
@@ -1078,31 +1138,7 @@ sub _realtime_bop_result {
         $error = $msg_template->send( 'cust_main' => $self,
                                       'object'    => $cust_pay_pending );
       }
-      else { #!$msgnum
-
-        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 = {
-          'company_name'    =>
-            scalar( $conf->config('company_name', $self->agentnum ) ),
-          'company_address' =>
-            join("\n", $conf->config('company_address', $self->agentnum ) ),
-          'error'           => $transaction->error_message,
-        };
-
-        my $error = send_email(
-          'from'    => $conf->config('invoice_from', $self->agentnum ),
-          'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
-          'subject' => 'Your payment could not be processed',
-          'body'    => [ $template->fill_in(HASH => $templ_hash) ],
-        );
-      }
+
 
       $perror .= " (also received error sending decline notification: $error)"
         if $error;
@@ -1232,11 +1268,6 @@ sub realtime_botpp_capture {
     'amount'         => $cust_pay_pending->paid,
     #'invoice_number' => $options{'invnum'},
     'customer_id'    => $self->custnum,
-
-    #3.0 is a good a time as any to get rid of this... add a config to pass it
-    # if anyone still needs it
-    #'referer'        => 'http://cleanwhisker.420.am/',
-
     'reference'      => $cust_pay_pending->paypendingnum,
     'email'          => $email,
     'phone'          => $self->daytime || $self->night,
@@ -1300,14 +1331,14 @@ L<http://420.am/business-onlinepayment> for supported gateways.
 
 Available methods are: I<CC>, I<ECHECK> and I<LEC>
 
-Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
+Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
 
 Most gateways require a reference to an original payment transaction to refund,
 so you probably need to specify a I<paynum>.
 
 I<amount> defaults to the original amount of the payment if not specified.
 
-I<reason> specifies a reason for the refund.
+I<reasonnum> specified an existing refund reason for the refund
 
 I<paydate> specifies the expiration date for a credit card overriding the
 value from the customer record or the payment record. Specified as yyyy-mm-dd
@@ -1350,6 +1381,10 @@ sub realtime_refund_bop {
     warn "  $_ => $options{$_}\n" foreach keys %options;
   }
 
+  return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
+
+  my %content = ();
+
   ###
   # look up the original payment and optionally a gateway for that payment
   ###
@@ -1368,6 +1403,9 @@ sub realtime_refund_bop {
       or return "Unknown paynum $options{'paynum'}";
     $amount ||= $cust_pay->paid;
 
+    my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
+    $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
+
     if ( $cust_pay->get('processor') ) {
       ($gatewaynum, $processor, $auth, $order_number) =
       (
@@ -1440,16 +1478,13 @@ sub realtime_refund_bop {
   eval "use $namespace";  
   die $@ if $@;
 
-  my %content = (
+  %content = (
+    %content,
     'type'           => $options{method},
     'login'          => $login,
     'password'       => $password,
     'order_number'   => $order_number,
     'amount'         => $amount,
-
-    #3.0 is a good a time as any to get rid of this... add a config to pass it
-    # if anyone still needs it
-    #'referer'        => 'http://cleanwhisker.420.am/',
   );
   $content{authorization} = $auth
     if length($auth); #echeck/ACH transactions have an order # but no auth
@@ -1506,7 +1541,12 @@ sub realtime_refund_bop {
       if $conf->exists('business-onlinepayment-test_transaction');
     $void->submit();
     if ( $void->is_success ) {
-      my $error = $cust_pay->void($options{'reason'});
+      # specified as a refund reason, but now we want a payment void reason
+      # extract just the reason text, let cust_pay::void handle new_or_existing
+      my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
+      my $error;
+      $error = 'Reason could not be loaded' unless $reason;      
+      $error = $cust_pay->void($reason->reason) unless $error;
       if ( $error ) {
         # gah, even with transactions.
         my $e = 'WARNING: Card/ACH voided but database not updated - '.
@@ -1615,6 +1655,7 @@ sub realtime_refund_bop {
 
   $order_number = $refund->order_number if $refund->can('order_number');
 
+  # change this to just use $cust_pay->delete_cust_bill_pay?
   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
     my @cust_bill_pay = $cust_pay->cust_bill_pay;
     last unless @cust_bill_pay;
@@ -1626,11 +1667,12 @@ sub realtime_refund_bop {
   my $cust_refund = new FS::cust_refund ( {
     'custnum'  => $self->custnum,
     'paynum'   => $options{'paynum'},
+    'source_paynum' => $options{'paynum'},
     'refund'   => $amount,
     '_date'    => '',
     'payby'    => $bop_method2payby{$options{method}},
     'payinfo'  => $payinfo,
-    'reason'   => $options{'reason'} || 'card or ACH refund',
+    'reasonnum'     => $options{'reasonnum'},
     'gatewaynum'    => $gatewaynum, # may be null
     'processor'     => $processor,
     'auth'          => $refund->authorization,
@@ -1639,6 +1681,7 @@ sub realtime_refund_bop {
   my $error = $cust_refund->insert;
   if ( $error ) {
     $cust_refund->paynum(''); #try again with no specific paynum
+    $cust_refund->source_paynum('');
     my $error2 = $cust_refund->insert;
     if ( $error2 ) {
       # gah, even with transactions.