From dcdf657e77ec7b46dc69e19a849a9c133123db7c Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 14 Dec 2006 06:00:46 +0000 Subject: [PATCH] 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 + httemplate/edit/cust_main/billing.html | 26 ++- httemplate/edit/cust_refund.cgi | 147 +++++++++------ httemplate/edit/process/cust_main.cgi | 6 + httemplate/misc/payment.cgi | 4 +- httemplate/misc/process/payment.cgi | 21 ++- httemplate/search/cust_pay.cgi | 2 +- httemplate/view/cust_main/billing.html | 8 +- httemplate/view/cust_main/payment_history.html | 4 +- 18 files changed, 494 insertions(+), 256 deletions(-) create mode 100644 FS/FS/payinfo_Mixin.pm create mode 100644 FS/t/payinfo_Mixin.t 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"; diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html index 78a2002a4..ae5630739 100644 --- a/httemplate/edit/cust_main/billing.html +++ b/httemplate/edit/cust_main/billing.html @@ -23,7 +23,10 @@
% # XXX key -% foreach my $field (qw( payinfo payname paycvv paystart_month paystart_year payissue payip )) { + + + +% foreach my $field (qw( payname paycvv paystart_month paystart_year payissue payip )) { @@ -136,16 +139,19 @@ % % -% my($payby, $payinfo, $payname)=( +% my($payby, $paymask, $payname)=( % $cust_main->payby, -% $cust_main->payinfo, +% $cust_main->paymask, % $cust_main->payname, % ); -% my( $account, $aba ) = split('@', $payinfo); +% my( $account, $aba ) = split('@', $paymask); % % my $disabled = 'DISABLED style="background-color: #dddddd"'; % my $text_disabled = 'style="color: #999999"'; -% if ( $payby =~ /^(CARD|DCRD)$/ && cardtype($payinfo) =~ /^(Switch|Solo)$/ ) { +% +% # this is not going to work unless the mask-generation recognizes +% # Switch/Solo cards +% if ( $payby =~ /^(CARD|DCRD)$/ && cardtype($paymask) =~ /^(Switch|Solo)$/ ) { % $disabled = 'style="background-color: #ffffff"'; % $text_disabled = 'style="color: #000000";' % } @@ -157,7 +163,7 @@ % ''. % % qq!!. -% qq!!. +% qq!!. % % qq!!. % '!. -% '!. @@ -188,7 +194,7 @@ % 'end_year' => (localtime())[5] + 1900, % 'selected_date' => ( % ( $payby =~ /^(CARD|DCRD)$/ -% && cardtype($payinfo) =~ /^(Switch|Solo)$/ ) +% && cardtype($paymask) =~ /^(Switch|Solo)$/ ) #also % ? $cust_main->paystart_month. '-'. % $cust_main->paystart_year % : '' @@ -236,7 +242,7 @@ % '
${r}Card number
${r}Expiration '. @@ -174,7 +180,7 @@ % % qq!(help)!. % qq!'. +% ''. % % % qq!
Start date
'. % % qq!!. -% qq!!. +% qq!!. % % qq!!. % qq!!. @@ -256,7 +262,7 @@ % '
${r}Phone number
'. % % qq!!. -% qq!!. +% qq!!. % % qq!!. % qq!!. diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi index 2b3e02614..aa825af94 100755 --- a/httemplate/edit/cust_refund.cgi +++ b/httemplate/edit/cust_refund.cgi @@ -1,4 +1,3 @@ - % % %my $conf = new FS::Conf; @@ -26,70 +25,102 @@ % %my $p1 = popurl(1); % -%print header('Refund '. ucfirst(lc($payby)). ' payment', ''); -%print qq!Error: !, $cgi->param('error'), -% "" -% if $cgi->param('error'); -%print <config('countrydefault')); -% -% -% -% -% -% -% -% -% -%
-%END % -%if ( $cust_pay ) { + + +<% include('/elements/header.html', 'Refund '. ucfirst(lc($payby)). ' payment', '') %> +% if ( $cgi->param('error') ) { + + Error: <% $cgi->param('error') %> +

+% } + + +<% small_custview($custnum, $conf->config('countrydefault')) %> + + + + + + + + + + +
+% if ( $cust_pay ) { % % #false laziness w/FS/FS/cust_pay.pm % my $payby = $cust_pay->payby; -% my $payinfo = $cust_pay->payinfo; -% $payby =~ s/^BILL$/Check/ if $payinfo; +% my $paymask = $cust_pay->paymask; +% $payby =~ s/^BILL$/Check/ if $paymask; % $payby =~ s/^CHEK$/Electronic check/; -% $payinfo = $cust_pay->payinfo_masked if $payby eq 'CARD'; % -% print '
Payment'. ntable("#cccccc", 2). -% '
'. -% ''. -% ''; +% + + +
Payment + <% ntable("#cccccc", 2) %> + + + + + + + + + + + + +% % #false laziness w/FS/FS/cust_main::realtime_refund_bop % if ( $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/ ) { % my ( $processor, $auth, $order_number ) = ( $1, $2, $4 ); -% print ''; -% print '' -% if length($auth); -% print '' -% if length($order_number); -% } -% print '
P.O.
Amount$'. -% $cust_pay->paid. '
Date'. -% time2str("%D",$cust_pay->_date). '
Method'. -% ucfirst(lc($payby)). ' # '. $payinfo. '
Amount$<% $cust_pay->paid %>
Date<% time2str("%D",$cust_pay->_date) %>
Method<% ucfirst(lc($payby)) %> # <% $paymask %>
Processor'. -% $processor. '
Authorization'. -% $auth. '
Order number'. -% $order_number. '
'; -%} -% -%print '
Refund'. ntable("#cccccc", 2). -% 'Date'. -% time2str("%D",$_date). ''; -% -%print qq!Amount\$!; -% -%print qq!Reason!; -% -%print < -%
-% -% -% -% -%END -% -% +% + + + + Processor<% $processor %> + +% if ( length($auth) ) { + + + Authorization<% $auth %> + +% } +% if ( length($order_number) ) { + + + Order number<% $order_number %> + +% } +% } + + +% } + + +
Refund +<% ntable("#cccccc", 2) %> + + + Date<% time2str("%D",$_date) %> + + + + Amount$ + + + + Reason + + + +
+ + + + +<% include('/elements/footer.html') %> diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index d5d127b2d..789f29522 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -46,6 +46,9 @@ % } fields('cust_main') %} ); % +% delete( $new->hashref->{'agent_custid'} ) +% unless $new->hashref->{'agent_custid'}; +% %if ( defined($cgi->param('same')) && $cgi->param('same') eq "Y" ) { % $new->setfield("ship_$_", '') foreach qw( % last first company address1 address2 city county state zip @@ -151,6 +154,9 @@ % && $new->paycvv =~ /^\s*\*+\s*$/ ) { % $new->paycvv($old->paycvv); % } +% if ($new->payby =~ /CARD|DCRD|CHEK|DCHK/ && $new->payinfo =~ /xx/) { +% $new->payinfo($old->payinfo); +% } % $error ||= $new->replace($old, \@invoicing_list); % %} diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi index 4c6ae3349..081964e93 100644 --- a/httemplate/misc/payment.cgi +++ b/httemplate/misc/payment.cgi @@ -67,7 +67,7 @@ function OLiframeContent(src, width, height, name) { % my $state = $cust_main->state; % my $zip = $cust_main->zip; % if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { -% $payinfo = $cust_main->payinfo; +% $payinfo = $cust_main->paymask; % $paycvv = $cust_main->paycvv; % ( $month, $year ) = $cust_main->paydate_monthyear; % $payname = $cust_main->payname if $cust_main->payname; @@ -153,7 +153,7 @@ function OLiframeContent(src, width, height, name) { % } elsif ( $payby eq 'CHEK' ) { % my( $payinfo1, $payinfo2, $payname, $ss ) = ( '', '', '', '' ); % if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) { -% $cust_main->payinfo =~ /^(\d+)\@(\d+)$/ +% $cust_main->paymask =~ /^(\d+)\@(\d+)$/ % or die "unparsable payinfo ". $cust_main->payinfo; % ($payinfo1, $payinfo2) = ($1, $2); % $payname = $cust_main->payname; diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 027cd502a..d591db1e8 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -44,17 +44,24 @@ %my $paycvv = ''; %if ( $payby eq 'CHEK' ) { % -% $cgi->param('payinfo1') =~ /^(\d+)$/ -% or eidiot "illegal account number ". $cgi->param('payinfo1'); -% my $payinfo1 = $1; -% $cgi->param('payinfo2') =~ /^(\d+)$/ -% or eidiot "illegal ABA/routing number ". $cgi->param('payinfo2'); -% my $payinfo2 = $1; -% $payinfo = $payinfo1. '@'. $payinfo2; +% if ($cgi->param('payinfo1') =~ /xx/ || $cgi->param('payinfo2') =~ /xx/ ) { +% $payinfo = $cust_main->payinfo; +% } else { +% $cgi->param('payinfo1') =~ /^(\d+)$/ +% or eidiot "illegal account number ". $cgi->param('payinfo1'); +% my $payinfo1 = $1; +% $cgi->param('payinfo2') =~ /^(\d+)$/ +% or eidiot "illegal ABA/routing number ". $cgi->param('payinfo2'); +% my $payinfo2 = $1; +% $payinfo = $payinfo1. '@'. $payinfo2; +% } % %} elsif ( $payby eq 'CARD' ) { % % $payinfo = $cgi->param('payinfo'); +% if ($payinfo eq $cust_main->paymask) { +% $payinfo = $cust_main->payinfo; +% } % $payinfo =~ s/\D//g; % $payinfo =~ /^(\d{13,16})$/ % or eidiot gettext('invalid_card'); # . ": ". $self->payinfo; diff --git a/httemplate/search/cust_pay.cgi b/httemplate/search/cust_pay.cgi index 87da1791b..98cc0e799 100755 --- a/httemplate/search/cust_pay.cgi +++ b/httemplate/search/cust_pay.cgi @@ -164,7 +164,7 @@ sub { my $cust_pay = shift; if ( $cust_pay->payby eq 'CARD' ) { - 'Card #'. $cust_pay->payinfo_masked; + 'Card #'. $cust_pay->paymask; } elsif ( $cust_pay->payby eq 'CHEK' ) { 'E-check acct#'. $cust_pay->payinfo; } elsif ( $cust_pay->payby eq 'BILL' ) { diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index d1be8936f..1f80dc5bc 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -7,7 +7,11 @@ Billing information -(Bill now) +% # If we can't see the unencrypted card, then bill now is an exercise in frustration +%if ( ! $cust_main->is_encrypted($cust_main->payinfo) ) { + (Bill now) +% } + <% ntable("#cccccc") %><% ntable("#cccccc",2) %> % %( my $balance = $cust_main->balance ) @@ -31,7 +35,7 @@ Billing information Card number - <% $cust_main->payinfo_masked %> + <% $cust_main->paymask %> % %#false laziness w/elements/select-month_year.html & edit/cust_main/billing.html diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index 4f46ace6d..57d11b9e8 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -18,6 +18,7 @@ % if ( ( $payby{'CARD'} || $payby{'DCRD'} ) % && $curuser->access_right('Process payment') +% && ! $cust_main->is_encrypted($cust_main->payinfo) % ) { <% $s++ ? ' | ' : '' %> Process credit card payment @@ -25,6 +26,7 @@ % if ( ( $payby{'CHEK'} || $payby{'DCHK'} ) % && $curuser->access_right('Process payment') +% && ! $cust_main->is_encrypted($cust_main->payinfo) % ) { <% $s++ ? ' | ' : '' %> Process electronic check (ACH) payment @@ -76,7 +78,7 @@ % % my $payinfo; % if ( $payby eq 'CARD' ) { -% $payinfo = $cust_pay->payinfo_masked; +% $payinfo = $cust_pay->paymask; % } elsif ( $payby eq 'CHEK' && $cust_pay->payinfo =~ /^(\d+)\@(\d+)$/ ) { % $payinfo = "ABA $2, Acct# $1"; % } else { -- 2.11.0