RT# 78547 Allow for simulated billing within a transaction
authorMitch Jackson <mitch@freeside.biz>
Tue, 11 Sep 2018 07:23:52 +0000 (03:23 -0400)
committerMitch Jackson <mitch@freeside.biz>
Wed, 19 Sep 2018 16:02:19 +0000 (12:02 -0400)
FS/FS/Misc/Savepoint.pm
FS/FS/UID.pm
FS/FS/cust_bill.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/part_export/nena2.pm

index b15b36d..f8e2c5f 100644 (file)
@@ -55,7 +55,7 @@ Savepoints cannot work while AutoCommit is enabled.
 Savepoint labels must be valid sql identifiers.  If your choice of label
 would not make a valid column name, it probably will not make a valid label.
 
-Savepint labels must be unique within the transaction.
+Savepoint labels must be unique within the transaction.
 
 =cut
 
index 50a9178..693e5d9 100644 (file)
@@ -5,7 +5,7 @@ use strict;
 use vars qw(
   @EXPORT_OK $DEBUG $me $cgi $freeside_uid $conf_dir $cache_dir
   $secrets $datasrc $db_user $db_pass $schema $dbh $driver_name
-  $AutoCommit %callback @callback $callback_hack
+  $AutoCommit $ForceObeyAutoCommit %callback @callback $callback_hack
 );
 use subs qw( getsecrets );
 use Carp qw( carp croak cluck confess );
@@ -26,7 +26,17 @@ $freeside_uid = scalar(getpwnam('freeside'));
 $conf_dir  = "%%%FREESIDE_CONF%%%";
 $cache_dir = "%%%FREESIDE_CACHE%%%";
 
+# Code wanting to issue a COMMIT statement to the database is expected to
+# obey the convention of checking this flag first.  Setting $AutoCommit = 0
+# should (usually) suppress COMMIT statements.
 $AutoCommit = 1; #ours, not DBI
+
+# Not all methods obey $AutoCommit, by design choice.  Setting
+# $ForceObeyAutoCommit = 1 will override that design choice for:
+#   &FS::cust_main::Billing::collect
+#   &FS::cust_main::Billing::do_cust_event
+$ForceObeyAutoCommit = 0;
+
 $callback_hack = 0;
 
 =head1 NAME
index dc63fdb..84487d2 100644 (file)
@@ -41,6 +41,7 @@ use FS::cust_bill_void;
 use FS::reason;
 use FS::reason_type;
 use FS::L10N;
+use FS::Misc::Savepoint;
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
@@ -971,6 +972,9 @@ sub apply_payments_and_credits {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  my $savepoint_label = 'cust_bill__apply_payments_and_credits';
+  savepoint_create( $savepoint_label );
+
   $self->select_for_update; #mutex
 
   my @payments = grep { $_->unapplied > 0 } 
@@ -1059,6 +1063,7 @@ sub apply_payments_and_credits {
 
     my $error = $app->insert(%options);
     if ( $error ) {
+      savepoint_rollback_and_release( $savepoint_label );
       $dbh->rollback if $oldAutoCommit;
       return "Error inserting ". $app->table. " record: $error";
     }
@@ -1066,6 +1071,7 @@ sub apply_payments_and_credits {
 
   }
 
+  savepoint_release( $savepoint_label );
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
index 64b002b..aa8ae2a 100644 (file)
@@ -79,6 +79,7 @@ use FS::sales;
 use FS::cust_payby;
 use FS::contact;
 use FS::reason;
+use FS::Misc::Savepoint;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -2441,11 +2442,15 @@ sub cancel_pkgs {
   my( $self, %opt ) = @_;
 
   # we're going to cancel services, which is not reversible
+  #   unless exports are suppressed
   die "cancel_pkgs cannot be run inside a transaction"
-    if $FS::UID::AutoCommit == 0;
+    if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack;
 
+  my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
 
+  savepoint_create('cancel_pkgs');
+
   return ( 'access denied' )
     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
 
@@ -2462,7 +2467,8 @@ sub cancel_pkgs {
       my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
       my $error = $ban->insert;
       if ($error) {
-        dbh->rollback;
+        savepoint_rollback_and_release('cancel_pkgs');
+        dbh->rollback if $oldAutoCommit;
         return ( $error );
       }
 
@@ -2482,13 +2488,14 @@ sub cancel_pkgs {
                              'time'     => $cancel_time );
     if ($error) {
       warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
-      dbh->rollback;
+      savepoint_rollback_and_release('cancel_pkgs');
+      dbh->rollback if $oldAutoCommit;
       return ( "Error billing during cancellation: $error" );
     }
   }
-  dbh->commit;
+  savepoint_release('cancel_pkgs');
+  dbh->commit if $oldAutoCommit;
 
-  $FS::UID::AutoCommit = 1;
   my @errors;
   # now cancel all services, the same way we would for individual packages.
   # if any of them fail, cancel the rest anyway.
@@ -2501,11 +2508,23 @@ sub cancel_pkgs {
   warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
     $self->custnum."\n"
     if $DEBUG;
+  my $i = 0;
   foreach my $cust_svc (@sorted_cust_svc) {
+    my $savepoint = 'cancel_pkgs_'.$i++;
+    savepoint_create( $savepoint );
     my $part_svc = $cust_svc->part_svc;
     next if ( defined($part_svc) and $part_svc->preserve );
-    my $error = $cust_svc->cancel; # immediate cancel, no date option
-    push @errors, $error if $error;
+    # immediate cancel, no date option
+    # transactionize individually
+    my $error = try { $cust_svc->cancel } catch { $_ };
+    if ( $error ) {
+      savepoint_rollback_and_release( $savepoint );
+      dbh->rollback if $oldAutoCommit;
+      push @errors, $error;
+    } else {
+      savepoint_release( $savepoint );
+      dbh->commit if $oldAutoCommit;
+    }
   }
   if (@errors) {
     return @errors;
@@ -2520,8 +2539,11 @@ sub cancel_pkgs {
     @cprs = @{ delete $opt{'cust_pkg_reason'} };
   }
   my $null_reason;
+  $i = 0;
   foreach (@pkgs) {
     my %lopt = %opt;
+    my $savepoint = 'cancel_pkgs_'.$i++;
+    savepoint_create( $savepoint );
     if (@cprs) {
       my $cpr = shift @cprs;
       if ( $cpr ) {
@@ -2541,7 +2563,14 @@ sub cancel_pkgs {
       }
     }
     my $error = $_->cancel(%lopt);
-    push @errors, 'pkgnum '.$_->pkgnum.': '.$error if $error;
+    if ( $error ) {
+      savepoint_rollback_and_release( $savepoint );
+      dbh->rollback if $oldAutoCommit;
+      push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
+    } else {
+      savepoint_release( $savepoint );
+      dbh->commit if $oldAutoCommit;
+    }
   }
 
   return @errors;
index 71d5c9b..1be7d39 100644 (file)
@@ -26,6 +26,7 @@ use FS::pkg_category;
 use FS::FeeOrigin_Mixin;
 use FS::Log;
 use FS::TaxEngine;
+use FS::Misc::Savepoint;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -1753,7 +1754,10 @@ sub collect {
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   #never want to roll back an event just because it returned an error
-  local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+  # unless $FS::UID::ForceObeyAutoCommit is set
+  local $FS::UID::AutoCommit = 1
+    unless !$oldAutoCommit
+        && $FS::UID::ForceObeyAutoCommit;
 
   $self->do_cust_event(
     'debug'      => ( $options{'debug'} || 0 ),
@@ -1961,9 +1965,13 @@ sub do_cust_event {
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
   #never want to roll back an event just because it or a different one
   # returned an error
-  local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+  # unless $FS::UID::ForceObeyAutoCommit is set
+  local $FS::UID::AutoCommit = 1
+    unless !$oldAutoCommit
+        && $FS::UID::ForceObeyAutoCommit;
 
   foreach my $cust_event ( @$due_cust_event ) {
 
@@ -2288,16 +2296,21 @@ sub apply_payments_and_credits {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  my $savepoint_label = 'Billing__apply_payments_and_credits';
+  savepoint_create( $savepoint_label );
+
   $self->select_for_update; #mutex
 
   foreach my $cust_bill ( $self->open_cust_bill ) {
     my $error = $cust_bill->apply_payments_and_credits(%options);
     if ( $error ) {
+      savepoint_rollback_and_release( $savepoint_label );
       $dbh->rollback if $oldAutoCommit;
       return "Error applying: $error";
     }
   }
 
+  savepoint_release( $savepoint_label );
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
index 70dc9d1..64b551c 100644 (file)
@@ -16,6 +16,7 @@ use FS::cust_bill_pay;
 use FS::cust_refund;
 use FS::banned_pay;
 use FS::payment_gateway;
+use FS::Misc::Savepoint;
 
 $realtime_bop_decline_quiet = 0;
 
@@ -27,6 +28,7 @@ $me = '[FS::cust_main::Billing_Realtime]';
 
 our $BOP_TESTING = 0;
 our $BOP_TESTING_SUCCESS = 1;
+our $BOP_TESTING_TIMESTAMP = '';
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -405,7 +407,7 @@ sub realtime_bop {
 
   confess "Can't call realtime_bop within another transaction ".
           '($FS::UID::AutoCommit is false)'
-    unless $FS::UID::AutoCommit;
+    unless $FS::UID::AutoCommit || $BOP_TESTING;
 
   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
 
@@ -682,7 +684,7 @@ sub realtime_bop {
   my $cust_pay_pending = new FS::cust_pay_pending {
     'custnum'           => $self->custnum,
     'paid'              => $options{amount},
-    '_date'             => '',
+    '_date'             => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
     'payby'             => $bop_method2payby{$options{method}},
     'payinfo'           => $options{payinfo},
     'paymask'           => $options{paymask},
@@ -757,7 +759,7 @@ sub realtime_bop {
     return { reference => $cust_pay_pending->paypendingnum,
              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
 
-  } elsif ( $transaction->is_success() && $action2 ) {
+  } elsif ( !$BOP_TESTING && $transaction->is_success() && $action2 ) {
 
     $cust_pay_pending->status('authorized');
     my $cpp_authorized_err = $cust_pay_pending->replace;
@@ -946,7 +948,7 @@ sub _realtime_bop_result {
        'custnum'  => $self->custnum,
        'invnum'   => $options{'invnum'},
        'paid'     => $cust_pay_pending->paid,
-       '_date'    => '',
+       '_date'    => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
        'payby'    => $cust_pay_pending->payby,
        'payinfo'  => $options{'payinfo'},
        'paymask'  => $options{'paymask'} || $cust_pay_pending->paymask,
@@ -967,12 +969,16 @@ sub _realtime_bop_result {
     local $FS::UID::AutoCommit = 0;
     my $dbh = dbh;
 
+    my $savepoint_label = '_realtime_bop_result';
+    savepoint_create( $savepoint_label );
+
     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
 
     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
 
     if ( $error ) {
-      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      savepoint_rollback( $savepoint_label );
+
       $cust_pay->invnum(''); #try again with no specific invnum
       $cust_pay->paynum('');
       my $error2 = $cust_pay->insert( $options{'manual'} ?
@@ -981,7 +987,8 @@ sub _realtime_bop_result {
       if ( $error2 ) {
         # gah.  but at least we have a record of the state we had to abort in
         # from cust_pay_pending now.
-        $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+        savepoint_rollback_and_release( $savepoint_label );
+
         my $e = "WARNING: $options{method} captured but payment not recorded -".
                 " error inserting payment (". $payment_gateway->gateway_module.
                 "): $error2".
@@ -996,9 +1003,10 @@ sub _realtime_bop_result {
     my $jobnum = $cust_pay_pending->jobnum;
     if ( $jobnum ) {
        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
-      
+
        unless ( $placeholder ) {
-         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+         savepoint_rollback_and_release( $savepoint_label );
+
          my $e = "WARNING: $options{method} captured but job $jobnum not ".
              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
          warn $e;
@@ -1008,7 +1016,8 @@ sub _realtime_bop_result {
        $error = $placeholder->delete;
 
        if ( $error ) {
-         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+        savepoint_rollback_and_release( $savepoint_label );
+
          my $e = "WARNING: $options{method} captured but could not delete ".
               "job $jobnum for paypendingnum ".
               $cust_pay_pending->paypendingnum. ": $error\n";
@@ -1030,8 +1039,8 @@ sub _realtime_bop_result {
     my $cpp_done_err = $cust_pay_pending->replace;
 
     if ( $cpp_done_err ) {
+      savepoint_rollback_and_release( $savepoint_label );
 
-      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
       my $e = "WARNING: $options{method} captured but payment not recorded - ".
               "error updating status for paypendingnum ".
               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
@@ -1039,7 +1048,7 @@ sub _realtime_bop_result {
       return $e;
 
     } else {
-
+      savepoint_release( $savepoint_label );
       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
       if ( $options{'apply'} ) {
index f6a730e..cc4069c 100644 (file)
@@ -10,6 +10,7 @@ use Date::Format qw(time2str);
 use Parse::FixedLength;
 use File::Temp qw(tempfile);
 use vars qw(%info %options $initial_load_hack $DEBUG);
+use Carp qw( carp );
 
 my %upload_targets;
 
@@ -396,6 +397,13 @@ sub process {
   my $self = shift;
   my $batch = shift;
   local $DEBUG = $self->option('debug');
+
+  if ( $FS::svc_Common::noexport_hack ) {
+    carp 'FS::part_export::nena2::process() suppressed by noexport_hack'
+      if $DEBUG;
+    return;
+  }
+
   local $FS::UID::AutoCommit = 0;
   my $error;