add reporting on (and resolution of) stuck pending transactions, RT#4837 (RT#3572)
authorivan <ivan>
Mon, 16 Feb 2009 23:56:39 +0000 (23:56 +0000)
committerivan <ivan>
Mon, 16 Feb 2009 23:56:39 +0000 (23:56 +0000)
FS/FS/AccessRight.pm
FS/FS/cust_main.pm
FS/FS/cust_pay_pending.pm
httemplate/edit/cust_pay_pending.html [new file with mode: 0644]
httemplate/edit/process/cust_pay_pending.html [new file with mode: 0644]
httemplate/elements/menu.html
httemplate/search/cust_pay_pending.html [new file with mode: 0755]
httemplate/search/elements/cust_pay_or_refund.html
httemplate/view/cust_main/payment_history.html

index fe10572..93660e2 100644 (file)
@@ -152,6 +152,8 @@ tie my %rights, 'Tie::IxHash',
     'Resend invoices', #NEWNEW
     'View customer tax exemptions', #yow
     'View customer batched payments', #NEW
+    'View customer pending payments', #NEW
+    'Edit customer pending payments', #NEW
     'View customer billing events', #NEW
   ],
   
index 1d234a7..7d68536 100644 (file)
@@ -3691,7 +3691,7 @@ sub realtime_bop {
     'country'        => ( exists($options{'country'})
                             ? $options{'country'}
                             : $self->country          ),
-    'referer'        => 'http://cleanwhisker.420.am/',
+    'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
     'email'          => $email,
     'phone'          => $self->daytime || $self->night,
     %content, #after
@@ -3847,6 +3847,7 @@ sub realtime_bop {
 
     $cust_pay_pending->status('done');
     $cust_pay_pending->statustext('captured');
+    $cust_pay_pending->paynum($cust_pay->paynum);
     my $cpp_done_err = $cust_pay_pending->replace;
 
     if ( $cpp_done_err ) {
@@ -4196,7 +4197,7 @@ sub realtime_refund_bop {
     'password'       => $password,
     'order_number'   => $order_number,
     'amount'         => $amount,
-    'referer'        => 'http://cleanwhisker.420.am/',
+    'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
   );
   $content{authorization} = $auth
     if length($auth); #echeck/ACH transactions have an order # but no auth
@@ -5349,6 +5350,41 @@ sub cust_pay_batch {
     qsearch( 'cust_pay_batch', { 'custnum' => $self->custnum } )
 }
 
+=item cust_pay_pending
+
+Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
+(without status "done").
+
+=cut
+
+sub cust_pay_pending {
+  my $self = shift;
+  return $self->num_cust_pay_pending unless wantarray;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_pay_pending', {
+                                   'custnum' => $self->custnum,
+                                   'status'  => { op=>'!=', value=>'done' },
+                                 },
+           );
+}
+
+=item num_cust_pay_pending
+
+Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
+customer (without status "done").  Also called automatically when the
+cust_pay_pending method is used in a scalar context.
+
+=cut
+
+sub num_cust_pay_pending {
+  my $self = shift;
+  my $sql = " SELECT COUNT(*) FROM cust_pay_pending ".
+            "   WHERE custnum = ? AND status != 'done' ";
+  my $sth = dbh->prepare($sql) or die dbh->errstr;
+  $sth->execute($self->custnum) or die $sth->errstr;
+  $sth->fetchrow_arrayref->[0];
+}
+
 =item cust_refund
 
 Returns all the refunds (see L<FS::cust_refund>) for this customer.
index 7469720..bbabd24 100644 (file)
@@ -3,12 +3,12 @@ package FS::cust_pay_pending;
 use strict;
 use vars qw( @ISA  @encrypted_fields );
 use FS::Record qw( qsearch qsearchs dbh ); #dbh for _upgrade_data
-use FS::payby;
-use FS::payinfo_Mixin;
+use FS::payinfo_transaction_Mixin;
+use FS::cust_main_Mixin;
 use FS::cust_main;
 use FS::cust_pay;
 
-@ISA = qw(FS::Record FS::payinfo_Mixin);
+@ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
 
 @encrypted_fields = ('payinfo');
 
@@ -215,6 +215,83 @@ sub check {
   $self->SUPER::check;
 }
 
+#these two are kind-of false laziness w/cust_main::realtime_bop
+#(currently only used when resolving pending payments manually)
+
+=item insert_cust_pay
+
+Sets the status of this pending pament to "done" (with statustext
+"captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
+
+Currently only used when resolving pending payments manually.
+
+=cut
+
+sub insert_cust_pay {
+  my $self = shift;
+
+  my $cust_pay = new FS::cust_pay ( {
+     'custnum'  => $self->custnum,
+     'paid'     => $self->paid,
+     '_date'    => $self->_date, #better than passing '' for now
+     'payby'    => $self->payby,
+     'payinfo'  => $self->payinfo,
+     'paybatch' => $self->paybatch,
+     'paydate'  => $self->paydate,
+  } );
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  #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 ) {
+    # gah.
+    $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+    return $error;
+  }
+
+  $self->status('done');
+  $self->statustext('captured (manual)');
+  $self->paynum($cust_pay->paynum);
+  my $cpp_done_err = $self->replace;
+
+  if ( $cpp_done_err ) {
+
+    $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+    return $cpp_done_err;
+
+  } else {
+
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return ''; #no error
+
+  }
+
+}
+
+=item decline
+
+Sets the status of this pending pament to "done" (with statustext
+"declined (manual)").
+
+Currently only used when resolving pending payments manually.
+
+=cut
+
+sub decline {
+  my $self = shift;
+
+  #could send decline email too?  doesn't seem useful in manual resolution
+
+  $self->status('done');
+  $self->statustext("declined (manual)");
+  $self->replace;
+}
+
 # _upgrade_data
 #
 # Used by FS::Upgrade to migrate to a new database.
diff --git a/httemplate/edit/cust_pay_pending.html b/httemplate/edit/cust_pay_pending.html
new file mode 100644 (file)
index 0000000..0916a1c
--- /dev/null
@@ -0,0 +1,154 @@
+<% include('/elements/header-popup.html', $title ) %>
+
+% if ( $action eq 'delete' ) {
+
+    <CENTER><FONT SIZE="+1"><B>Are you sure you want to delete this pending payment?</B></FONT></CENTER>
+
+% } elsif ( $action eq 'complete' ) {
+
+    <CENTER><FONT SIZE="+1"><B>No response was received from <% $cust_pay_pending->processor || 'the payment gateway' %> for this transaction.  Check <% $cust_pay_pending->processor || 'the payment gateway' %>'s reporting and determine if this transaction completed successfully.</B></FONT></CENTER>
+
+% }
+
+<BR>
+
+%#false laziness w/view/cust_pay.html
+<% include('/elements/small_custview.html',
+             $cust_pay_pending->custnum,
+             scalar($conf->config('countrydefault')),
+             1, #no balance
+          )
+%>
+<BR>
+
+<% ntable("#cccccc", 2) %>
+
+<TR>
+  <TD ALIGN="right">Pending payment#</TD>
+  <TD BGCOLOR="#FFFFFF"><B><% $cust_pay_pending->paypendingnum %></B></TD>
+</TR>
+
+<TR>
+  <TD ALIGN="right">Date</TD>
+  <TD BGCOLOR="#FFFFFF"><B><% time2str"%a&nbsp;%b&nbsp;%o,&nbsp;%Y&nbsp;%r", $cust_pay_pending->_date %></B></TD>
+</TR>
+
+<TR>
+  <TD ALIGN="right">Amount</TD>
+  <TD BGCOLOR="#FFFFFF"><B><% $money_char. $cust_pay_pending->paid %></B></TD>
+</TR>
+
+<TR>
+  <TD ALIGN="right">Payment method</TD>
+  <TD BGCOLOR="#FFFFFF"><B><% $cust_pay_pending->payby_name %> #<% $cust_pay_pending->paymask %></B></TD>
+</TR>
+
+% #if ( $cust_pay_pending->payby =~ /^(CARD|CHEK|LECB)$/ && $cust_pay_pending->paybatch ) { 
+
+    <TR>
+      <TD ALIGN="right">Processor</TD>
+      <TD BGCOLOR="#FFFFFF"><B><% $cust_pay_pending->processor %></B></TD>
+    </TR>
+
+    <TR>
+      <TD ALIGN="right">Authorization#</TD>
+      <TD BGCOLOR="#FFFFFF"><B><% $cust_pay_pending->authorization %></B></TD>
+    </TR>
+
+%   if ( $cust_pay_pending->order_number ) {
+      <TR>
+        <TD ALIGN="right">Order#</TD>
+        <TD BGCOLOR="#FFFFFF"><B><% $cust_pay_pending->order_number %></B></TD>
+      </TR>
+%   }
+
+% #}
+
+</TABLE>
+
+<BR>
+
+<FORM NAME   = "pendingform"
+      METHOD = "POST"
+      ACTION = "process/cust_pay_pending.html"
+>
+
+<INPUT TYPE="hidden" NAME="paypendingnum" VALUE="<% $paypendingnum %>">
+
+<% itable() %>
+
+% if ( $action eq 'delete' ) {
+
+    <INPUT TYPE="hidden" NAME="action" VALUE="<% $action %>">
+
+    <TR>
+      <TD ALIGN="center">
+        <BUTTON TYPE="button" onClick="document.pendingform.submit();"><!--IMG SRC="<%$p%>images/tick.png" ALT=""-->Yes, delete payment</BUTTON>
+      </TD>
+      <TD>&nbsp;&nbsp;&nbsp;</TD>
+      <TD ALIGN="center">
+        <BUTTON TYPE="button" onClick="parent.cClick();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->No, cancel deletion</BUTTON>
+      </TD>
+    </TR>
+
+% } elsif ( $action eq 'complete' ) {
+
+    <INPUT TYPE="hidden" NAME="action" VALUE="">
+
+    <TR>
+      <TD ALIGN="center">
+        <BUTTON TYPE="button" onClick="document.pendingform.action.value = 'insert_cust_pay'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/tick.png" ALT=""-->Yes, transaction completed sucessfully.</BUTTON>
+      </TD>
+      <TD>&nbsp;&nbsp;&nbsp;</TD>
+      <TD ALIGN="center">
+        <BUTTON TYPE="button" onClick="document.pendingform.action.value = 'decline'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->No, transaction was declined</BUTTON>
+      </TD>
+      <TD>&nbsp;&nbsp;&nbsp;</TD>
+      <TD ALIGN="center">
+        <BUTTON TYPE="button" onClick="document.pendingform.action.value = 'delete'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->No, transaction was not received</BUTTON>
+      </TD>
+    </TR>
+
+    <TR><TD COLSPAN=5></TD></TR>
+
+    <TR>
+      <TD COLSPAN=5 ALIGN="center">
+        <BUTTON TYPE="button" onClick="parent.cClick();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->Cancel payment completion; transaction status not yet known</BUTTON>
+      </TD>
+    </TR>
+
+% }
+
+</TABLE>
+
+</FORM>
+</BODY>
+</HTML>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Edit customer pending payments');
+
+$cgi->param('action') =~ /^(\w+)$/ or die 'illegal action';
+my $action = $1;
+my $title = ucfirst($action). ' pending payment';
+
+$cgi->param('paypendingnum') =~ /^(\d+)$/ or die 'illegal paypendingnum';
+my $paypendingnum = $1;
+my $cust_pay_pending =
+  qsearchs({
+    'select'    => 'cust_pay_pending.*',
+    'table'     => 'cust_pay_pending',
+    'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+    'hashref'   => { 'paypendingnum' => $paypendingnum },
+    'extra_sql' => ' AND '. $curuser->agentnums_sql,
+  })
+  or die 'unknown paypendingnum';
+
+my $conf = new FS::Conf;
+
+my $money_char = $conf->config('money_char') || '$';
+
+</%init>
diff --git a/httemplate/edit/process/cust_pay_pending.html b/httemplate/edit/process/cust_pay_pending.html
new file mode 100644 (file)
index 0000000..1bad6cf
--- /dev/null
@@ -0,0 +1,68 @@
+<% include('/elements/header-popup.html', $title ) %>
+% if ( $error ) {
+  <FONT SIZE="+1" COLOR="#ff0000">Error: <% $error |h %></FONT>
+% } else {
+    <SCRIPT TYPE="text/javascript">
+      window.top.location.reload();
+    </SCRIPT>
+% }
+</BODY>
+</HTML>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Edit customer pending payments');
+
+$cgi->param('action') =~ /^(\w+)$/ or die 'illegal action';
+my $action = $1;
+
+$cgi->param('paypendingnum') =~ /^(\d+)$/ or die 'illegal paypendingnum';
+my $paypendingnum = $1;
+my $cust_pay_pending =
+  qsearchs({
+    'select'    => 'cust_pay_pending.*',
+    'table'     => 'cust_pay_pending',
+    'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+    'hashref'   => { 'paypendingnum' => $paypendingnum },
+    'extra_sql' => ' AND '. $curuser->agentnums_sql,
+  })
+  or die 'unknown paypendingnum';
+
+my $error;
+my $title;
+if ( $action eq 'delete' ) {
+
+  $error = $cust_pay_pending->delete;
+  if ( $error ) {
+    $title = 'Error deleting pending payment';
+  } else {
+    $title = 'Pending payment deletion sucessful';
+  }
+
+} elsif ( $action eq 'insert_cust_pay' ) { 
+
+  $error = $cust_pay_pending->insert_cust_pay;
+  if ( $error ) {
+    $title = 'Error completing pending payment';
+  } else {
+    $title = 'Pending payment completed';
+  }
+
+} elsif ( $action eq 'decline' ) {
+
+  $error = $cust_pay_pending->decline;
+  if ( $error ) {
+    $title = 'Error declining pending payment';
+  } else {
+    $title = 'Pending payment completed (decline)';
+  }
+
+} else {
+
+  die "unknown action $action";
+
+}
+
+</%init> 
index ae5ff58..627f9c8 100644 (file)
@@ -191,6 +191,8 @@ tie my %report_financial, 'Tie::IxHash',
   'Credit Report' => [ $fsurl.'search/report_cust_credit.html', 'Credit report (by employee and/or date range)' ],
   'Payment Report' => [ $fsurl.'search/report_cust_pay.html', 'Payment report (by type and/or date range)' ],
 ;
+$report_financial{'Pending Payment Report'} = [ $fsurl.'search/cust_pay_pending.html?magic=_date;statusNOT=done', 'Pending real-time payments' ]
+  if $curuser->access_right('View customer pending payments');
 $report_financial{'Payment Batch Report'} = [ $fsurl.'search/pay_batch.html', 'Payment batches (by status and/or date range)' ]
   if $conf->exists('batch-enable') || $conf->config('batch-enable_payby');
 $report_financial{'A/R Aging'} = [ $fsurl.'search/report_receivables.html', 'Accounts Receivable Aging report' ];
diff --git a/httemplate/search/cust_pay_pending.html b/httemplate/search/cust_pay_pending.html
new file mode 100755 (executable)
index 0000000..f0a3a01
--- /dev/null
@@ -0,0 +1,47 @@
+<% include( 'elements/cust_pay_or_refund.html',
+                'thing'         => 'pay_pending',
+                'amount_field'  => 'paid',
+                'name_singular' => 'pending payment',
+                'name_verb'     => 'pending',
+                'disable_link'  => 1,
+                'disable_by'    => 1, #add otaker to cust_pay_pending?
+                'html_init'     => include('/elements/init_overlib.html'),
+                'addl_header'   => [ 'Time', 'Payment Status', ],
+                'addl_fields'   => [ sub { time2str('%r', shift->_date ) },
+                                     $status_sub,
+                                   ],
+          )
+%>
+<%init>
+
+my %statusaction = (
+  'new'        => 'delete',
+  'pending'    => 'complete',
+  #'authorized' => '',
+  #'captured'   => '',
+  #'declined'   => '',
+  #wouldn't need to take action on a done state#'done'
+);
+
+my $edit_pending =
+  $FS::CurrentUser::CurrentUser->access_right('Edit customer pending payments');
+
+my $status_sub = sub {
+  my $pending = shift;
+  my $return = $pending->status;
+  my $action = $statusaction{$pending->status};
+  return $return unless $action && $edit_pending;
+  my $link = include('/elements/popup_link.html',
+                       'action' => $p. 'edit/cust_pay_pending.html'.
+                                     '?paypendingnum='. $pending->paypendingnum.
+                                     ";action=$action",
+                       'label'  => $action,
+                       'color'  => '#ff0000',
+                       'width'  => 655,
+                       'height' => ( $action eq 'delete' ? 480 : 575 ),
+                       'actionlabel' => ucfirst($action). ' pending payment',
+                    );
+  $return. qq! <FONT SIZE="-1">($link)</FONT>!;
+};
+
+</%init>
index 9457ae2..9e2eceb 100755 (executable)
@@ -16,6 +16,18 @@ Examples:
                'name_verb'     => 'refunded',
          )
 
+  include( 'elements/cust_pay_or_refund.html',
+               'thing'         => 'pay_pending',
+               'amount_field'  => 'paid',
+               'name_singular' => 'pending payment',
+               'name_verb'     => 'pending',
+               'disable_link'  => 1,
+               'disable_by'    => 1,
+               'html_init'     => '',
+               'addl_header'   => [],
+               'addl_fields'   => [],
+          )
+
 </%doc>
 <% include( 'search.html',
                 'title'         => $title,
@@ -26,27 +38,25 @@ Examples:
                 'header'        => [ "\u$name_singular",
                                      'Amount',
                                      'Date',
-                                     'By',
+                                     @header,
                                      FS::UI::Web::cust_header(),
                                    ],
                 'fields'      => [
                   'payby_payinfo_pretty',
                   sub { sprintf('$%.2f', shift->$amount_field() ) },
                   sub { time2str('%b %d %Y', shift->_date ) },
-                  sub { my $o = shift->otaker;
-                        $o = 'auto billing'          if $o eq 'fs_daily';
-                        $o = 'customer self-service' if $o eq 'fs_selfservice';
-                        $o;
-                      },
+                  @fields,
                   \&FS::UI::Web::cust_fields,
                 ],
                 #'align' => 'lrrrll',
-                'align' => 'rrrc'.FS::UI::Web::cust_aligns(),
+                'align' => 'rrr'.
+                           join('', map 'c', @fields ).
+                           FS::UI::Web::cust_aligns(),
                 'links' => [
                   $link,
                   $link,
                   $link,
-                  '',
+                  ( map '', @fields ),
                   ( map { $_ ne 'Cust. Status' ? $cust_link : '' }
                         FS::UI::Web::cust_header()
                   ),
@@ -55,14 +65,14 @@ Examples:
                              '',
                              '',
                              '',
-                             '',
+                             ( map '', @fields ),
                              FS::UI::Web::cust_colors(),
                            ],
                 'style' => [ 
                              '',
                              '',
                              '',
-                             '',
+                             ( map '', @fields ),
                              FS::UI::Web::cust_styles(),
                            ],
           )
@@ -71,14 +81,35 @@ Examples:
 
 my %opt = @_;
 
+my $curuser = $FS::CurrentUser::CurrentUser;
+
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+  unless $curuser->access_right('Financial reports');
 
 my $thing = $opt{'thing'};
 my $amount_field = $opt{'amount_field'};
 my $name_singular = $opt{'name_singular'};
 
 my $title = "\u$name_singular Search Results";
+
+my @header = ();
+my @fields = ();
+unless ( $opt{'disable_by'} ) {
+  push @header, 'By';
+  push @fields, sub {
+                  sub { my $o = shift->otaker;
+                        $o = 'auto billing'          if $o eq 'fs_daily';
+                        $o = 'customer self-service' if $o eq 'fs_selfservice';
+                        $o;
+                      },
+  };
+}
+
+push @header, @{ $opt{'addl_header'} }
+  if $opt{'addl_header'};
+push @fields, @{ $opt{'addl_fields'} }
+  if $opt{'addl_fields'};
+
 my( $count_query, $sql_query );
 if ( $cgi->param('magic') ) {
 
@@ -93,7 +124,11 @@ if ( $cgi->param('magic') ) {
       die "unknown agentnum $1" unless $agent;
       $title = $agent->agent. " $title";
     }
-  
+
+    if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+      push @search, "custnum = $1";
+    }
+
     if ( $cgi->param('payby') ) {
       $cgi->param('payby') =~
         /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/
@@ -175,6 +210,11 @@ if ( $cgi->param('magic') ) {
       push @search, "cust_$thing.payinfo = '$1'";
     }
 
+    #for cust_pay_pending...  statusNOT=done
+    if ( $cgi->param('statusNOT') =~ /^(\w+)$/ ) {
+      push @search, "status != '$1'";
+    }
+
     my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
     push @search, "_date >= $beginning ",
                   "_date <= $ending";
@@ -197,7 +237,7 @@ if ( $cgi->param('magic') ) {
   }
 
   #here is the agent virtualization
-  push @search, $FS::CurrentUser::CurrentUser->agentnums_sql;
+  push @search, $curuser->agentnums_sql;
 
   my $search = ' WHERE '. join(' AND ', @search);
 
@@ -229,21 +269,25 @@ if ( $cgi->param('magic') ) {
 
   $count_query = "SELECT COUNT(*), SUM($amount_field) FROM cust_$thing".
                  "  WHERE payinfo = '$payinfo' AND payby = '$payby'".
-                 "  AND ". $FS::CurrentUser::CurrentUser->agentnums_sql;
+                 "  AND ". $curuser->agentnums_sql;
 
   $sql_query = {
     'table'     => "cust_$thing",
     'hashref'   => { 'payinfo' => $payinfo,
                      'payby'   => $payby    },
-    'extra_sql' => $FS::CurrentUser::CurrentUser->agentnums_sql.
+    'extra_sql' => $curuser->agentnums_sql.
                    " ORDER BY _date",
   };
 
 }
 
 my $link = '';
-if ( $FS::CurrentUser::CurrentUser->access_right('View invoices') #XXX for now
-     || $FS::CurrentUser::CurrentUser->access_right('View customer payments') ){
+if (    ( $curuser->access_right('View invoices') #XXX for now
+          || $curuser->access_right('View customer payments')
+        )
+     && ! $opt{'disable_link'}
+   )
+{
   $link = [ "${p}view/cust_$thing.html?${thing}num=", $thing.'num' ]
 }
 
index 2ab5339..335ce24 100644 (file)
 
 % if ( ( $conf->exists('batch-enable') || $conf->config('batch-enable_payby') )
 %      && $curuser->access_right('View customer batched payments')
-%    ) { 
-  View batched payments:
+%    )
+% { 
+    View batched payments:
 %   foreach my $status (qw( Queued In-transit Complete All )) {
       <A HREF="<% $p %>search/cust_pay_batch.cgi?status=<% $status{$status} %>;custnum=<% $custnum %>"><% $status %></A> 
       <% $status ne 'All' ? '|' : '' %>
 %   }
-  <BR>
+    <BR>
 % } 
 
+%# pending payment links
+
+% if ( $curuser->access_right('View customer pending payments')
+%      && scalar($cust_main->cust_pay_pending)
+%    )
+% {
+    <A HREF="<% $p %>search/cust_pay_pending.html?magic=_date;statusNOT=done;custnum=<% $custnum %>">View pending payments</A><BR>
+% }
+
 %# and now the table
 
 <% include("/elements/table-grid.html") %>