'invnum', 'int', 'NULL', '', '', '',
'manual', 'char', 'NULL', 1, '', '',
'discount_term','int', 'NULL', '', '', '',
+ 'failure_status','varchar','NULL', 16, '', '',
],
'primary_key' => 'paypendingnum',
'unique' => [ [ 'payunique' ] ],
'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',
} 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 ) {
}
$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 ".
=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
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;
} # !$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";
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.
|| $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' )
'';
}
-=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.
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;
}
$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 ) {
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
#!/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 ) {
.row0 { background-color: #eeeeee; }
.row1 { background-color: #ffffff; }
</STYLE>
+<& /elements/error.html &>
<FORM ACTION="process/bulk-part_pkg.html" METHOD="POST">
<DIV>
% 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>
}
}
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>
% 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') %>
% }
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>
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;
}
}
// 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;
}
}
VALUE="<% $curr_value %>"
CLASS="tristate">
<INPUT TYPE="checkbox" ID="checkbox_<%$opt{field}%>" CLASS="partial">
+<LABEL />
<%init>
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>
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,
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',
$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>
$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>
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+)$/ ) ) {
Starts => undef,
Started => undef,
Resolved => undef,
+ WillResolve => undef,
MIMEObj => undef,
_RecordTransaction => 1,
DryRun => 0,
$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;
Starts => $Starts->ISO,
Started => $Started->ISO,
Resolved => $Resolved->ISO,
+ WillResolve => $WillResolve->ISO,
Due => $Due->ISO
);