merge
authorIvan Kohler <ivan@freeside.biz>
Sun, 18 Aug 2013 05:41:28 +0000 (22:41 -0700)
committerIvan Kohler <ivan@freeside.biz>
Sun, 18 Aug 2013 05:41:28 +0000 (22:41 -0700)
16 files changed:
FS/FS/Schema.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_pay_batch.pm
FS/FS/cust_pay_pending.pm
FS/FS/pay_batch.pm
FS/FS/payinfo_Mixin.pm
bin/part_pkg-bulk_change
httemplate/edit/bulk-part_pkg.html
httemplate/edit/process/bulk-part_pkg.html
httemplate/elements/checkbox-tristate.html
httemplate/search/cust_pay_batch.cgi
httemplate/search/cust_pay_pending.html
httemplate/view/cust_main/payment_history/attempted_batch_payment.html
httemplate/view/cust_main/payment_history/attempted_payment.html
rt/lib/RT/Interface/Web_Vendor.pm
rt/lib/RT/Ticket.pm

index 56cd065..2e9a10a 100644 (file)
@@ -1612,6 +1612,7 @@ sub tables_hashref {
         'invnum',       'int',     'NULL',  '', '', '',
         'manual',       'char',    'NULL',   1, '', '',
         'discount_term','int',     'NULL',  '', '', '',
+        'failure_status','varchar','NULL',  16, '', '',
       ],
       'primary_key' => 'paypendingnum',
       'unique'      => [ [ 'payunique' ] ],
@@ -1776,6 +1777,7 @@ sub tables_hashref {
         'amount',      @money_type,                  '', '', 
         'currency',         'char', 'NULL',       3, '', '',
         'status',        'varchar', 'NULL', $char_d, '', '', 
+        'failure_status','varchar', 'NULL',      16, '', '',
         'error_message', 'varchar', 'NULL', $char_d, '', '',
       ],
       'primary_key' => 'paybatchnum',
index 6e89f71..e5e5291 100644 (file)
@@ -1010,8 +1010,9 @@ sub _realtime_bop_result {
 
   } else {
 
-    my $perror = $payment_gateway->gateway_module. " error: ".
-      $transaction->error_message;
+    my $perror = $transaction->error_message;
+    #$payment_gateway->gateway_module. " error: ".
+    # removed for conciseness
 
     my $jobnum = $cust_pay_pending->jobnum;
     if ( $jobnum ) {
@@ -1109,7 +1110,11 @@ sub _realtime_bop_result {
     }
 
     $cust_pay_pending->status('done');
-    $cust_pay_pending->statustext("declined: $perror");
+    $cust_pay_pending->statustext($perror);
+    #'declined:': no, that's failure_status
+    if ( $transaction->can('failure_status') ) {
+      $cust_pay_pending->failure_status( $transaction->failure_status );
+    }
     my $cpp_done_err = $cust_pay_pending->replace;
     if ( $cpp_done_err ) {
       my $e = "WARNING: $options{method} declined but pending payment not ".
index e1e32d3..b93d816 100644 (file)
@@ -84,6 +84,9 @@ following fields are currently supported:
 
 =item error_message - the error returned by the gateway if any
 
+=item failure_status - the normalized L<Business::BatchPayment> failure 
+status, if any
+
 =back
 
 =head1 METHODS
@@ -340,20 +343,24 @@ sub approve {
   return;
 }
 
-=item decline [ REASON ]
+=item decline [ REASON [ STATUS ] ]
 
 Decline this payment.  This will replace the existing record with the 
 same paybatchnum, set its status to 'Declined', and run collection events
 as appropriate.  This should only be called from the batch import process.
 
 REASON is a string description of the decline reason, defaulting to 
-'Returned payment'.
+'Returned payment', and will go into the "error_message" field.
+
+STATUS is a normalized failure status defined by L<Business::BatchPayment>,
+and will go into the "failure_status" field.
 
 =cut
 
 sub decline {
   my $new = shift;
   my $reason = shift || 'Returned payment';
+  my $failure_status = shift || '';
   #my $conf = new FS::Conf;
 
   my $paybatchnum = $new->paybatchnum;
@@ -390,6 +397,7 @@ sub decline {
   } # !$old->status
   $new->status('Declined');
   $new->error_message($reason);
+  $new->failure_status($failure_status);
   my $error = $new->replace($old);
   if ( $error ) {
     return "error updating status of paybatchnum $paybatchnum: $error\n";
index 8e29f08..8c6ef69 100644 (file)
@@ -124,6 +124,13 @@ Transaction recorded in database
 
 Additional status information.
 
+=item failure_status
+
+One of the standard failure status strings defined in 
+L<Business::OnlinePayment>: "expired", "nsf", "stolen", "pickup", 
+"blacklisted", "declined".  If the transaction status is not "declined", 
+this will be empty.
+
 =item gatewaynum
 
 L<FS::payment_gateway> id.
@@ -215,6 +222,7 @@ sub check {
     || $self->ut_text('status')
     #|| $self->ut_textn('statustext')
     || $self->ut_anything('statustext')
+    || $self->ut_textn('failure_status')
     #|| $self->ut_money('cust_balance')
     || $self->ut_hexn('session_id')
     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
@@ -425,10 +433,11 @@ sub approve {
   '';
 }
 
-=item decline [ STATUSTEXT ]
+=item decline [ STATUSTEXT [ STATUS ] ]
 
 Sets the status of this pending payment to "done" (with statustext
-"declined (manual)" unless otherwise specified).
+"declined (manual)" unless otherwise specified).  The optional STATUS can be
+used to set the failure_status field.
 
 Currently only used when resolving pending payments manually.
 
@@ -437,11 +446,15 @@ Currently only used when resolving pending payments manually.
 sub decline {
   my $self = shift;
   my $statustext = shift || "declined (manual)";
+  my $failure_status = shift || '';
 
   #could send decline email too?  doesn't seem useful in manual resolution
+  # this is also used for thirdparty payment execution failures, but a decline
+  # email isn't useful there either, and will just confuse people.
 
   $self->status('done');
   $self->statustext($statustext);
+  $self->failure_status($failure_status);
   $self->replace;
 }
 
index 3a06914..8c6c368 100644 (file)
@@ -735,7 +735,8 @@ sub import_from_gateway {
           $total += $cust_pay_batch->paid;
         }
         else {
-          $error = $cust_pay_batch->decline($item->error_message);
+          $error = $cust_pay_batch->decline($item->error_message,
+                                            $item->failure_status);
         }
 
         if ( $error ) {        
index 5c4acf7..66c1e59 100644 (file)
@@ -290,6 +290,33 @@ sub payinfo_used {
   return 0;
 }
 
+=item display_status
+
+For transactions that have both 'status' and 'failure_status', shows the
+status in a single, display-friendly string.
+
+=cut
+
+sub display_status {
+  my $self = shift;
+  my %status = (
+    'done'        => 'Approved',
+    'expired'     => 'Card Expired',
+    'stolen'      => 'Lost/Stolen',
+    'pickup'      => 'Pick Up Card',
+    'nsf'         => 'Insufficient Funds',
+    'inactive'    => 'Inactive Account',
+    'blacklisted' => 'Blacklisted',
+    'declined'    => 'Declined',
+    'approved'    => 'Approved',
+  );
+  if ( $self->failure_status ) {
+    return $status{$self->failure_status};
+  } else {
+    return $status{$self->status};
+  }
+}
+
 =back
 
 =head1 BUGS
index 21a6c5a..cb29b18 100755 (executable)
@@ -1,19 +1,22 @@
 #!/usr/bin/perl
 
 use strict;
-use vars qw( $opt_r $opt_o $opt_v $opt_t );
+use vars qw( $opt_r $opt_p $opt_o $opt_v $opt_t );
 use Getopt::Std;
 use FS::UID qw(adminsuidsetup);
 use FS::Record qw(qsearch qsearchs);
 use FS::part_pkg;
 use FS::part_pkg_option;
 
-getopts('ro:v:t:');
+getopts('rp:o:v:t:');
 
 my $user = shift or &usage;
 adminsuidsetup $user;
 
-foreach my $part_pkg ( qsearch('part_pkg', {}) ) {
+my %plan;
+%plan = ( 'plan' => $opt_p ) if $opt_p;
+
+foreach my $part_pkg ( qsearch('part_pkg',\%plan) ) {
   next if ! $part_pkg->freq && $opt_r;
 
   if ( $opt_o ) {
index a1c6f0c..4665c9f 100644 (file)
@@ -6,6 +6,7 @@
 .row0 { background-color: #eeeeee; }
 .row1 { background-color: #ffffff; }
 </STYLE>
+<& /elements/error.html &>
 
 <FORM ACTION="process/bulk-part_pkg.html" METHOD="POST">
 <DIV>
@@ -22,15 +23,12 @@ The following packages will be changed:<BR>
 % foreach my $num (sort keys %report_class) {
   <TR CLASS="row<%$row % 2%>">
     <TD>
-%   if ( defined $initial_state{$num} ) {
-      <& /elements/checkbox.html,
-            field => 'report_option_'.$num,
-            value => 1,
-            curr_value => $initial_state{$num}
-      &>
-%   } else {
+%   if ( $initial_state{$num} == -1 ) {
 %     # needs to be a tristate so that you can say "don't change it"
       <& /elements/checkbox-tristate.html, field => 'report_option_'.$num &>
+%   } else {
+%# for visual consistency
+      <INPUT TYPE="checkbox" CLASS="partial" NAME="report_option_<%$num%>" VALUE="1" <% $initial_state{$num} ? 'CHECKED':'' %>><LABEL />
 %   }
     </TD>
     <TD><% $report_class{$num}->name %></TD>
@@ -64,11 +62,11 @@ foreach my $num (keys %report_class) {
     }
   }
   if ( $yes and $no ) {
-    $initial_state{$num} = undef;
+    $initial_state{$num} = -1;
   } elsif ( $yes ) {
     $initial_state{$num} = 1;
   } elsif ( $no ) {
-    $initial_state{$num} = 0;
+    $initial_state{$num} = '';
   } # else, uh, you didn't provide any pkgparts
 }
 </%init>
index 4775a93..59c914a 100644 (file)
@@ -1,6 +1,6 @@
 % if ( $error ) {
 %  $cgi->param('error', $error);
-<% $cgi->redirect(popurl(3).'/edit/bulk-part_pkg.cgi?', $cgi->query_string) %>
+<% $cgi->redirect(popurl(3).'/edit/bulk-part_pkg.html?'.$cgi->query_string) %>
 % } else {
 <% $cgi->redirect(popurl(3).'/browse/part_pkg.cgi') %>
 % }
@@ -10,21 +10,26 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edi
 my @pkgparts = $cgi->param('pkgpart')
   or die "no package definitions selected";
 
-my %changes;
+my %delete = map { 'report_option_'.($_->num) => 1 }
+  qsearch('part_pkg_report_option', {});
+my %insert;
+
 foreach my $param (grep { /^report_option_\d+$/ } $cgi->param) {
-  if ( length($cgi->param($param)) ) {
-    if ( $cgi->param($param) == 1 ) {
-      $changes{$param} = 1;
-    } else {
-      $changes{$param} = '';
-    }
-  }
+  if ( $cgi->param($param) == 1 ) {
+    $insert{$param} = 1;
+    delete $delete{$param};
+  } elsif ( $cgi->param($param) == -1 ) {
+    # leave it alone
+    delete $delete{$param};
+  } # else it's empty, so leave it on the delete list
 }
 
+
 my $error;
 foreach my $pkgpart (@pkgparts) {
   my $part_pkg = FS::part_pkg->by_key($pkgpart);
-  my %options = ( $part_pkg->options, %changes );
+  my %options = ( $part_pkg->options, %insert );
+  delete $options{$_} foreach keys(%delete);
   $error ||= $part_pkg->replace( options => \%options );
 }
 </%init>
index 4c26ed7..90966a5 100644 (file)
@@ -2,29 +2,54 @@
 A tristate checkbox (with three values: true, false, and null).
 Internally, this creates a checkbox, coupled via javascript to a hidden
 field that actually contains the value.  For now, the only values these
-can have are 1, 0, and empty.  Clicking the checkbox cycles between them.
+can have are 1, -1, and empty.  Clicking the checkbox cycles between them.
+
+For compatibility with regular checkboxes, empty is the false state and 
+-1 is the indeterminate state.
+
+Displaying these is a problem.  "indeterminate" is a standard HTML5 attribute
+but some browsers display it in unhelpful ways (e.g. Firefox slightly grays
+the checkbox, approximately #dddddd), and checkboxes ignore nearly all CSS 
+styling.
 </%doc>
 <%shared>
 my $init = 0;
 </%shared>
 % if ( !$init ) {
 %   $init = 1;
+<STYLE>
+input.partial {
+  position: absolute;
+  opacity: 0;
+  z-index: 1;
+}
+input.partial + label::before {
+  position: relative;
+  content: "\2610";
+}
+input.partial:checked + label::before {
+  content: "\2611";
+}
+input.partial:indeterminate + label::before {
+  content: "\2612";
+}
+</STYLE>
 <SCRIPT TYPE="text/javascript">
 function tristate_onclick() {
   var checkbox = this;
   var input = checkbox.input;
-  if ( input.value == "" ) {
-    input.value = "0";
-    checkbox.checked = false;
-    checkbox.indeterminate = false;
-  } else if ( input.value == "0" ) {
+  if ( input.value == "" ) { // false -> true
     input.value = "1";
     checkbox.checked = true;
     checkbox.indeterminate = false;
-  } else if ( input.value == "1" ) {
+  } else if ( input.value == "1" ) { // true -> indeterminate
+    input.value = "-1";
+    checkbox.checked = false;
+    checkbox.indeterminate = true;
+  } else if ( input.value == "-1" ) { // indeterminate -> false
     input.value = "";
-    checkbox.checked = true;
-    checkbox.indeterminate = true
+    checkbox.checked = false;
+    checkbox.indeterminate = false;
   }
 }
 
@@ -47,10 +72,10 @@ window.onload = function() { // don't do this until all of the checkboxes exist
     // set event handler
     tristate_boxes[i].onclick = tristate_onclick;
     // set initial value
-    if ( tristates[i].value == "" ) {
+    if ( tristates[i].value == "-1" ) {
       tristate_boxes[i].indeterminate = true
     }
-    if ( tristates[i].value != "0" ) {
+    if ( tristates[i].value == "1" ) {
       tristate_boxes[i].checked = true;
     }
   }
@@ -62,6 +87,7 @@ window.onload = function() { // don't do this until all of the checkboxes exist
                      VALUE="<% $curr_value %>"
                      CLASS="tristate">
 <INPUT TYPE="checkbox" ID="checkbox_<%$opt{field}%>" CLASS="partial">
+<LABEL />
 <%init>
 
 my %opt = @_;
@@ -72,7 +98,10 @@ my %opt = @_;
 #                 : '';
 
 $opt{'id'} ||= 'hidden_'.$opt{'field'};
-my $curr_value = $opt{curr_value};
-$curr_value = undef
-  unless $curr_value eq '0' or $curr_value eq '1';
+my $curr_value = '-1';
+if (exists $opt{curr_value}) {
+  $curr_value = $opt{curr_value};
+  $curr_value = '' unless $curr_value eq '-1' or $curr_value eq '1';
+}
+
 </%init>
index 9f9eb30..d5fe52b 100755 (executable)
                                   sub {
                                     sprintf('%.02f', $_[0]->amount)
                                   },
-                                  'status',
+                                  sub { $_[0]->display_status },
                                   'error_message',
                                 ],
-             'align'       => 'rrrlllcrll',
+             'align'       => 'rrrlllcrlll',
              'links'       => [ '',
                                 ["${p}view/cust_bill.cgi?", 'invnum'],
                                 (["${p}view/cust_main.cgi?", 'custnum']) x 2,
index 54c9935..fe82268 100755 (executable)
@@ -28,6 +28,9 @@ my $edit_pending =
 my $status_sub = sub {
   my $pending = shift;
   my $return = $pending->status;
+  if ( $pending->failure_status ) {
+    $return = $pending->display_status;
+  }
   my $action = $statusaction{$pending->status};
   return $return unless $action && $edit_pending;
   my $link = include('/elements/popup_link.html',
index 95947f5..765e542 100644 (file)
@@ -7,7 +7,14 @@ my ($payby,$payinfo) = translate_payinfo($cust_pay_batch);
 $payby = translate_payby($payby,$payinfo);
 my $info = $payby ? "($payby$payinfo)" : '';
 
-$info .= ': '. $cust_pay_batch->error_message
-  if length($cust_pay_batch->error_message);
+my $detail = '';
+if ( $cust_pay_batch->failure_status ) {
+  $detail = $cust_pay_batch->display_status;
+  $detail .= ' ('.$cust_pay_batch->error_message.')' 
+    if $cust_pay_batch->error_message;
+} else {
+  $detail = $cust_pay_batch->error_message;
+}
+$info .= ': '.$detail if length($detail);
 
 </%init>
index f044fc0..63209c7 100644 (file)
@@ -12,7 +12,15 @@ if ( $opt{'pkg-balances'} && $cust_pay_pending->pkgnum ) {
   $info .= ' for '. $cust_pkg->pkg_label_long;
 }
 
-$info .= ': '. $cust_pay_pending->statustext
-  if length($cust_pay_pending->statustext);
+my $detail = '';
+if ( $cust_pay_pending->failure_status ) {
+  $detail = $cust_pay_pending->display_status;
+  $detail .= ' (' . $cust_pay_pending->statustext . ')'
+    if $cust_pay_pending->statustext;
+} else {
+  $detail = $cust_pay_pending->statustext;
+}
+
+$info .= ': '.$detail if length($detail);
 
 </%init>
index 023dede..0c061e2 100644 (file)
@@ -264,10 +264,11 @@ sub ProcessTicketBasics {
       my $DateObj = RT::Date->new($session{'CurrentUser'});
       if ( $to_date ) {
           $DateObj->Set(Format => 'unknown', Value => $to_date);
-      } else {
+          $ARGSRef->{'WillResolve'} = $DateObj->ISO;
+      } elsif ( $TicketObj and $TicketObj->WillResolveObj->Unix > 0 ) {
           $DateObj->Set(Value => 0);
+          $ARGSRef->{'WillResolve'} = $DateObj->ISO;
       }
-      $ARGSRef->{'WillResolve'} = $DateObj->ISO;
     }
 
     if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
index 4da1d48..6165378 100755 (executable)
@@ -255,6 +255,7 @@ sub Create {
         Starts             => undef,
         Started            => undef,
         Resolved           => undef,
+        WillResolve        => undef,
         MIMEObj            => undef,
         _RecordTransaction => 1,
         DryRun             => 0,
@@ -357,6 +358,11 @@ sub Create {
         $Started->Set( Format => 'ISO', Value => $args{'Started'} );
     }
 
+    my $WillResolve = RT::Date->new($self->CurrentUser );
+    if ( defined $args{'WillResolve'} ) {
+        $WillResolve->Set( Format => 'ISO', Value => $args{'WillResolve'} );
+    }
+
     # If the status is not an initial status, set the started date
     elsif ( !$cycle->IsInitial($args{'Status'}) ) {
         $Started->SetToNow;
@@ -483,6 +489,7 @@ sub Create {
         Starts          => $Starts->ISO,
         Started         => $Started->ISO,
         Resolved        => $Resolved->ISO,
+        WillResolve     => $WillResolve->ISO,
         Due             => $Due->ISO
     );