From dcdf657e77ec7b46dc69e19a849a9c133123db7c Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 14 Dec 2006 06:00:46 +0000 Subject: encryption fixes from huntsberg & jayce --- FS/FS/ClientAPI/MyAccount.pm | 4 +- FS/FS/Record.pm | 56 ++++++++-- FS/FS/Schema.pm | 9 +- FS/FS/cust_main.pm | 87 ++------------- FS/FS/cust_pay.pm | 66 ++++-------- FS/FS/cust_pay_void.pm | 11 +- FS/FS/cust_refund.pm | 47 +++------ FS/FS/payinfo_Mixin.pm | 245 +++++++++++++++++++++++++++++++++++++++++++ FS/MANIFEST | 2 + FS/t/payinfo_Mixin.t | 5 + 10 files changed, 357 insertions(+), 175 deletions(-) create mode 100644 FS/FS/payinfo_Mixin.pm create mode 100644 FS/t/payinfo_Mixin.t (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index ff5b77565..de364724a 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -130,7 +130,7 @@ sub customer_info { } if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { - $return{payinfo} = $cust_main->payinfo_masked; + $return{payinfo} = $cust_main->paymask; @return{'month', 'year'} = $cust_main->paydate_monthyear; } @@ -175,7 +175,7 @@ sub edit_info { if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) { $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01'); - if ( $new->payinfo eq $cust_main->payinfo_masked ) { + if ( $new->payinfo eq $cust_main->paymask ) { $new->payinfo($cust_main->payinfo); } else { $new->paycvv($p->{'paycvv'}); diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 8f11fdbeb..29f2dc618 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -2,7 +2,8 @@ package FS::Record; use strict; use vars qw( $AUTOLOAD @ISA @EXPORT_OK $DEBUG - $me %virtual_fields_cache $nowarn_identical ); + $conf $me + %virtual_fields_cache $nowarn_identical ); use Exporter; use Carp qw(carp cluck croak confess); use File::CounterFile; @@ -36,9 +37,11 @@ my $rsa_encrypt; my $rsa_decrypt; FS::UID->install_callback( sub { - $File::CounterFile::DEFAULT_DIR = "/usr/local/etc/freeside/counters.". datasrc; + $conf = new FS::Conf; + $File::CounterFile::DEFAULT_DIR = $conf->base_dir . "/counters.". datasrc; } ); + =head1 NAME FS::Record - Database record objects @@ -442,8 +445,11 @@ sub qsearch { } # Check for encrypted fields and decrypt them. - my $conf = new FS::Conf; - if ($conf->exists('encryption') && eval 'defined(@FS::'. $table . '::encrypted_fields)') { + ## only in the local copy, not the cached object + if ( $conf && $conf->exists('encryption') # $conf doesn't exist when doing + # the initial search for + # access_user + && eval 'defined(@FS::'. $table . '::encrypted_fields)') { foreach my $record (@return) { foreach my $field (eval '@FS::'. $table . '::encrypted_fields') { # Set it directly... This may cause a problem in the future... @@ -713,11 +719,10 @@ sub insert { # Encrypt before the database - my $conf = new FS::Conf; if ($conf->exists('encryption') && defined(eval '@FS::'. $table . '::encrypted_fields')) { foreach my $field (eval '@FS::'. $table . '::encrypted_fields') { $self->{'saved'} = $self->getfield($field); - $self->setfield($field, $self->enrypt($self->getfield($field))); + $self->setfield($field, $self->encrypt($self->getfield($field))); } } @@ -1006,7 +1011,7 @@ sub replace { # Encrypt for replace my $conf = new FS::Conf; my $saved = {}; - if ($conf->exists('encryption') && defined(eval '@FS::'. $new->table . 'encrypted_fields')) { + if ($conf->exists('encryption') && defined(eval '@FS::'. $new->table . '::encrypted_fields')) { foreach my $field (eval '@FS::'. $new->table . '::encrypted_fields') { $saved->{$field} = $new->getfield($field); $new->setfield($field, $new->encrypt($new->getfield($field))); @@ -1205,6 +1210,12 @@ sub _h_statement { grep { defined($self->getfield($_)) && $self->getfield($_) ne "" } real_fields($self->table); ; + + # If we're encrypting then don't ever store the payinfo or CVV2 in the history.... + # You can see if it changed by the paymask... + if ($conf->exists('encryption') ) { + @fields = grep $_ ne 'payinfo' && $_ ne 'cvv2', @fields; + } my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields; "INSERT INTO h_". $self->table. " ( ". @@ -1869,6 +1880,17 @@ sub _dump { } (fields($self->table)) ); } +=item encrypt($value) + +Encrypts the credit card using a combination of PK to encrypt and uuencode to armour. + +Returns the encrypted string. + +You should generally not have to worry about calling this, as the system handles this for you. + +=cut + + sub encrypt { my ($self, $value) = @_; my $encrypted; @@ -1893,17 +1915,32 @@ sub encrypt { return $encrypted; } +=item is_encrypted($value) + +Checks to see if the string is encrypted and returns true or false (1/0) to indicate it's status. + +=cut + + sub is_encrypted { my ($self, $value) = @_; # Possible Bug - Some work may be required here.... - if (length($value) > 80) { + if ($value =~ /^M/ && length($value) > 80) { return 1; } else { return 0; } } +=item decrypt($value) + +Uses the private key to decrypt the string. Returns the decryoted string or undef on failure. + +You should generally not have to worry about calling this, as the system handles this for you. + +=cut + sub decrypt { my ($self,$value) = @_; my $decrypted = $value; # Will return the original value if it isn't encrypted or can't be decrypted. @@ -1912,7 +1949,8 @@ sub decrypt { $self->loadRSA; if (ref($rsa_decrypt) =~ /::RSA/) { my $encrypted = unpack ("u*", $value); - $decrypted = unpack("Z*", $rsa_decrypt->decrypt($encrypted)); + $decrypted = unpack("Z*", eval{$rsa_decrypt->decrypt($encrypted)}); + if ($@) {warn "Decryption Failed"}; } } return $decrypted; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index a97c396c5..691edd7a7 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -520,7 +520,8 @@ sub tables_hashref { 'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should be # index into payby table # eventually - 'payinfo', 'varchar', 'NULL', $char_d, '', '', #see cust_main above + 'payinfo', 'varchar', 'NULL', 512, '', '', #see cust_main above + 'paymask', 'varchar', 'NULL', $char_d, '', '', 'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes. 'closed', 'char', 'NULL', 1, '', '', ], @@ -538,7 +539,8 @@ sub tables_hashref { 'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should be # index into payby table # eventually - 'payinfo', 'varchar', 'NULL', $char_d, '', '', #see cust_main above + 'payinfo', 'varchar', 'NULL', 512, '', '', #see cust_main above + 'paymask', 'varchar', 'NULL', $char_d, '', '', 'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes. 'closed', 'char', 'NULL', 1, '', '', 'void_date', @date_type, '', '', @@ -677,7 +679,8 @@ sub tables_hashref { 'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should # be index into payby # table eventually - 'payinfo', 'varchar', 'NULL', $char_d, '', '', #see cust_main above + 'payinfo', 'varchar', 'NULL', 512, '', '', #see cust_main above + 'paymask', 'varchar', 'NULL', $char_d, '', '', 'paybatch', 'varchar', 'NULL', $char_d, '', '', 'closed', 'char', 'NULL', 1, '', '', ], diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 712777526..80b1a5b6e 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -50,8 +50,9 @@ use FS::type_pkgs; use FS::payment_gateway; use FS::agent_payment_gateway; use FS::banned_pay; +use FS::payinfo_Mixin; -@ISA = qw( FS::Record ); +@ISA = qw( FS::Record FS::payinfo_Mixin ); @EXPORT_OK = qw( smart_search ); @@ -189,81 +190,15 @@ FS::Record. The following fields are currently supported: =item ship_fax - phone (optional) -=item payby +=item payby - Payment Type (See L for valid payby values) -I (credit card - automatic), I (credit card - on-demand), I (electronic check - automatic), I (electronic check - on-demand), I (Phone bill billing), I (billing), I (free), or I (special billing type: applies a credit - see L and sets billing type to I) - -=item payinfo - -Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L) - -=cut - -sub payinfo { - my($self,$payinfo) = @_; - if ( defined($payinfo) ) { - $self->paymask($payinfo); - $self->setfield('payinfo', $payinfo); # This is okay since we are the 'setter' - } else { - $payinfo = $self->getfield('payinfo'); # This is okay since we are the 'getter' - return $payinfo; - } -} +=item payinfo - Payment Information (See L for data format) +=item paymask - Masked payinfo (See L for how this works) =item paycvv - -Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card - -=cut - -=item paymask - Masked payment type - -=over 4 - -=item Credit Cards - -Mask all but the last four characters. - -=item Checks -Mask all but last 2 of account number and bank routing number. - -=item Others - -Do nothing, return the unmasked string. - -=back - -=cut - -sub paymask { - my($self,$value)=@_; - - # If it doesn't exist then generate it - my $paymask=$self->getfield('paymask'); - if (!defined($value) && (!defined($paymask) || $paymask eq '')) { - $value = $self->payinfo; - } - - if ( defined($value) && !$self->is_encrypted($value)) { - my $payinfo = $value; - my $payby = $self->payby; - if ($payby eq 'CARD' || $payby eq 'DCRD') { # Credit Cards (Show last four) - $paymask = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4)); - } elsif ($payby eq 'CHEK' || - $payby eq 'DCHK' ) { # Checks (Show last 2 @ bank) - my( $account, $aba ) = split('@', $payinfo ); - $paymask = 'x'x(length($account)-2). substr($account,(length($account)-2))."@".$aba; - } else { # Tie up loose ends - $paymask = $payinfo; - } - $self->setfield('paymask', $paymask); # This is okay since we are the 'setter' - } elsif (defined($value) && $self->is_encrypted($value)) { - $paymask = 'N/A'; - } - return $paymask; -} +Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy @@ -1141,11 +1076,6 @@ sub replace { local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; - # If the mask is blank then try to set it - if we can... - if (!defined($self->getfield('paymask')) || $self->getfield('paymask') eq '') { - $self->paymask($self->payinfo); - } - # We absolutely have to have an old vs. new record to make this work. if (!defined($old)) { $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); @@ -1486,11 +1416,10 @@ sub check { $payinfo =~ s/[^\d\@]//g; if ( $conf->exists('echeck-nonus') ) { $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba'; - $payinfo = "$1\@$2"; } else { $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba'; - $payinfo = "$1\@$2"; } + $payinfo = "$1\@$2"; $self->payinfo($payinfo); $self->paycvv('') if $self->dbdef_table->column('paycvv'); @@ -3405,6 +3334,8 @@ sub paydate_monthyear { =item payinfo_masked +< DEPRICATED > Use $self->paymask + Returns a "masked" payinfo field appropriate to the payment type. Masked characters are replaced by 'x'es. Use this to display publicly accessable account Information. Credit Cards - Mask all but the last four characters. diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index bc5fbab08..a86bbc23a 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -1,7 +1,7 @@ package FS::cust_pay; use strict; -use vars qw( @ISA $conf $unsuspendauto $ignore_noapply ); +use vars qw( @ISA $conf $unsuspendauto $ignore_noapply @encrypted_fields ); use Date::Format; use Business::CreditCard; use Text::Template; @@ -14,7 +14,7 @@ use FS::cust_pay_refund; use FS::cust_main; use FS::cust_pay_void; -@ISA = qw( FS::cust_main_Mixin FS::Record ); +@ISA = qw(FS::Record FS::cust_main_Mixin FS::payinfo_Mixin ); $ignore_noapply = 0; @@ -24,6 +24,8 @@ FS::UID->install_callback( sub { $unsuspendauto = $conf->exists('unsuspendauto'); } ); +@encrypted_fields = ('payinfo'); + =head1 NAME FS::cust_pay - Object methods for cust_pay objects @@ -60,12 +62,11 @@ currently supported: =item _date - specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH), -`LECB' (phone bill billing), `BILL' (billing), `PREP` (prepaid card), -`CASH' (cash), `WEST' (Western Union), `MCRD' (Manual credit card), or -`COMP' (free) +=item payby - Payment Type (See L for valid payby values) + +=item payinfo - Payment Information (See L for data format) -=item payinfo - card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively +=item paymask - Masked payinfo (See L for how this works) =item paybatch - text field for tracking card processing @@ -327,7 +328,7 @@ sub delete { 'paid: $'. sprintf("%.2f", $self->paid). "\n", 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n", 'payby: '. $self->payby. "\n", - 'payinfo: '. $self->payinfo. "\n", + 'payinfo: '. $self->paymask. "\n", 'paybatch: '. $self->paybatch. "\n", ], ); @@ -375,6 +376,7 @@ sub check { || $self->ut_numbern('_date') || $self->ut_textn('paybatch') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->payinfo_check() ; return $error if $error; @@ -386,30 +388,6 @@ sub check { $self->_date(time) unless $self->_date; - $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP|CASH|WEST|MCRD)$/ - or return "Illegal payby"; - $self->payby($1); - - #false laziness with cust_refund::check - if ( $self->payby eq 'CARD' ) { - my $payinfo = $self->payinfo; - $payinfo =~ s/\D//g; - $self->payinfo($payinfo); - if ( $self->payinfo ) { - $self->payinfo =~ /^(\d{13,16})$/ - 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 cardtype($self->payinfo) eq "Unknown"; - } else { - $self->payinfo('N/A'); - } - - } else { - $error = $self->ut_textn('payinfo'); - return $error if $error; - } - $self->SUPER::check; } @@ -542,31 +520,27 @@ sub cust_main { =item payinfo_masked -Returns a "masked" payinfo field with all but the last four characters replaced -by 'x'es. Useful for displaying credit cards. + Use $self->paymask + +Returns a "masked" payinfo field appropriate to the payment type. Masked characters are replaced by 'x'es. Use this to display publicly accessable account Information. + +Credit Cards - Mask all but the last four characters. +Checks - Mask all but last 2 of account number and bank routing number. +Others - Do nothing, return the unmasked string. =cut sub payinfo_masked { my $self = shift; - #some false laziness w/cust_main::paymask - if ( $self->payby eq 'CARD' ) { - my $payinfo = $self->payinfo; - 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4)); - } elsif ( $self->payby eq 'CHEK' ) { - my( $account, $aba ) = split('@', $self->payinfo ); - 'x'x(length($account)-2). substr($account,(length($account)-2)). "@". $aba; - } else { - $self->payinfo; - } + return $self->paymask; } + =back =head1 BUGS -Delete and replace methods. payinfo_masked false laziness with cust_main.pm -and cust_refund.pm +Delete and replace methods. =head1 SEE ALSO diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm index 946d69fe1..9a0e58293 100644 --- a/FS/FS/cust_pay_void.pm +++ b/FS/FS/cust_pay_void.pm @@ -1,6 +1,6 @@ package FS::cust_pay_void; use strict; -use vars qw( @ISA ); +use vars qw( @ISA @encrypted_fields ); use Business::CreditCard; use FS::UID qw(getotaker); use FS::Record qw(qsearchs dbh fields); # qsearch ); @@ -10,7 +10,9 @@ use FS::cust_pay; #use FS::cust_pay_refund; #use FS::cust_main; -@ISA = qw( FS::Record ); +@ISA = qw( FS::Record FS::payinfo_Mixin ); + +@encrypted_fields = ('payinfo'); =head1 NAME @@ -209,6 +211,8 @@ sub cust_main { =item payinfo_masked +< DEPRICATED > Use $self->paymask + Returns a "masked" payinfo field with all but the last four characters replaced by 'x'es. Useful for displaying credit cards. @@ -216,8 +220,7 @@ by 'x'es. Useful for displaying credit cards. sub payinfo_masked { my $self = shift; - my $payinfo = $self->payinfo; - 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4)); + return $self->paymask; } =back diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm index 8c672b8d7..a3a0e5ede 100644 --- a/FS/FS/cust_refund.pm +++ b/FS/FS/cust_refund.pm @@ -1,7 +1,7 @@ package FS::cust_refund; use strict; -use vars qw( @ISA ); +use vars qw( @ISA @encrypted_fields ); use Business::CreditCard; use FS::Record qw( qsearch qsearchs dbh ); use FS::UID qw(getotaker); @@ -9,8 +9,11 @@ use FS::cust_credit; use FS::cust_credit_refund; use FS::cust_pay_refund; use FS::cust_main; +use FS::payinfo_Mixin; -@ISA = qw( FS::Record ); +@ISA = qw( FS::Record FS::payinfo_Mixin ); + +@encrypted_fields = ('payinfo'); =head1 NAME @@ -50,11 +53,11 @@ inherits from FS::Record. The following fields are currently supported: =item _date - specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH), -`LECB' (Phone bill billing), `BILL' (billing), `CASH' (cash), -`WEST' (Western Union), `MCRD' (Manual credit card), or `COMP' (free) +=item payby - Payment Type (See L for valid payby values) + +=item payinfo - Payment Information (See L for data format) -=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username) +=item paymask - Masked payinfo (See L for how this works) =item paybatch - text field for tracking card processing @@ -212,29 +215,8 @@ sub check { unless $self->crednum || qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); - $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|CASH|WEST|MCRD)$/ - or return "Illegal payby"; - $self->payby($1); - - #false laziness with cust_pay::check - if ( $self->payby eq 'CARD' ) { - my $payinfo = $self->payinfo; - $payinfo =~ s/\D//g; - $self->payinfo($payinfo); - if ( $self->payinfo ) { - $self->payinfo =~ /^(\d{13,16})$/ - 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 cardtype($self->payinfo) eq "Unknown"; - } else { - $self->payinfo('N/A'); - } - - } else { - $error = $self->ut_textn('payinfo'); - return $error if $error; - } + $error = $self->payinfo_check; + return $error if $error; $self->otaker(getotaker); @@ -285,10 +267,10 @@ sub unapplied { sprintf("%.2f", $amount ); } - - =item payinfo_masked + Use $self->paymask + Returns a "masked" payinfo field with all but the last four characters replaced by 'x'es. Useful for displaying credit cards. @@ -297,8 +279,7 @@ by 'x'es. Useful for displaying credit cards. sub payinfo_masked { my $self = shift; - my $payinfo = $self->payinfo; - 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4)); + return $self->paymask; } diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm new file mode 100644 index 000000000..b1790c625 --- /dev/null +++ b/FS/FS/payinfo_Mixin.pm @@ -0,0 +1,245 @@ +package FS::payinfo_Mixin; + +use strict; +use Business::CreditCard; + +=head1 NAME + +FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo. + +=head1 SYNOPSIS + +package FS::some_table; +use vars qw(@ISA); +@ISA = qw( FS::payinfo_Mixin FS::Record ); + +=head1 DESCRIPTION + +This is a mixin class for records that contain payinfo. + +This class handles the following functions for payinfo... + +Payment Mask (Generation and Storage) +Data Validation (parent checks need to be sure to call this) +Encryption - In the Future (Pull from Record.pm) +Bad Card Stuff - In the Future (Integrate Banned Pay) +Currency - In the Future + +=head1 fields + +=over 4 + +=item payby + +The following payment types (payby) are supported: + +For Customers (cust_main): +'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand), +'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand), +'LECB' (Phone bill billing), 'BILL' (billing), 'COMP' (free), or +'PREPAY' (special billing type: applies a credit - see L and sets billing type to I) + +For Refunds (cust_refund): +'CARD' (credit cards), 'CHEK' (electronic check/ACH), +'LECB' (Phone bill billing), 'BILL' (billing), 'CASH' (cash), +'WEST' (Western Union), 'MCRD' (Manual credit card), 'CBAK' Chargeback, or 'COMP' (free), + + +For Payments (cust_pay): +'CARD' (credit cards), 'CHEK' (electronic check/ACH), +'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card), +'CASH' (cash), 'WEST' (Western Union), or 'MCRD' (Manual credit card) +'COMP' (free) is depricated as a payment type in cust_pay + +=cut + +sub payby { + my($self,$payby) = @_; + if ( defined($payby) ) { + $self->setfield('payby', $payby); + } + return $self->getfield('payby') +} + + +=item payinfo + +Payment information (payinfo) can be one of the following types: + +Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L) + +=cut + +sub payinfo { + my($self,$payinfo) = @_; + if ( defined($payinfo) ) { + $self->setfield('payinfo', $payinfo); # This is okay since we are the 'setter' + $self->paymask($self->mask_payinfo()); + } else { + $payinfo = $self->getfield('payinfo'); # This is okay since we are the 'getter' + return $payinfo; + } +} + +=item paycvv + +Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card + +=cut + +sub paycvv { + my($self,$paycvv) = @_; + # This is only allowed in cust_main... Even then it really shouldn't be stored... + if ($self->table eq 'cust_main') { + if ( defined($paycvv) ) { + $self->setfield('paycvv', $paycvv); # This is okay since we are the 'setter' + } else { + $paycvv = $self->getfield('paycvv'); # This is okay since we are the 'getter' + return $paycvv; + } + } else { +# warn "This doesn't work for other tables besides cust_main + } +} + +=item paymask + +=cut + +sub paymask { + my($self,$paymask)=@_; + + + if ($paymask ne '') { + # I hate this little bit of magic... I don't expect it to cause a problem, but who knows... + # If the payinfo is passed in masked then ignore it and set it based on the payinfo + # The only guy that should call this in this way is... $self->payinfo + $self->setfield('paymask', $self->mask_payinfo()); + } else { + $paymask=$self->getfield('paymask'); + if (!defined($paymask) || $paymask eq '') { + # Generate it if it's blank - Note that we're not going to set it - just generate + $paymask = $self->mask_payinfo(); + } + } + return $paymask; +} + +=item mask_payinfo() + +This method converts the payment info (credit card, bank account, etc.) into a masked string. + +=cut + +sub mask_payinfo { + my $self = shift; + my $paymask; + my $payinfo = $self->payinfo; + my $payby = $self->payby; + # Check to see if it's encrypted... + if ($self->is_encrypted($payinfo)) { + $paymask = 'N/A'; + } else { + # if not, mask it... + if ($payby eq 'CARD' || $payby eq 'DCRD' || $payby eq 'MCRD') { # Credit Cards (Show first and last four) + $paymask = substr($payinfo,0,4). 'x'x(length($payinfo)-8). substr($payinfo,(length($payinfo)-4)); + } elsif ($payby eq 'CHEK' || + $payby eq 'DCHK' ) { # Checks (Show last 2 @ bank) + my( $account, $aba ) = split('@', $payinfo ); + $paymask = 'x'x(length($account)-2). substr($account,(length($account)-2))."@".$aba; + } else { # Tie up loose ends + $paymask = $payinfo; + } + } + return $paymask; +} + +=back + + +=head1 METHODS + +=over 4 + +=item payinfo_check + +For Customers (cust_main): +'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand), +'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand), +'LECB' (Phone bill billing), 'BILL' (billing), 'COMP' (free), or +'PREPAY' (special billing type: applies a credit - see L and sets billing type to I) + +For Refunds (cust_refund): +'CARD' (credit cards), 'CHEK' (electronic check/ACH), +'LECB' (Phone bill billing), 'BILL' (billing), 'CASH' (cash), +'WEST' (Western Union), 'MCRD' (Manual credit card), 'CBAK' (Chargeback), or 'COMP' (free) + +For Payments (cust_pay): +'CARD' (credit cards), 'CHEK' (electronic check/ACH), +'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card), +'CASH' (cash), 'WEST' (Western Union), or 'MCRD' (Manual credit card) +'COMP' (free) is depricated as a payment type in cust_pay + +=cut + + + + + +sub payinfo_check { + my $self = shift; + + # Make sure it's a valid payby + $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD|PREP|CBAK)$/ + or return "Illegal payby (overall payinfo_check)"; + $self->payby($1); + + + # Okay some aren't valid depending on table + if ($self->table eq 'cust_main') { + if ($self->payby =~ /^(CASH|WEST|MCRD|PREP|CBAK)$/) { + return "Illegal payby (cust_main)"; + } + } elsif ($self->table eq 'cust_refund') { + if ($self->payby =~ /^(DCRD|DCHK|PREPAY|PREP)$/) { + return "Illegal payby (cust_refund)"; + } + } elsif ($self->table eq 'cust_pay') { + if ($self->payby =~ /^(DCRD|DCHK|PREPAY|CBAK)$/) { + return "Illegal payby (cust_pay)"; + } + } + + if ( $self->payby eq 'CARD' ) { + my $payinfo = $self->payinfo; + $payinfo =~ s/\D//g; + $self->payinfo($payinfo); + if ( $self->payinfo ) { + $self->payinfo =~ /^(\d{13,16})$/ + or return "Illegal (mistyped?) credit card number (payinfo)"; + $self->payinfo($1); + Business::CreditCard::validate($self->payinfo) or return "Illegal credit card number"; + return "Unknown card type" if Business::CreditCard::cardtype($self->payinfo) eq "Unknown"; + } else { + $self->payinfo('N/A'); + } + } else { + my $error = $self->ut_textn('payinfo'); + return $error if $error; + } +} + + + +=head1 BUGS + +Have to add the future items... + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index aeb012851..906cc9cdb 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -81,6 +81,7 @@ FS/h_svc_external.pm FS/h_svc_forward.pm FS/h_svc_www.pm FS/part_bill_event.pm +FS/payinfo_Mixin.pm FS/export_svc.pm FS/part_export.pm FS/part_export_option.pm @@ -272,6 +273,7 @@ t/part_referral.t t/part_svc.t t/part_svc_column.t t/payby.t +t/payinfo_Mixin.t t/pkg_class.t t/pkg_svc.t t/port.t diff --git a/FS/t/payinfo_Mixin.t b/FS/t/payinfo_Mixin.t new file mode 100644 index 000000000..3567c8e08 --- /dev/null +++ b/FS/t/payinfo_Mixin.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::payinfo_Mixin; +$loaded=1; +print "ok 1\n"; -- cgit v1.2.1