diff options
| author | ivan <ivan> | 2004-07-06 17:26:02 +0000 | 
|---|---|---|
| committer | ivan <ivan> | 2004-07-06 17:26:02 +0000 | 
| commit | 4ae85517a9c3a8a2f61e87bc27a74eb616e396a4 (patch) | |
| tree | 6f6c57f1719b34a9b59cc25ac1fcc7298d69daa5 | |
| parent | c0c6e34a0818945d86165b47adcddfea91f3f7e4 (diff) | |
payment voiding part deux & credit card refunds!
| -rw-r--r-- | FS/FS/Conf.pm | 9 | ||||
| -rw-r--r-- | FS/FS/cust_main.pm | 255 | ||||
| -rw-r--r-- | FS/FS/cust_pay.pm | 18 | ||||
| -rwxr-xr-x | httemplate/edit/cust_refund.cgi | 93 | ||||
| -rwxr-xr-x | httemplate/edit/process/cust_credit.cgi | 4 | ||||
| -rwxr-xr-x | httemplate/edit/process/cust_refund.cgi | 37 | ||||
| -rwxr-xr-x | httemplate/view/cust_main.cgi | 127 | 
7 files changed, 477 insertions, 66 deletions
| diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index c553f0c22..c4b214845 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -543,7 +543,7 @@ httemplate/docs/config.html    {      'key'         => 'invoice_send_receipts',      'section'     => 'deprecated',q -    'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits.  See the payment_receipt_email and  instead.', +    'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits.  See the payment_receipt_email and XXXX instead.',      'type'        => 'checkbox',    }, @@ -1269,7 +1269,12 @@ httemplate/docs/config.html      'type'        => 'checkbox',    }, - +  { +    'key'         => 'card_refund-days', +    'section'     => 'billing', +    'description' => 'After a payment, the number of days a refund link will be available for that payment.  Defaults to 120.', +    'type'        => 'text', +  },  ); diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 740b483d5..cc9195913 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -21,6 +21,7 @@ use FS::cust_pkg;  use FS::cust_bill;  use FS::cust_bill_pkg;  use FS::cust_pay; +use FS::cust_pay_void;  use FS::cust_credit;  use FS::cust_refund;  use FS::part_referral; @@ -1775,7 +1776,7 @@ sub realtime_bop {    }    my $email = $invoicing_list[0]; -  my %content; +  my %content = ();    if ( $method eq 'CC' ) {       $content{card_number} = $self->payinfo; @@ -1808,8 +1809,7 @@ sub realtime_bop {    my( $action1, $action2 ) = split(/\s*\,\s*/, $action ); -  my $transaction = -    new Business::OnlinePayment( $processor, @bop_options ); +  my $transaction = new Business::OnlinePayment( $processor, @bop_options );    $transaction->content(      'type'           => $method,      'login'          => $login, @@ -1900,6 +1900,11 @@ sub realtime_bop {        'LEC'    => 'LECB',      ); +    my $paybatch = "$processor:". $transaction->authorization; +    $paybatch .= ':'. $transaction->order_number +      if $transaction->can('order_number') +      && length($transaction->order_number); +      my $cust_pay = new FS::cust_pay ( {         'custnum'  => $self->custnum,         'invnum'   => $options{'invnum'}, @@ -1907,7 +1912,7 @@ sub realtime_bop {         '_date'     => '',         'payby'    => $method2payby{$method},         'payinfo'  => $self->payinfo, -       'paybatch' => "$processor:". $transaction->authorization, +       'paybatch' => $paybatch,      } );      my $error = $cust_pay->insert;      if ( $error ) { @@ -1962,6 +1967,235 @@ sub realtime_bop {  } +=item realtime_refund_bop METHOD [ OPTION => VALUE ... ] + +Refunds a realtime credit card, ACH (electronic check) or phone bill transaction +via a Business::OnlinePayment realtime gateway.  See +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> + +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. + +Implementation note: If I<amount> is unspecified or equal to the amount of the +orignal payment, first an attempt is made to "void" the transaction via +the gateway (to cancel a not-yet settled transaction) and then if that fails, +the normal attempt is made to "refund" ("credit") the transaction via the +gateway is attempted. + +#The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>, +#I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options, +#if set, will override the value from the customer record. + +#If an I<invnum> is specified, this payment (if sucessful) is applied to the +#specified invoice.  If you don't specify an I<invnum> you might want to +#call the B<apply_payments> method. + +=cut + +#some false laziness w/realtime_bop, not enough to make it worth merging +#but some useful small subs should be pulled out +sub realtime_refund_bop { +  my( $self, $method, %options ) = @_; +  if ( $DEBUG ) { +    warn "$self $method refund\n"; +    warn "  $_ => $options{$_}\n" foreach keys %options; +  } + +  #pre-requisites +  die "Real-time processing not enabled\n" +    unless $conf->exists('business-onlinepayment'); +  eval "use Business::OnlinePayment";   +  die $@ if $@; + +  ##overrides +  #$self->set( $_ => $options{$_} ) +  #  foreach grep { exists($options{$_}) } +  #          qw( payname address1 address2 city state zip payinfo paydate paycvv); + +  #load up config +  my $bop_config = 'business-onlinepayment'; +  $bop_config .= '-ach' +    if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach'); +  my ( $processor, $login, $password, $unused_action, @bop_options ) = +    $conf->config($bop_config); +  #$action ||= 'normal authorization'; +  pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/; +  die "No real-time processor is enabled - ". +      "did you set the business-onlinepayment configuration value?\n" +    unless $processor; + +  my $cust_pay = ''; +  my $amount = $options{'amount'}; +  my( $pay_processor, $auth, $order_number ); +  if ( $options{'paynum'} ) { +    warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG; +    $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } ) +      or return "Unknown paynum $options{'paynum'}"; +    $amount ||= $cust_pay->paid; +    $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/ +      or return "Can't parse paybatch for paynum $options{'paynum'}: ". +                $cust_pay->paybatch; +    ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 ); +    return "processor of payment $options{'paynum'} $pay_processor does not". +           " match current processor $processor" +      unless $pay_processor eq $processor; +  } +  return "neither amount nor paynum specified" unless $amount; + +  #first try void if applicable +  if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates? +    my $void = new Business::OnlinePayment( $processor, @bop_options ); +    $void->content( +      'type'           => $method, +      'action'         => 'void', +      'login'          => $login, +      'password'       => $password, +      'order_number'   => $order_number, +      'amount'         => $amount, +      'authorization'  => $auth, +      'referer'        => 'http://cleanwhisker.420.am/', +    ); +    $void->submit(); +    if ( $void->is_success ) { +      my $error = $cust_pay->void($options{'reason'}); +      if ( $error ) { +        # gah, even with transactions. +        my $e = 'WARNING: Card/ACH voided but database not updated - '. +                "error voiding payment: $error"; +        warn $e; +        return $e; +      } +      return ''; +    } +  } + +  #massage data +  my $address = $self->address1; +  $address .= ", ". $self->address2 if $self->address2; + +  my($payname, $payfirst, $paylast); +  if ( $self->payname && $method ne 'ECHECK' ) { +    $payname = $self->payname; +    $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ +      or return "Illegal payname $payname"; +    ($payfirst, $paylast) = ($1, $2); +  } else { +    $payfirst = $self->getfield('first'); +    $paylast = $self->getfield('last'); +    $payname =  "$payfirst $paylast"; +  } + +  my %content = (); +  if ( $method eq 'CC' ) {  + +    $content{card_number} = $self->payinfo; +    $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; +    $content{expiration} = "$2/$1"; + +    #$content{cvv2} = $self->paycvv +    #  if defined $self->dbdef_table->column('paycvv') +    #     && length($self->paycvv); + +    #$content{recurring_billing} = 'YES' +    #  if qsearch('cust_pay', { 'custnum' => $self->custnum, +    #                           'payby'   => 'CARD', +    #                           'payinfo' => $self->payinfo, } ); + +  } elsif ( $method eq 'ECHECK' ) { +    my($account_number,$routing_code) = $self->payinfo; +    ( $content{account_number}, $content{routing_code} ) = +      split('@', $self->payinfo); +    $content{bank_name} = $self->payname; +    $content{account_type} = 'CHECKING'; +    $content{account_name} = $payname; +    $content{customer_org} = $self->company ? 'B' : 'I'; +    $content{customer_ssn} = $self->ss; +  } elsif ( $method eq 'LEC' ) { +    $content{phone} = $self->payinfo; +  } + +  #then try refund +  my $refund = new Business::OnlinePayment( $processor, @bop_options ); +  $refund->content( +    'type'           => $method, +    'action'         => 'credit', +    'login'          => $login, +    'password'       => $password, +    'order_number'   => $order_number, +    'amount'         => $amount, +    'authorization'  => $auth, +    'customer_id'    => $self->custnum, +    'last_name'      => $paylast, +    'first_name'     => $payfirst, +    'name'           => $payname, +    'address'        => $address, +    'city'           => $self->city, +    'state'          => $self->state, +    'zip'            => $self->zip, +    'country'        => $self->country, +    'referer'        => 'http://cleanwhisker.420.am/', +    %content, #after +  ); +  $refund->submit(); + +  return "$processor error: ". $refund->error_message +    unless $refund->is_success(); + +  my %method2payby = ( +    'CC'     => 'CARD', +    'ECHECK' => 'CHEK', +    'LEC'    => 'LECB', +  ); + +  my $paybatch = "$processor:". $refund->authorization; +  $paybatch .= ':'. $refund->order_number +    if $refund->can('order_number') && $refund->order_number; + +  while ( $cust_pay && $cust_pay->unappled < $amount ) { +    my @cust_bill_pay = $cust_pay->cust_bill_pay; +    last unless @cust_bill_pay; +    my $cust_bill_pay = pop @cust_bill_pay; +    my $error = $cust_bill_pay->delete; +    last if $error; +  } + +  my $cust_refund = new FS::cust_refund ( { +    'custnum'  => $self->custnum, +    'paynum'   => $options{'paynum'}, +    'refund'   => $amount, +    '_date'    => '', +    'payby'    => $method2payby{$method}, +    'payinfo'  => $self->payinfo, +    'paybatch' => $paybatch, +    'reason'   => $options{'reason'} || 'card refund', +  } ); +  my $error = $cust_refund->insert; +  if ( $error ) { +    $cust_refund->paynum(''); #try again with no specific paynum +    my $error2 = $cust_refund->insert; +    if ( $error2 ) { +      # gah, even with transactions. +      my $e = 'WARNING: Card/ACH refunded but database not updated - '. +              "error inserting refund ($processor): $error2". +              " (previously tried insert with paynum #$options{'paynum'}" . +              ": $error )"; +      warn $e; +      return $e; +    } +  } + +  ''; #no error + +} +  =item total_owed  Returns the total owed for this customer on all invoices @@ -2516,6 +2750,19 @@ sub cust_pay {      qsearch( 'cust_pay', { 'custnum' => $self->custnum } )  } +=item cust_pay_void + +Returns all voided payments (see L<FS::cust_pay_void>) for this customer. + +=cut + +sub cust_pay_void { +  my $self = shift; +  sort { $a->_date <=> $b->_date } +    qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } ) +} + +  =item cust_refund  Returns all the refunds (see L<FS::cust_refund>) for this customer. diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 3317a32bd..42e3e1e1f 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -396,7 +396,8 @@ payment.  sub cust_bill_pay {    my $self = shift; -  sort { $a->_date <=> $b->_date } +  sort {    $a->_date  <=> $b->_date +         || $a->invnum <=> $b->invnum }      qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )    ;  } @@ -432,6 +433,21 @@ sub unapplied {    sprintf("%.2f", $amount );  } +=item unrefunded + +Returns the amount of this payment that has not been refuned; which is +paid minus all  refund applications (see L<FS::cust_pay_refund>). + +=cut + +sub unrefunded { +  my $self = shift; +  my $amount = $self->paid; +  $amount -= $_->amount foreach ( $self->cust_pay_refund ); +  sprintf("%.2f", $amount ); +} + +  =item cust_main  Returns the parent customer object (see L<FS::cust_main>). diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi new file mode 100755 index 000000000..425c17738 --- /dev/null +++ b/httemplate/edit/cust_refund.cgi @@ -0,0 +1,93 @@ +<!-- mason kludge --> +<% + +my $conf = new FS::Conf; +my $custnum = $cgi->param('custnum'); +my $refund  = $cgi->param('refund'); +my $payby   = $cgi->param('payby'); +my $reason  = $cgi->param('reason'); + +my( $paynum, $cust_pay ) = ( '', '' ); +if ( $cgi->param('paynum') =~ /^(\d+)$/ ) { +  $paynum = $1; +  $cust_pay = qsearchs('cust_pay', { paynum=>$paynum } ) +    or die "unknown payment # $paynum"; +  $refund ||= $cust_pay->unrefunded; +  if ( $custnum ) { +    die "payment # $paynum is not for specified customer # $custnum" +      unless $custnum == $cust_pay->custnum; +  } else { +    $custnum = $cust_pay->custnum; +  } +} +die "no custnum or paynum specified!" unless $custnum; + +my $_date = time; + +my $p1 = popurl(1); + +print header('Refund '. ucfirst(lc($payby)). ' payment', ''); +print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'), +      "</FONT>" +  if $cgi->param('error'); +print <<END, small_custview($custnum, $conf->config('countrydefault')); +    <FORM ACTION="${p1}process/cust_refund.cgi" METHOD=POST> +    <INPUT TYPE="hidden" NAME="refundnum" VALUE=""> +    <INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum"> +    <INPUT TYPE="hidden" NAME="paynum" VALUE="$paynum"> +    <INPUT TYPE="hidden" NAME="_date" VALUE="$_date"> +    <INPUT TYPE="hidden" NAME="payby" VALUE="$payby"> +    <INPUT TYPE="hidden" NAME="payinfo" VALUE=""> +    <INPUT TYPE="hidden" NAME="paybatch" VALUE=""> +    <INPUT TYPE="hidden" NAME="credited" VALUE=""> +    <BR> +END + +if ( $cust_pay ) { + +  #false laziness w/FS/FS/cust_pay.pm +  my $payby = $cust_pay->payby; +  my $payinfo = $cust_pay->payinfo; +  $payby =~ s/^BILL$/Check/ if $payinfo; +  $payinfo = $cust_pay->payinfo_masked if $payby eq 'CARD'; + +  print '<BR>Payment'. ntable("#cccccc", 2). +        '<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$'. +          $cust_pay->paid. '</TD></TR>'. +        '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'. +          time2str("%D",$cust_pay->_date). '</TD></TR>'. +        '<TR><TD ALIGN="right">Method</TD><TD BGCOLOR="#ffffff">'. +          ucfirst(lc($payby)). ' # '. $payinfo. '</TD></TR>'; +  #false laziness w/FS/FS/cust_main::realtime_refund_bop +  if ( $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/ ) { +    my ( $processor, $auth, $order_number ) = ( $1, $2, $4 ); +    print '<TR><TD ALIGN="right">Processor</TD><TD BGCOLOR="#ffffff">'. +          $processor. '</TD></TR>'; +    print '<TR><TD ALIGN="right">Authorization</TD><TD BGCOLOR="#ffffff">'. +          $auth. '</TD></TR>' +      if length($auth); +    print '<TR><TD ALIGN="right">Order number</TD><TD BGCOLOR="#ffffff">'. +          $order_number. '</TD></TR>' +      if length($order_number); +  } +  print '</TABLE>'; +} + +print '<BR>Refund'. ntable("#cccccc", 2). +      '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'. +      time2str("%D",$_date). '</TD></TR>'; + +print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="refund" VALUE="$refund" SIZE=8 MAXLENGTH=8></TD></TR>!; + +print qq!<TR><TD ALIGN="right">Reason</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="$reason"></TD></TR>!; + +print <<END; +</TABLE> +<BR> +<INPUT TYPE="submit" VALUE="Post refund"> +    </FORM> +  </BODY> +</HTML> +END + +%> diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi index ac92631f8..85bfd4489 100755 --- a/httemplate/edit/process/cust_credit.cgi +++ b/httemplate/edit/process/cust_credit.cgi @@ -3,12 +3,9 @@  $cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";  my $custnum = $1; -$cgi->param('otaker',getotaker); -  my $new = new FS::cust_credit ( {    map {      $_, scalar($cgi->param($_)); -  #} qw(custnum _date amount otaker reason)    } fields('cust_credit')  } ); @@ -26,5 +23,4 @@ if ( $error ) {    print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");  } -  %> diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi new file mode 100755 index 000000000..fc1635781 --- /dev/null +++ b/httemplate/edit/process/cust_refund.cgi @@ -0,0 +1,37 @@ +<% + +$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!"; +my $custnum = $1; +my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) +  or die "unknown custnum $custnum"; + +my $error = ''; +if ( $cgi->param('payby') eq 'CARD' ) {  +  $cgi->param('refund') =~ /^(\d*)(\.\d{2})?$/ +    or die "illegal refund amount ". $cgi->param('refund'); +  my $refund = "$1$2"; +  $cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!"; +  my $paynum = $1; +  my $reason = $cgi->param('reason'); +  $error = $cust_main->realtime_refund_bop( 'CC', 'amount' => $refund, +                                                  'paynum' => $paynum, +                                                  'reason' => $reason, ); +} else { +  die 'unimplemented'; +  #my $new = new FS::cust_refund ( { +  #  map { +  #    $_, scalar($cgi->param($_)); +  #  } ( fields('cust_refund'), 'paynum' ) +  #} ); +  #$error = $new->insert; +} + + +if ( $error ) { +  $cgi->param('error', $error); +  print $cgi->redirect(popurl(2). "cust_refund.cgi?". $cgi->query_string ); +} else { +  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum"); +} + +%> diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi index 125c51aef..a34ddc429 100755 --- a/httemplate/view/cust_main.cgi +++ b/httemplate/view/cust_main.cgi @@ -36,15 +36,15 @@ print qq!<A HREF="${p}edit/cust_main.cgi?$custnum">Edit this customer</A>!;  %>  <SCRIPT> -function cancel_areyousure(href) { -    if (confirm("Perminantly delete all services and cancel this customer?") == true) +function areyousure(href, message) { +    if (confirm(message) == true)          window.location.href = href;  }  </SCRIPT>  <% -print qq! | <A HREF="javascript:cancel_areyousure('${p}misc/cust_main-cancel.cgi?$custnum')">!. +print qq! | <A HREF="javascript:areyousure('${p}misc/cust_main-cancel.cgi?$custnum', 'Perminantly delete all services and cancel this customer?')">!.        'Cancel this customer</A>'    if $cust_main->ncancelled_pkgs; @@ -349,21 +349,6 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {  } -%> - -<SCRIPT> -function cust_pkg_areyousure(href) { -    if (confirm("Permanently delete included services and cancel this package?") == true) -        window.location.href = href; -} -function svc_areyousure(href) { -    if (confirm("Permanently unprovision and delete this service?") == true) -        window.location.href = href; -} -</SCRIPT> - -<% -  print qq!<A NAME="cust_pkg">Packages</A> !,        qq!( <A HREF="!, popurl(2), qq!edit/cust_pkg.cgi?$custnum">Order and cancel packages</A> (preserves services) )!,  ; @@ -544,29 +529,6 @@ print '</TABLE>';  #end display packages  %> -<SCRIPT> -function cust_pay_areyousure(href) { -    if (confirm("Are you sure you want to delete this payment?") - == true) -        window.location.href = href; -} -function cust_pay_unapply_areyousure(href) { -    if (confirm("Are you sure you want to unapply this payment?") - == true) -        window.location.href = href; -} -function cust_credit_unapply_areyousure(href) { -    if (confirm("Are you sure you want to unapply this credit?") - == true) -        window.location.href = href; -} -function cust_credit_areyousure(href) { -    if (confirm("Are you sure you want to delete this credit?") - == true) -        window.location.href = href; -} -</SCRIPT> -  <% if ( $conf->config('payby-default') ne 'HIDE' ) { %>    <BR><BR><A NAME="history"><FONT SIZE="+2">Payment History</FONT></A><BR> @@ -660,31 +622,77 @@ function cust_credit_areyousure(href) {        }      } +    my $refund = ''; +    my $refund_days = $conf->config('card_refund-days') || 120; +    if (    $cust_pay->closed !~ /^Y/i +         && $cust_pay->payby eq 'CARD'  +         && time-$cust_pay->_date < $refund_days*86400 +         && $cust_pay->unrefunded > 0 +    ) { +      $refund = qq! (<A HREF="!. qq!${p}edit/cust_refund.cgi?payby=CARD;!. +                qq!paynum=!. $cust_pay->paynum. qq!">refund</A>)!; +    } + +    my $void = ''; +    if (    $cust_pay->closed !~ /^Y/i +         && $cust_pay->payby ne 'CARD' +       ) { +      $void = qq! (<A HREF="javascript:areyousure('!. +              qq!${p}misc/void-cust_pay.cgi?!. $cust_pay->paynum. +              qq!', 'Are you sure you want to void this payment?')">!. +              qq!void</A>)!; +    } +      my $delete = '';      if ( $cust_pay->closed !~ /^Y/i && $conf->exists('deletepayments') ) { -      $delete = qq! (<A HREF="javascript:cust_pay_areyousure('!. +      $delete = qq! (<A HREF="javascript:areyousure('!.                  qq!${p}misc/delete-cust_pay.cgi?!. $cust_pay->paynum. -                qq!')">delete</A>)!; +                qq!', 'Are you sure you want to delete this payment?')">!. +                qq!delete</A>)!;      }      my $unapply = '';      if (    $cust_pay->closed !~ /^Y/i           && $conf->exists('unapplypayments')           && scalar(@cust_bill_pay)           ) { -      $unapply = qq! (<A HREF="javascript:cust_pay_unapply_areyousure('!. +      $unapply = qq! (<A HREF="javascript:areyousure('!.                   qq!${p}misc/unapply-cust_pay.cgi?!. $cust_pay->paynum. -                 qq!')">unapply</A>)!; +                 qq!', 'Are you sure you want to unapply this payment?')">!. +                 qq!unapply</A>)!;      }      push @history, {        'date'    => $cust_pay->_date,        'desc'    => $pre. "Payment$post$info$desc". -                   "$apply$delete$unapply", +                   "$apply$refund$void$delete$unapply",        'payment' => $cust_pay->paid,        'target'  => $target,      };    } +  #voided payments +  foreach my $cust_pay_void ($cust_main->cust_pay_void) { + +    my $payby = $cust_pay_void->payby; +    my $payinfo = $payby eq 'CARD' +                    ? $cust_pay_void->payinfo_masked +                    : $cust_pay_void->payinfo; + +    $payby =~ s/^BILL$/Check #/ if $payinfo; +    $payby =~ s/^BILL$//; +    $payby =~ s/^(CARD|COMP)$/$1 /; +    my $info = $payby ? " ($payby$payinfo)" : ''; + +    push @history, { +      'date'   => $cust_pay_void->_date, +      'desc'   => "Payment $info <I>voided ". +                  time2str("%D", $cust_pay_void->void_date). +                  " by ". $cust_pay_void->otaker. '</i>', +      'void_payment' => $cust_pay_void->paid, +    }; +   +  } +    #credits (some false laziness w/payments)    foreach my $cust_credit ($cust_main->cust_credit) { @@ -740,18 +748,20 @@ function cust_credit_areyousure(href) {  #      my $delete = '';      if ( $cust_credit->closed !~ /^Y/i && $conf->exists('deletecredits') ) { -      $delete = qq! (<A HREF="javascript:cust_credit_areyousure('!. +      $delete = qq! (<A HREF="javascript:areyousure('!.                  qq!${p}misc/delete-cust_credit.cgi?!. $cust_credit->crednum. -                qq!')">delete</A>)!; +                qq!', 'Are you sure you want to delete this credit?')">!. +                qq!delete</A>)!;      }      my $unapply = '';      if (    $cust_credit->closed !~ /^Y/i           && $conf->exists('unapplycredits')           && scalar(@cust_credit_bill)       ) { -      $unapply = qq! (<A HREF="javascript:cust_credit_unapply_areyousure('!. +      $unapply = qq! (<A HREF="javascript:areyousure('!.                   qq!${p}misc/unapply-cust_credit.cgi?!. $cust_credit->crednum. -                 qq!')">unapply</A>)!; +                 qq!', 'Are you sure you want to unapply this credit?')">!. +                 qq!unapply</A>)!;      }      push @history, { @@ -809,6 +819,8 @@ function cust_credit_areyousure(href) {      my $payment = exists($item->{'payment'})                      ? sprintf('- $%.2f', $item->{'payment'})                      : ''; +    $payment ||= sprintf('<DEL>- $%.2f</DEL>', $item->{'void_payment'}) +      if exists($item->{'void_payment'});      my $credit  = exists($item->{'credit'})                      ? sprintf('- $%.2f', $item->{'credit'})                      : ''; @@ -976,7 +988,8 @@ sub svc_provision_link {  sub svc_unprovision_link {    my $svc = shift or return ''; -  return qq!<A HREF="javascript:svc_areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}')">Unprovision</A>!; +  qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}',!. +  qq!'Permanently unprovision and delete this service?')">Unprovision</A>!;  }  # This should be generalized to use config options to determine order. @@ -1004,7 +1017,8 @@ sub pkg_datestr {  sub pkg_change_link {    my $pkg = shift or return ''; -  return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">Change package</a>!; +  return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">!. +         qq!Change package</a>!;  }  sub pkg_suspend_link { @@ -1019,19 +1033,22 @@ sub pkg_unsuspend_link {  sub pkg_cancel_link {    my $pkg = shift or return ''; -  qq!<A HREF="javascript:cust_pkg_areyousure('${p}misc/cancel_pkg.cgi?$pkg->{pkgnum}')">Cancel now</A> | !. +  qq!<A HREF="javascript:areyousure('${p}misc/cancel_pkg.cgi?$pkg->{pkgnum}', !. +  qq!'Permanently delete included services and cancel this package?')">!. +  qq!Cancel now</A> | !.    qq!<A HREF="${p}misc/expire_pkg.cgi?$pkg->{pkgnum}">Cancel later</A>!;  }  sub pkg_dates_link {    my $pkg = shift or return ''; -  return qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit dates</A>!; +  qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit dates</A>!;  }  sub pkg_customize_link {    my $pkg = shift or return '';    my $custnum = shift; -  return qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};pkgnum=$pkg->{pkgnum}">Customize</A>!; +  qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};!. +  qq!pkgnum=$pkg->{pkgnum}">Customize</A>!;  }  %> | 
