summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Schema.pm9
-rw-r--r--FS/FS/Upgrade.pm3
-rw-r--r--FS/FS/cust_pay.pm10
-rw-r--r--FS/FS/cust_pay_void.pm4
-rw-r--r--FS/FS/cust_payby.pm54
-rw-r--r--FS/FS/cust_refund.pm7
-rw-r--r--FS/FS/msg_template.pm1
-rw-r--r--FS/FS/payinfo_Mixin.pm37
-rwxr-xr-xhttemplate/search/elements/cust_pay_or_refund.html179
9 files changed, 146 insertions, 158 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 748df8b1b..eab0c1934 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1709,7 +1709,7 @@ sub tables_hashref {
'weight', 'int', 'NULL', '', '', '',
'payby', 'char', '', 4, '', '',
'payinfo', 'varchar', 'NULL', 512, '', '',
- 'cardtype', 'varchar', 'NULL', $char_d, '', '',
+ 'paycardtype', 'varchar', 'NULL', $char_d, '', '',
'paycvv', 'varchar', 'NULL', 512, '', '',
'paymask', 'varchar', 'NULL', $char_d, '', '',
#'paydate', @date_type, '', '',
@@ -2459,6 +2459,7 @@ sub tables_hashref {
'usernum', 'int', 'NULL', '', '', '',
'payby', 'char', '', 4, '', '',
'payinfo', 'varchar', 'NULL', 512, '', '',
+ 'paycardtype', 'varchar', 'NULL', $char_d, '', '',
'paymask', 'varchar', 'NULL', $char_d, '', '',
'paydate', 'varchar', 'NULL', 10, '', '',
'paybatch', 'varchar', 'NULL', $char_d, '', '',#for auditing purposes
@@ -2516,7 +2517,8 @@ sub tables_hashref {
'usernum', 'int', 'NULL', '', '', '',
'payby', 'char', '', 4, '', '',
'payinfo', 'varchar', 'NULL', 512, '', '',
- 'paymask', 'varchar', 'NULL', $char_d, '', '',
+ 'paycardtype', 'varchar', 'NULL', $char_d, '', '',
+ 'paymask', 'varchar', 'NULL', $char_d, '', '',
#'paydate' ?
'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes.
'closed', 'char', 'NULL', 1, '', '',
@@ -3076,7 +3078,8 @@ sub tables_hashref {
# be index into payby
# table eventually
'payinfo', 'varchar', 'NULL', 512, '', '', #see cust_main above
- 'paymask', 'varchar', 'NULL', $char_d, '', '',
+ 'paycardtype', 'varchar', 'NULL', $char_d, '', '',
+ 'paymask', 'varchar', 'NULL', $char_d, '', '',
'paybatch', 'varchar', 'NULL', $char_d, '', '',
'closed', 'char', 'NULL', 1, '', '',
'source_paynum', 'int', 'NULL', '', '', '', # link to cust_payby, to prevent unapply of gateway-generated refunds
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 3ff943fcf..6e2a62cd6 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -428,6 +428,9 @@ sub upgrade_data {
'cust_refund' => [],
'banned_pay' => [],
+ #paycardtype
+ 'cust_payby' => [],
+
#default namespace
'payment_gateway' => [],
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 331a15623..e0a7143c4 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -97,6 +97,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
Payment Information (See L<FS::payinfo_Mixin> for data format)
+=item paycardtype
+
+Credit card type, if appropriate; autodetected.
+
=item paymask
Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
@@ -1205,6 +1209,12 @@ sub _upgrade_data { #class method
process_upgrade_paybatch();
}
}
+
+ ###
+ # set paycardtype
+ ###
+ $class->upgrade_set_cardtype;
+
}
sub process_upgrade_paybatch {
diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm
index 8d37a58b5..29540d1c6 100644
--- a/FS/FS/cust_pay_void.pm
+++ b/FS/FS/cust_pay_void.pm
@@ -74,6 +74,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively
+=item cardtype
+
+Credit card type, if appropriate.
+
=item paybatch
text field for tracking card processing
diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm
index 62fa9be5f..e4a1d193c 100644
--- a/FS/FS/cust_payby.pm
+++ b/FS/FS/cust_payby.pm
@@ -115,6 +115,9 @@ paytype
payip
+=item paycardtype
+
+The credit card type (deduced from the card number).
=back
@@ -331,6 +334,13 @@ sub check {
# 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)$/ ) {
@@ -343,9 +353,12 @@ sub check {
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 } );
@@ -367,7 +380,7 @@ sub check {
}
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);
@@ -380,7 +393,6 @@ sub check {
$self->paycvv('');
}
- my $cardtype = cardtype($payinfo);
if ( $cardtype =~ /^(Switch|Solo)$/i ) {
return "Start date or issue number is required for $cardtype cards"
@@ -438,6 +450,15 @@ sub check {
}
}
+ } 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;
@@ -449,8 +470,6 @@ sub check {
# unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
# $self->paycvv('');
- }
-
if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
$self->paydate('');
@@ -458,6 +477,7 @@ sub check {
} 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 '-';
@@ -520,10 +540,14 @@ sub check_payinfo_cardtype {
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};
@@ -599,7 +623,7 @@ sub label {
my $self = shift;
my $name = $self->payby =~ /^(CARD|DCRD)$/
- && cardtype($self->paymask) || FS::payby->shortname($self->payby);
+ && $self->paycardtype || FS::payby->shortname($self->payby);
( $self->payby =~ /^(CARD|CHEK)$/ ? $weight{$self->weight}. ' automatic '
: 'Manual '
@@ -872,6 +896,18 @@ sub search_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
diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm
index ced954036..4d2baa514 100644
--- a/FS/FS/cust_refund.pm
+++ b/FS/FS/cust_refund.pm
@@ -82,6 +82,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
Payment Information (See L<FS::payinfo_Mixin> for data format)
+=item paycardtype
+
+Detected credit card type, if appropriate; autodetected.
+
=item paymask
Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
@@ -472,6 +476,9 @@ sub _upgrade_data { # class method
my ($class, %opts) = @_;
$class->_upgrade_reasonnum(%opts);
$class->_upgrade_otaker(%opts);
+
+ local $ignore_empty_reasonnum = 1;
+ $class->upgrade_set_cardtype;
}
=back
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index 1dd48cc1a..b89071710 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -93,6 +93,7 @@ sub extension_table { ''; } # subclasses don't HAVE to have extensions
sub _rebless {
my $self = shift;
+ return '' unless $self->msgclass;
my $class = 'FS::msg_template::' . $self->msgclass;
eval "use $class;";
bless($self, $class) unless $@;
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 41768189e..4f26e8c6f 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -5,6 +5,7 @@ use Business::CreditCard;
use FS::payby;
use FS::Record qw(qsearch);
use FS::UID qw(driver_name);
+use FS::Cursor;
use Time::Local qw(timelocal);
use vars qw($ignore_masked_payinfo);
@@ -193,7 +194,12 @@ sub payinfo_check {
or return "Illegal payby: ". $self->payby;
if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
+
my $payinfo = $self->payinfo;
+ my $cardtype = cardtype($payinfo);
+ $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
+ $self->set('paycardtype', $cardtype);
+
if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
# allow it
} else {
@@ -204,13 +210,18 @@ sub payinfo_check {
or return "Illegal (mistyped?) credit card number (payinfo)";
$self->payinfo($1);
validate($self->payinfo) or return "Illegal credit card number";
- return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
- && cardtype($self->payinfo) eq "Unknown";
+ return "Unknown card type" if $cardtype eq "Unknown";
} else {
$self->payinfo('N/A'); #???
}
}
} else {
+ if ( $self->payby eq 'CARD' and $self->paymask ) {
+ # if we can't decrypt the card, at least detect the cardtype
+ $self->set('paycardtype', cardtype($self->paymask));
+ } else {
+ $self->set('paycardtype', '');
+ }
if ( $self->is_encrypted($self->payinfo) ) {
#something better? all it would cause is a decryption error anyway?
my $error = $self->ut_anything('payinfo');
@@ -404,6 +415,28 @@ sub paydate_epoch_sql {
END"
}
+=item upgrade_set_cardtype
+
+Find all records with a credit card payment type and no paycardtype, and
+replace them in order to set their paycardtype.
+
+=cut
+
+sub upgrade_set_cardtype {
+ my $class = shift;
+ # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
+ # will do this. ignore any problems with the cards.
+ local $ignore_masked_payinfo = 1;
+ my $search = FS::Cursor->new({
+ table => $class->table,
+ extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
+ });
+ while (my $record = $search->fetch) {
+ my $error = $record->replace;
+ die $error if $error;
+ }
+}
+
=back
=head1 BUGS
diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html
index 4ed297dac..03aaedd36 100755
--- a/httemplate/search/elements/cust_pay_or_refund.html
+++ b/httemplate/search/elements/cust_pay_or_refund.html
@@ -67,6 +67,15 @@ Examples:
],
'show_combined' => 1,
&>
+<%shared>
+# canonicalize the payby subtype string to an SQL-quoted list
+my %cardtype_of = (
+ 'VisaMC' => q['VISA card', 'MasterCard'],
+ 'Amex' => q['American Express card'],
+ 'Discover' => q['Discover card'],
+ 'Maestro' => q['Switch', 'Solo', 'Laser'],
+);
+</%shared>
<%init>
my %opt = @_;
@@ -191,10 +200,8 @@ if ($opt{'show_card_type'}) {
push @header, emt('Card Type');
$align .= 'r';
push @links, '';
- push @fields, sub {
- (($_[0]->payby eq 'CARD') && ($_[0]->paymask !~ /N\/A/)) ? cardtype($_[0]->paymask) : ''
- };
- push @sort_fields, '';
+ push @fields, 'paycardtype';
+ push @sort_fields, 'paycardtype';
}
if ( $unapplied ) {
@@ -305,150 +312,32 @@ if ( $cgi->param('magic') ) {
if ( $cgi->param('payby') ) {
my @all_payby_search = ();
- foreach my $payby ( $cgi->param('payby') ) {
-
- $payby =~
- /^(CARD|CHEK|BILL|CASH|PPAL|APPL|ANRD|PREP|WIRE|WEST|IDTP|EDI|MCRD|MCHK)(-(VisaMC|Amex|Discover|Maestro|Tokenized))?$/
- or die "illegal payby $payby";
-
- my $payby_search = "$table.payby = '$1'";
-
- if ( $3 ) {
-
- my $cardtype = $3;
-
- my $similar_to = dbh->{Driver}->{Name} =~ /^mysql/i
- ? 'REGEXP' #doesn't behave exactly the same, but
- #should work for our patterns
- : 'SIMILAR TO';
-
- my $search;
- if ( $cardtype eq 'VisaMC' ) {
-
- #avoid posix regexes for portability
- $search =
- # Visa
- " ( ( substring($table.payinfo from 1 for 1) = '4' ".
- # is not Switch
- " AND substring($table.payinfo from 1 for 4) != '4936' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT $similar_to '49030[2-9]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT $similar_to '49033[5-9]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT $similar_to '49110[1-2]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT $similar_to '49117[4-9]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT $similar_to '49118[1-2]' ".
- " )".
- # MasterCard
- " OR substring($table.payinfo from 1 for 2) = '51' ".
- " OR substring($table.payinfo from 1 for 2) = '52' ".
- " OR substring($table.payinfo from 1 for 2) = '53' ".
- " OR substring($table.payinfo from 1 for 2) = '54' ".
- " OR substring($table.payinfo from 1 for 2) = '54' ".
- " OR substring($table.payinfo from 1 for 2) = '55' ".
- " OR substring($table.payinfo from 1 for 4) $similar_to '222[1-9]' ".
- " OR substring($table.payinfo from 1 for 3) $similar_to '22[3-9]' ".
- " OR substring($table.payinfo from 1 for 2) $similar_to '2[3-6]' ".
- " OR substring($table.payinfo from 1 for 3) $similar_to '27[0-1]' ".
- " OR substring($table.payinfo from 1 for 4) = '2720' ".
- " OR substring($table.payinfo from 1 for 3) = '2[2-7]x' ".
- " ) ";
-
- } elsif ( $cardtype eq 'Amex' ) {
-
- $search =
- " ( substring($table.payinfo from 1 for 2 ) = '34' ".
- " OR substring($table.payinfo from 1 for 2 ) = '37' ".
- " ) ";
-
- } elsif ( $cardtype eq 'Discover' ) {
-
- my $country = $conf->config('countrydefault') || 'US';
-
- $search =
- " ( substring($table.payinfo from 1 for 4 ) = '6011' ".
- " OR substring($table.payinfo from 1 for 3 ) = '60x' ".
- " OR substring($table.payinfo from 1 for 2 ) = '65' ".
-
- # diner's 300-305 / 3095
- " OR substring($table.payinfo from 1 for 3 ) = '300' ".
- " OR substring($table.payinfo from 1 for 3 ) = '301' ".
- " OR substring($table.payinfo from 1 for 3 ) = '302' ".
- " OR substring($table.payinfo from 1 for 3 ) = '303' ".
- " OR substring($table.payinfo from 1 for 3 ) = '304' ".
- " OR substring($table.payinfo from 1 for 3 ) = '305' ".
- " OR substring($table.payinfo from 1 for 4 ) = '3095' ".
- " OR substring($table.payinfo from 1 for 3 ) = '30x' ".
-
- # diner's 36, 38, 39
- " OR substring($table.payinfo from 1 for 2 ) = '36' ".
- " OR substring($table.payinfo from 1 for 2 ) = '38' ".
- " OR substring($table.payinfo from 1 for 2 ) = '39' ".
-
- " OR substring($table.payinfo from 1 for 3 ) = '644' ".
- " OR substring($table.payinfo from 1 for 3 ) = '645' ".
- " OR substring($table.payinfo from 1 for 3 ) = '646' ".
- " OR substring($table.payinfo from 1 for 3 ) = '647' ".
- " OR substring($table.payinfo from 1 for 3 ) = '648' ".
- " OR substring($table.payinfo from 1 for 3 ) = '649' ".
- " OR substring($table.payinfo from 1 for 3 ) = '64x' ".
-
- # JCB cards in the 3528-3589 range identified as Discover inside US & territories (NOT Canada)
- ( $country =~ /^(US|PR|VI|MP|PW|GU)$/
- ?" OR substring($table.payinfo from 1 for 4 ) = '3528' ".
- " OR substring($table.payinfo from 1 for 4 ) = '3529' ".
- " OR substring($table.payinfo from 1 for 3 ) = '353' ".
- " OR substring($table.payinfo from 1 for 3 ) = '354' ".
- " OR substring($table.payinfo from 1 for 3 ) = '355' ".
- " OR substring($table.payinfo from 1 for 3 ) = '356' ".
- " OR substring($table.payinfo from 1 for 3 ) = '357' ".
- " OR substring($table.payinfo from 1 for 3 ) = '358' ".
- " OR substring($table.payinfo from 1 for 3 ) = '35x' "
- :""
- ).
-
- #China Union Pay processed as Discover in US, Mexico and Caribbean
- ( $country =~ /^(US|MX|AI|AG|AW|BS|BB|BM|BQ|VG|KY|CW|DM|DO|GD|GP|JM|MQ|MS|BL|KN|LC|VC|MF|SX|TT|TC)$/
- ?" OR substring($table.payinfo from 1 for 3 ) $similar_to '62[24-68x]' "
- :""
- ).
-
- " ) ";
-
- } elsif ( $cardtype eq 'Maestro' ) {
-
- $search =
- " ( substring($table.payinfo from 1 for 2 ) = '63' ".
- " OR substring($table.payinfo from 1 for 2 ) = '67' ".
- " OR substring($table.payinfo from 1 for 6 ) = '564182' ".
- " OR substring($table.payinfo from 1 for 4 ) = '4936' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " $similar_to '49030[2-9]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " $similar_to '49033[5-9]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " $similar_to '49110[1-2]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " $similar_to '49117[4-9]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " $similar_to '49118[1-2]' ".
- " ) ";
-
- } elsif ( $cardtype eq 'Tokenized' ) {
-
- $search = " substring($table.payinfo from 1 for 2 ) = '99' ";
+ foreach my $payby_string ( $cgi->param('payby') ) {
+
+ my $payby_search;
+
+ my ($payby, $subtype) = split('-', $payby_string);
+ # make sure it exists and is a transaction type
+ if ( FS::payby->payment_payby2longname($payby) ) {
+ $payby_search = "$table.payby = " . dbh->quote($payby);
+ } else {
+ die "illegal payby $payby_string";
+ }
+
+ if ( $subtype ) {
+
+ if ( $subtype eq 'Tokenized' ) {
+
+ $payby_search .= " AND substring($table.payinfo from 1 for 2 ) = '99' ";
+ # XXX should store the cardtype as 'Tokenized' in this case?
} else {
- die "unknown card type $cardtype";
- }
- my $masksearch = $search;
- $masksearch =~ s/$table\.payinfo/$table.paymask/gi;
+ my $in_cardtype = $cardtype_of{$subtype}
+ or die "unknown card type $subtype";
+ $payby_search .= " AND $table.paycardtype IN($in_cardtype)";
- $payby_search = "( $payby_search AND ( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) ) )";
+ }
}
@@ -610,6 +499,8 @@ if ( $cgi->param('magic') ) {
'addl_from' => $addl_from,
};
+warn Dumper \$sql_query;
+
} else {
#hmm... is this still used?