summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorivan <ivan>2006-12-14 06:00:46 +0000
committerivan <ivan>2006-12-14 06:00:46 +0000
commitdcdf657e77ec7b46dc69e19a849a9c133123db7c (patch)
treecfbe516c1113dbe86af59ab9288eccc8c71f96ab /FS
parentbd368448838fb00212fa34d70e467cf4c8e12206 (diff)
encryption fixes from huntsberg & jayce
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm4
-rw-r--r--FS/FS/Record.pm56
-rw-r--r--FS/FS/Schema.pm9
-rw-r--r--FS/FS/cust_main.pm87
-rw-r--r--FS/FS/cust_pay.pm66
-rw-r--r--FS/FS/cust_pay_void.pm11
-rw-r--r--FS/FS/cust_refund.pm47
-rw-r--r--FS/FS/payinfo_Mixin.pm245
-rw-r--r--FS/MANIFEST2
-rw-r--r--FS/t/payinfo_Mixin.t5
10 files changed, 357 insertions, 175 deletions
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<FS::payinfo_Mixin> for valid payby values)
-I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>)
-
-=item payinfo
-
-Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
-
-=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<FS::payinfo_Mixin> for data format)
+=item paymask - Masked payinfo (See L<FS::payinfo_Mixin> 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<perlfunc/"time">. Also see
L<Time::Local> and L<Date::Parse> 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<FS::payinfo_Mixin> for valid payby values)
+
+=item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
-=item payinfo - card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively
+=item paymask - Masked payinfo (See L<FS::payinfo_Mixin> 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.
+<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.
+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<perlfunc/"time">. Also see
L<Time::Local> and L<Date::Parse> 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<FS::payinfo_Mixin> for valid payby values)
+
+=item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
-=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
+=item paymask - Masked payinfo (See L<FS::payinfo_Mixin> 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
+<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.
@@ -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<FS::prepay_credit> and sets billing type to I<BILL>)
+
+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<FS::prepay_credit>)
+
+=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<FS::prepay_credit> and sets billing type to I<BILL>)
+
+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<FS::Record>
+
+=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";