according to landel CP no longer supports changing username
[freeside.git] / FS / FS / cust_main.pm
index 60556a5..3728a16 100644 (file)
@@ -1,7 +1,7 @@
 package FS::cust_main;
 
 use strict;
-use vars qw( @ISA @EXPORT_OK $conf $DEBUG $import );
+use vars qw( @ISA @EXPORT_OK $conf $DEBUG $import @encrypted_fields);
 use vars qw( $realtime_bop_decline_quiet ); #ugh
 use Safe;
 use Carp;
@@ -53,6 +53,8 @@ $DEBUG = 0;
 
 $import = 0;
 
+@encrypted_fields = ('payinfo', 'paycvv');
+
 #ask FS::UID to run this stuff for us later
 #$FS::UID::callback{'FS::cust_main'} = sub { 
 install_callback FS::UID sub { 
@@ -175,11 +177,84 @@ FS::Record.  The following fields are currently supported:
 
 =item ship_fax - phone (optional)
 
-=item payby - 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 payby 
+
+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 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 'DCARD') { # 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;
+}
+
 
-=item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
 
-=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
 
 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
 
@@ -271,20 +346,28 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $amount = 0;
+  my $prepay_credit = '';
   my $seconds = 0;
   if ( $self->payby eq 'PREPAY' ) {
     $self->payby('BILL');
-    my $prepay_credit = qsearchs(
+    $prepay_credit = qsearchs(
       'prepay_credit',
       { 'identifier' => $self->payinfo },
       '',
       'FOR UPDATE'
     );
-    warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
-      unless $prepay_credit;
-    $amount = $prepay_credit->amount;
+    unless ( $prepay_credit ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Invalid prepaid card: ". $self->payinfo;
+    }
     $seconds = $prepay_credit->seconds;
+    if ( $prepay_credit->agentnum ) {
+      if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "prepaid card not valid for agent ". $self->agentnum;
+      }
+      $self->agentnum($prepay_credit->agentnum);
+    }
     my $error = $prepay_credit->delete;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -321,15 +404,18 @@ sub insert {
     return "No svc_acct record to apply pre-paid time";
   }
 
-  if ( $amount ) {
-    my $cust_credit = new FS::cust_credit {
+  if ( $prepay_credit && $prepay_credit->amount ) {
+    my $cust_pay = new FS::cust_pay {
       'custnum' => $self->custnum,
-      'amount'  => $amount,
+      'paid'    => $prepay_credit->amount,
+      #'_date'   => #date the prepaid card was purchased???
+      'payby'   => 'PREP',
+      'payinfo' => $prepay_credit->identifier,
     };
-    $error = $cust_credit->insert;
+    $error = $cust_pay->insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "inserting credit (transaction rolled back): $error";
+      return "inserting prepayment (transaction rolled back): $error";
     }
   }
 
@@ -591,6 +677,16 @@ 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 } );
+  }
+
   if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
        && $conf->config('users-allow_comp')                  ) {
     return "You are not permitted to create complimentary accounts."
@@ -684,7 +780,7 @@ sub queue_fuzzyfiles_update {
 
 Checks all fields to make sure this is a valid customer record.  If there is
 an error, returns the error, otherwise returns false.  Called by the insert
-and repalce methods.
+and replace methods.
 
 =cut
 
@@ -815,9 +911,19 @@ sub check {
 
   $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
     or return "Illegal payby: ". $self->payby;
+
+  # If it is encrypted and the private key is not availaible then we can't
+  # check the credit card.
+
+  my $check_payinfo = 1;
+
+  if ($self->is_encrypted($self->payinfo)) {
+    $check_payinfo = 0;
+  }
+
   $self->payby($1);
 
-  if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
+  if ( $check_payinfo && ($self->payby eq 'CARD' || $self->payby eq 'DCRD')) {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/\D//g;
@@ -830,7 +936,7 @@ sub check {
     return gettext('unknown_card_type')
       if cardtype($self->payinfo) eq "Unknown";
     if ( defined $self->dbdef_table->column('paycvv') ) {
-      if ( length($self->paycvv) ) {
+      if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
         if ( cardtype($self->payinfo) eq 'American Express card' ) {
           $self->paycvv =~ /^(\d{4})$/
             or return "CVV2 (CID) for American Express cards is four digits.";
@@ -845,7 +951,7 @@ sub check {
       }
     }
 
-  } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
+  } elsif ($check_payinfo && ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' )) {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/[^\d\@]//g;
@@ -918,7 +1024,7 @@ sub check {
   ) {
     $self->payname( $self->first. " ". $self->getfield('last') );
   } else {
-    $self->payname =~ /^([\w \,\.\-\']+)$/
+    $self->payname =~ /^([\w \,\.\-\'\&]+)$/
       or return gettext('illegal_name'). " payname: ". $self->payname;
     $self->payname($1);
   }
@@ -2111,6 +2217,7 @@ sub realtime_refund_bop {
 
   #first try void if applicable
   if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
+    warn "FS::cust_main::realtime_bop: attempting void\n" if $DEBUG;
     my $void = new Business::OnlinePayment( $processor, @bop_options );
     $void->content( 'action' => 'void', %content );
     $void->submit();
@@ -2123,10 +2230,14 @@ sub realtime_refund_bop {
         warn $e;
         return $e;
       }
+      warn "FS::cust_main::realtime_bop: void successful\n" if $DEBUG;
       return '';
     }
   }
 
+  warn "FS::cust_main::realtime_bop: void unsuccessful, trying refund\n"
+    if $DEBUG;
+
   #massage data
   my $address = $self->address1;
   $address .= ", ". $self->address2 if $self->address2;
@@ -2143,36 +2254,34 @@ sub realtime_refund_bop {
     $payname =  "$payfirst $paylast";
   }
 
-  if ( $method eq 'CC' ) { 
-
-    $content{card_number} = $self->payinfo;
-    $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
-    $content{expiration} = "$2/$1";
+  my $payinfo = '';
+  if ( $method eq 'CC' ) {
 
-    #$content{cvv2} = $self->paycvv
-    #  if defined $self->dbdef_table->column('paycvv')
-    #     && length($self->paycvv);
-
-    #$content{recurring_billing} = 'YES'
-    #  if qsearch('cust_pay', { 'custnum' => $self->custnum,
-    #                           'payby'   => 'CARD',
-    #                           'payinfo' => $self->payinfo, } );
+    if ( $cust_pay ) {
+      $content{card_number} = $payinfo = $cust_pay->payinfo;
+      #$self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+      #$content{expiration} = "$2/$1";
+    } else {
+      $content{card_number} = $payinfo = $self->payinfo;
+      $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+      $content{expiration} = "$2/$1";
+    }
 
   } elsif ( $method eq 'ECHECK' ) {
     ( $content{account_number}, $content{routing_code} ) =
-      split('@', $self->payinfo);
+      split('@', $payinfo = $self->payinfo);
     $content{bank_name} = $self->payname;
     $content{account_type} = 'CHECKING';
     $content{account_name} = $payname;
     $content{customer_org} = $self->company ? 'B' : 'I';
     $content{customer_ssn} = $self->ss;
   } elsif ( $method eq 'LEC' ) {
-    $content{phone} = $self->payinfo;
+    $content{phone} = $payinfo = $self->payinfo;
   }
 
   #then try refund
   my $refund = new Business::OnlinePayment( $processor, @bop_options );
-  $refund->content(
+  my %sub_content = $refund->content(
     'action'         => 'credit',
     'customer_id'    => $self->custnum,
     'last_name'      => $paylast,
@@ -2185,6 +2294,8 @@ sub realtime_refund_bop {
     'country'        => $self->country,
     %content, #after
   );
+  warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
+    if $DEBUG > 1;
   $refund->submit();
 
   return "$processor error: ". $refund->error_message
@@ -2214,7 +2325,7 @@ sub realtime_refund_bop {
     'refund'   => $amount,
     '_date'    => '',
     'payby'    => $method2payby{$method},
-    'payinfo'  => $self->payinfo,
+    'payinfo'  => $payinfo,
     'paybatch' => $paybatch,
     'reason'   => $options{'reason'} || 'card or ACH refund',
   } );
@@ -2462,15 +2573,17 @@ sub paydate_monthyear {
 
 =item payinfo_masked
 
-Returns a "masked" payinfo field with all but the last four characters replaced
-by 'x'es.  Useful for displaying credit cards.
+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;
-  my $payinfo = $self->payinfo;
-  'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
+  return $self->paymask;
 }
 
 =item invoicing_list [ ARRAYREF ]
@@ -2542,6 +2655,11 @@ is an error, returns the error, otherwise returns false.
 sub check_invoicing_list {
   my( $self, $arrayref ) = @_;
   foreach my $address ( @{$arrayref} ) {
+
+    if ($address eq 'FAX' and $self->getfield('fax') eq '') {
+      return 'Can\'t add FAX invoice destination with a blank FAX number.';
+    }
+
     my $cust_main_invoice = new FS::cust_main_invoice ( {
       'custnum' => $self->custnum,
       'dest'    => $address,