our $ignore_expired_card = 0;
our $ignore_banned_card = 0;
our $ignore_invalid_card = 0;
+our $ignore_cardtype = 0;
our $conf;
install_callback FS::UID sub {
payip
+=item paycardtype
+
+The credit card type (deduced from the card number).
=back
? shift
: $self->replace_old;
- if ( length($old->paycvv) && $self->paycvv =~ /^\s*[\*x]*\s*$/ ) {
- $self->paycvv($old->paycvv);
- }
-
if ( $self->payby =~ /^(CARD|DCRD)$/
&& ( $self->payinfo =~ /xx/
|| $self->payinfo =~ /^\s*N\/A\s+\(tokenized\)\s*$/
$self->payinfo($new_account.'@'.$new_aba);
}
+ # only unmask paycvv if payinfo stayed the same
+ if ( $self->payby =~ /^(CARD|DCRD)$/ and $self->paycvv =~ /^\s*[\*x]+\s*$/ ) {
+ if ( $old->payinfo eq $self->payinfo
+ && $old->paymask eq $self->paymask
+ ) {
+ $self->paycvv($old->paycvv);
+ } else {
+ $self->paycvv('');
+ }
+ }
+
local($ignore_expired_card) = 1
if $old->payby =~ /^(CARD|DCRD)$/
&& $self->payby =~ /^(CARD|DCRD)$/
{
my $error = $self->check_payinfo_cardtype;
return $error if $error;
+
+ if ( $conf->exists('business-onlinepayment-verification') ) {
+ $error = $self->verify;
+ return $error if $error;
+ }
}
local $SIG{HUP} = 'IGNORE';
# Need some kind of global flag to accept invalid cards, for testing
# on scrubbed data.
#XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+
+ # In this block: detect card type; reject credit card / account numbers that
+ # are impossible or banned; reject other payment features (date, CVV length)
+ # that are inappropriate for the card type.
+ # However, if the payinfo is encrypted then just detect card type and assume
+ # the other checks were already done.
+
if ( !$ignore_invalid_card &&
$check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
validate($payinfo)
or return gettext('invalid_card'); # . ": ". $self->payinfo;
- return gettext('unknown_card_type')
- if $self->payinfo !~ /^99\d{14}$/ #token
- && cardtype($self->payinfo) eq "Unknown";
+ my $cardtype = cardtype($payinfo);
+ $cardtype = 'Tokenized' if $self->payinfo =~ /^99\d{14}$/; #token
+
+ return gettext('unknown_card_type') if $cardtype eq "Unknown";
+
+ $self->set('paycardtype', $cardtype);
unless ( $ignore_banned_card ) {
my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
}
if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
- if ( cardtype($self->payinfo) eq 'American Express card' ) {
+ if ( $cardtype eq 'American Express card' ) {
$self->paycvv =~ /^(\d{4})$/
or return "CVV2 (CID) for American Express cards is four digits.";
$self->paycvv($1);
$self->paycvv('');
}
- my $cardtype = cardtype($payinfo);
if ( $cardtype =~ /^(Switch|Solo)$/i ) {
return "Start date or issue number is required for $cardtype cards"
}
}
+ } elsif ( $self->payby =~ /^CARD|DCRD$/ and $self->paymask ) {
+ # either ignoring invalid cards, or we can't decrypt the payinfo, but
+ # try to detect the card type anyway. this never returns failure, so
+ # the contract of $ignore_invalid_cards is maintained.
+ $self->set('paycardtype', cardtype($self->paymask));
+ } else {
+ $self->set('paycardtype', '');
+ }
+
# } elsif ( $self->payby eq 'PREPAY' ) {
#
# my $payinfo = $self->payinfo;
# unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
# $self->paycvv('');
- }
-
if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
$self->paydate('');
} elsif ( $self->payby =~ /^(CARD|DCRD)$/ ) {
# shouldn't payinfo_check do this?
+ # (except we don't ever call payinfo_check from here)
return "Expiration date required"
if $self->paydate eq '' || $self->paydate eq '-';
}
- ###
+ if ( ! $self->custpaybynum
+ && $conf->exists('business-onlinepayment-verification') ) {
+ $error = $self->verify;
+ return $error if $error;
+ }
$self->SUPER::check;
}
sub check_payinfo_cardtype {
my $self = shift;
+ return '' if $ignore_cardtype;
+
return '' unless $self->payby =~ /^(CARD|CHEK)$/;
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
- return '' if $payinfo =~ /^99\d{14}$/; #token
+ if ( $payinfo =~ /^99\d{14}$/ ) {
+ $self->set('paycardtype', 'Tokenized');
+ return '';
+ }
my %bop_card_types = map { $_=>1 } values %{ card_types() };
my $cardtype = cardtype($payinfo);
+ $self->set('paycardtype', $cardtype);
return "$cardtype not accepted" unless $bop_card_types{$cardtype};
}
+=item label
+
+Returns a one line text label for this payment type.
+
+=cut
+
+my %weight = (
+ 1 => 'Primary',
+ 2 => 'Secondary',
+ 3 => 'Tertiary',
+ 4 => 'Fourth',
+ 5 => 'Fifth',
+ 6 => 'Sixth',
+ 7 => 'Seventh',
+);
+
+sub label {
+ my $self = shift;
+
+ my $name = $self->payby =~ /^(CARD|DCRD)$/
+ && $self->paycardtype || FS::payby->shortname($self->payby);
+
+ ( $self->payby =~ /^(CARD|CHEK)$/ ? $weight{$self->weight}. ' automatic '
+ : 'Manual '
+ ).
+ "$name: ". $self->paymask.
+ ( $self->payby =~ /^(CARD|DCRD)$/
+ ? ' Exp '. join('/', $self->paydate_mon_year)
+ : ''
+ );
+
+}
+
=item realtime_bop
=cut
}
+=item verify
+
+=cut
+
+sub verify {
+ my $self = shift;
+ return '' unless $self->payby =~ /^(CARD|DCRD)$/;
+
+ my %opt = ();
+
+ # false laziness with check
+ my( $m, $y );
+ if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
+ ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
+ } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
+ ( $m, $y ) = ( $2, "19$1" );
+ } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
+ ( $m, $y ) = ( $3, "20$2" );
+ } else {
+ return "Illegal expiration date: ". $self->paydate;
+ }
+ $m = sprintf('%02d',$m);
+ $opt{paydate} = "$y-$m-01";
+
+ $opt{$_} = $self->$_() for qw( payinfo payname paycvv );
+
+ if ( $self->locationnum ) {
+ my $cust_location = $self->cust_location;
+ $opt{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
+ }
+
+ $self->cust_main->realtime_verify_bop({
+ 'method' => FS::payby->payby2bop( $self->payby ),
+ %opt,
+ });
+
+}
+
=item paytypes
Returns a list of valid values for the paytype field (bank account type for
'CARD' => 'DCRD',
'CHEK' => 'DCHK',
);
+ # the payby selector gives the choice of CARD or CHEK (or others, but
+ # those are the ones with auto and on-demand versions). if the user didn't
+ # choose a weight, then they mean DCRD/DCHK.
$hashref->{payby} = $noauto{$hashref->{payby}}
if ! $hashref->{weight} && exists $noauto{$hashref->{payby}};
' LEFT JOIN cust_location AS '.$pre.'location '.
'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
}
+ # always make referral available in results
+ # (maybe we should be using FS::UI::Web::join_cust_main instead?)
+ $addl_from .= ' LEFT JOIN (select refnum, referral from part_referral) AS part_referral_x ON (cust_main.refnum = part_referral_x.refnum) ';
my $count_query = "SELECT COUNT(*) FROM cust_payby $addl_from $extra_sql";
=back
+=cut
+
+sub _upgrade_data {
+
+ my $class = shift;
+ local $ignore_banned_card = 1;
+ local $ignore_expired_card = 1;
+ local $ignore_invalid_card = 1;
+ $class->upgrade_set_cardtype;
+
+}
+
=head1 BUGS
=head1 SEE ALSO