diff options
| -rw-r--r-- | FS/FS/Schema.pm | 9 | ||||
| -rw-r--r-- | FS/FS/Upgrade.pm | 3 | ||||
| -rw-r--r-- | FS/FS/cust_pay.pm | 10 | ||||
| -rw-r--r-- | FS/FS/cust_pay_void.pm | 4 | ||||
| -rw-r--r-- | FS/FS/cust_payby.pm | 54 | ||||
| -rw-r--r-- | FS/FS/cust_refund.pm | 7 | ||||
| -rw-r--r-- | FS/FS/msg_template.pm | 1 | ||||
| -rw-r--r-- | FS/FS/payinfo_Mixin.pm | 37 | ||||
| -rwxr-xr-x | httemplate/search/elements/cust_pay_or_refund.html | 179 | 
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? | 
