);
map { $_->gatewaynum, $_->label } @gateways;
},
+ 'per_agent' => 1,
);
my %invoice_mode_options = (
'type' => 'checkbox',
},
+ {
+ 'key' => 'invoice_omit_due_date',
+ 'section' => 'invoice_balances',
+ 'description' => 'Omit the "Please pay by (date)" from invoices.',
+ 'type' => 'checkbox',
+ 'per_agent' => 1,
+ },
+
{
'key' => 'invoice_sections',
'section' => 'invoicing',
{
'key' => 'selfservice-payment_gateway',
- 'section' => 'self-service',
- 'description' => 'Force the use of this payment gateway for self-service.',
+ 'section' => 'deprecated',
+ 'description' => '(no longer supported) Force the use of this payment gateway for self-service.',
%payment_gateway_options,
},
],
},
+ {
+ 'key' => 'selfservice-password_reset_hours',
+ 'section' => 'self-service',
+ 'description' => 'Numbers of hours an email password reset is valid. Defaults to 24.',
+ 'type' => 'text',
+ },
+
{
'key' => 'selfservice-password_reset_msgnum',
'section' => 'self-service',
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
- return '' if $payinfo =~ /^99\d{14}$/; #token
+ return '' if $self->tokenized($payinfo); #token
my %bop_card_types = map { $_=>1 } values %{ card_types() };
my $cardtype = cardtype($payinfo);
CHEK only
+ =item saved_cust_payby
+
+ scalar reference, for returning saved object
+
=back
=cut
return $error;
}
+ ${$opt{'saved_cust_payby'}} = $new
+ if $opt{'saved_cust_payby'};
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
$class->_upgrade_otaker(%opts);
+ # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
+ # existing records will be encrypted in queueable_upgrade (below)
+ unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
+ eval "use FS::Setup";
+ die $@ if $@;
+ FS::Setup::enable_encryption();
+ }
+
}
sub queueable_upgrade {
my $class = shift;
+
+ ### encryption gets turned on in _upgrade_data, above
+
+ eval "use FS::upgrade_journal";
+ die $@ if $@;
+
+ # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted,
+ # clear that out before encrypting/tokenizing anything else
+ if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
+ foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+ my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ }
+ FS::upgrade_journal->set_done('clear_payinfo_history');
+ }
+
+ # encrypt old records
+ if ($conf->exists('encryption') && !FS::upgrade_journal->is_done('encryption_check')) {
+
+ # allow replacement of closed cust_pay/cust_refund records
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+
+ # because it looks like nothing's changing
+ local $FS::Record::no_update_diff = 1;
+
+ # commit everything immediately
+ local $FS::UID::AutoCommit = 1;
+
+ # encrypt what's there
+ foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+ my $tclass = 'FS::'.$table;
+ my $lastrecnum = 0;
+ my @recnums = ();
+ while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) {
+ my $record = $tclass->by_key($recnum);
+ next unless $record; # small chance it's been deleted, that's ok
+ next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+ # window for possible conflict is practically nonexistant,
+ # but just in case...
+ $record = $record->select_for_update;
+ my $error = $record->replace;
+ die $error if $error;
+ }
+ }
+
+ FS::upgrade_journal->set_done('encryption_check');
+ }
+
++ # now that everything's encrypted, tokenize...
+ FS::cust_main::Billing_Realtime::token_check(@_);
}
+# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
+# cust_payby might get deleted while this runs
+# not a method!
+sub _upgrade_next_recnum {
+ my ($dbh,$table,$lastrecnum,$recnums) = @_;
+ my $recnum = shift @$recnums;
+ return $recnum if $recnum;
+ my $tclass = 'FS::'.$table;
+ my $sql = 'SELECT '.$tclass->primary_key.
+ ' FROM '.$table.
+ ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
+ ' ORDER BY '.$tclass->primary_key.' LIMIT 500';;
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+ my @recnums;
+ while (my $rec = $sth->fetchrow_hashref) {
+ push @$recnums, $rec->{$tclass->primary_key};
+ }
+ $sth->finish();
+ $$lastrecnum = $$recnums[-1];
+ return shift @$recnums;
+}
+
=back
=head1 BUGS
use FS::Cursor;
use Time::Local qw(timelocal);
+ # allow_closed_replace only relevant to cust_pay/cust_refund, for upgrade tokenizing
use vars qw( $ignore_masked_payinfo $allow_closed_replace );
=head1 NAME
my($self,$payinfo) = @_;
if ( defined($payinfo) ) {
+ $self->paymask($self->mask_payinfo) unless $self->getfield('paymask') || $self->tokenized; #make sure old mask is set
$self->setfield('payinfo', $payinfo);
- $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #token
+ $self->paymask($self->mask_payinfo) unless $self->tokenized($payinfo); #remask unless tokenizing
} else {
$self->getfield('payinfo');
}
# Check to see if it's encrypted...
if ( ref($self) && $self->is_encrypted($payinfo) ) {
return 'N/A';
- } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
+ } elsif ( $self->tokenized($payinfo) || $payinfo eq 'N/A' ) { #token
return 'N/A (tokenized)'; #?
} else { # if not, mask it...
my $payinfo = $self->payinfo;
my $cardtype = cardtype($payinfo);
- $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
+ $cardtype = 'Tokenized' if $self->tokenized;
$self->set('paycardtype', $cardtype);
if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
validate($self->payinfo) or return "Illegal credit card number";
return "Unknown card type" if $cardtype eq "Unknown";
} else {
- $self->payinfo('N/A'); #???
+ $self->payinfo('N/A'); #??? re-masks card
}
}
} else {
}
}
+ return '';
}
=item payby_payinfo_pretty [ LOCALE ]
my $payinfo = shift || $self->payinfo;
my %hash = (
'custnum' => $self->custnum,
- 'payby' => 'CARD',
+ 'payby' => $self->payby,
);
return 1
if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
- || qsearch('cust_pay',
- { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } )
+ || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo } )
;
return 0;
}
}
+ =item tokenized [ PAYINFO ]
+
+ Returns true if object payinfo is tokenized
+
+ Optionally, an arbitrary payby and payinfo can be passed.
+
+ =cut
+
+ sub tokenized {
+ my $self = shift;
+ my $payinfo = scalar(@_) ? shift : $self->payinfo;
+ return 0 unless $payinfo; #avoid uninitialized value error
+ $payinfo =~ /^99\d{14}$/;
+ }
+
=back
=head1 BUGS
<% include("/elements/header.html","$action payment gateway override for ". $agent->agent, menubar(
#'View all payment gateways' => $p. 'browse/payment_gateway.html',
- 'View all agents' => $p. 'browse/agent.html',
+ 'View all agents' => $p. 'browse/agent.cgi',
)) %>
<% include('/elements/error.html') %>
% foreach my $payment_gateway (
% qsearch('payment_gateway', { 'disabled' => '' } )
% ) {
+% # don't let these be selected as agent overrides; there's a different mechanism
+% next if $payment_gateway->gateway_namespace eq 'Business::BatchPayment';
%
<OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>)
</SELECT>
<BR><BR>
- for <SELECT NAME="cardtype" MULTIPLE>
- % foreach my $cardtype (
- % "",
- % "VISA card",
- % "MasterCard",
- % "Discover card",
- % "American Express card",
- % "Diner's Club/Carte Blanche",
- % "enRoute",
- % "JCB",
- % "BankCard",
- % "Switch",
- % "Solo",
- % 'ACH',
- % 'PayPal',
- %) {
-
- <OPTION VALUE="<% $cardtype %>"><% $cardtype || '(Default fallback)' %>
- % }
-
- </SELECT>
- <BR><BR>
-
- (optional) when invoice contains only items of taxclass <INPUT TYPE="text" NAME="taxclass">
- <BR><BR>
-
<INPUT TYPE="submit" VALUE="Add gateway override">
</FORM>