add CASH and WEST payment types (payments only, not cust_main.payby)
[freeside.git] / FS / FS / cust_main.pm
index 0169039..d131af9 100644 (file)
@@ -14,6 +14,7 @@ BEGIN {
   #eval "use Time::Local qw(timelocal timelocal_nocheck);";
   eval "use Time::Local qw(timelocal_nocheck);";
 }
+use Digest::MD5 qw(md5_base64);
 use Date::Format;
 #use Date::Manip;
 use String::Approx qw(amatch);
@@ -21,6 +22,7 @@ use Business::CreditCard 0.28;
 use FS::UID qw( getotaker dbh );
 use FS::Record qw( qsearchs qsearch dbdef );
 use FS::Misc qw( send_email );
+use FS::Msgcat qw(gettext);
 use FS::cust_pkg;
 use FS::cust_svc;
 use FS::cust_bill;
@@ -44,7 +46,7 @@ use FS::cust_tax_exempt;
 use FS::type_pkgs;
 use FS::payment_gateway;
 use FS::agent_payment_gateway;
-use FS::Msgcat qw(gettext);
+use FS::banned_pay;
 
 @ISA = qw( FS::Record );
 
@@ -242,7 +244,7 @@ sub paymask {
   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)
+    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)
@@ -358,6 +360,7 @@ sub insert {
 
   my $prepay_identifier = '';
   my( $amount, $seconds ) = ( 0, 0 );
+  my $payby = '';
   if ( $self->payby eq 'PREPAY' ) {
 
     $self->payby('BILL');
@@ -371,6 +374,14 @@ sub insert {
       return $error;
     }
 
+    $payby = 'PREP' if $amount;
+
+  } elsif ( $self->payby =~ /^(CASH|WEST)$/ ) {
+
+    $payby = $1;
+    $self->payby('BILL');
+    $amount = $self->paid;
+
   }
 
   my $error = $self->SUPER::insert;
@@ -403,13 +414,15 @@ sub insert {
   }
 
   if ( $amount ) {
-    $error = $self->insert_cust_pay_prepay($amount, $prepay_identifier);
+    $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "inserting prepayment (transaction rolled back): $error";
+      return "inserting payment (transaction rolled back): $error";
     }
   }
 
+
+
   unless ( $import || $skip_fuzzyfiles ) {
     $error = $self->queue_fuzzyfiles_update;
     if ( $error ) {
@@ -689,14 +702,42 @@ If there is an error, returns the error, otherwise returns false.
 =cut
 
 sub insert_cust_pay_prepay {
-  my( $self, $amount ) = splice(@_, 0, 2);
+  shift->insert_cust_pay('PREP', @_);
+}
+
+=item insert_cust_pay_cash AMOUNT [ PAYINFO ]
+
+Inserts a cash payment in the specified amount for this customer.  An optional
+second argument can specify the payment identifier for tracking purposes.
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub insert_cust_pay_cash {
+  shift->insert_cust_pay('CASH', @_);
+}
+
+=item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
+
+Inserts a Western Union payment in the specified amount for this customer.  An
+optional second argument can specify the prepayment identifier for tracking
+purposes.  If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub insert_cust_pay_west {
+  shift->insert_cust_pay('WEST', @_);
+}
+
+sub insert_cust_pay {
+  my( $self, $payby, $amount ) = splice(@_, 0, 3);
   my $payinfo = scalar(@_) ? shift : '';
 
   my $cust_pay = new FS::cust_pay {
     'custnum' => $self->custnum,
     'paid'    => sprintf('%.2f', $amount),
     #'_date'   => #date the prepaid card was purchased???
-    'payby'   => 'PREP',
+    'payby'   => $payby,
     'payinfo' => $payinfo,
   };
   $cust_pay->insert;
@@ -1080,7 +1121,7 @@ sub check {
        } ) ) {
         return "Unknown ship_state/ship_county/ship_country: ".
           $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
-          unless qsearchs('cust_main_county',{
+          unless qsearch('cust_main_county',{
             'state'   => $self->ship_state,
             'county'  => $self->ship_county,
             'country' => $self->ship_country,
@@ -1103,7 +1144,7 @@ sub check {
     }
   }
 
-  $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
+  $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST)$/
     or return "Illegal payby: ". $self->payby;
 
   $error =    $self->ut_numbern('paystart_month')
@@ -1140,8 +1181,13 @@ sub check {
     $self->payinfo($payinfo);
     validate($payinfo)
       or return gettext('invalid_card'); # . ": ". $self->payinfo;
+
     return gettext('unknown_card_type')
       if cardtype($self->payinfo) eq "Unknown";
+
+    my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
+    return "Banned credit card" if $ban;
+
     if ( defined $self->dbdef_table->column('paycvv') ) {
       if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
         if ( cardtype($self->payinfo) eq 'American Express card' ) {
@@ -1191,6 +1237,9 @@ sub check {
     $self->payinfo($payinfo);
     $self->paycvv('') if $self->dbdef_table->column('paycvv');
 
+    my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
+    return "Banned ACH account" if $ban;
+
   } elsif ( $self->payby eq 'LECB' ) {
 
     my $payinfo = $self->payinfo;
@@ -1232,7 +1281,7 @@ sub check {
 
   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
     return "Expriation date required"
-      unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
+      unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB|CASH|WEST)$/;
     $self->paydate('');
   } else {
     my( $m, $y );
@@ -1428,19 +1477,56 @@ sub suspend_unless_pkgpart {
 
 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
 
-Available options are: I<quiet>
+Available options are: I<quiet>, I<reasonnum>, and I<ban>
 
 I<quiet> can be set true to supress email cancellation notices.
 
+# I<reasonnum> can be set to a cancellation reason (see L<FS::cancel_reason>)
+
+I<ban> can be set true to ban this customer's credit card or ACH information,
+if present.
+
 Always returns a list: an empty list on success or a list of errors.
 
 =cut
 
 sub cancel {
   my $self = shift;
+  my %opt = @_;
+
+  if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
+
+    #should try decryption (we might have the private key)
+    # and if not maybe queue a job for the server that does?
+    return ( "Can't (yet) ban encrypted credit cards" )
+      if $self->is_encrypted($self->payinfo);
+
+    my $ban = new FS::banned_pay $self->_banned_pay_hashref;
+    my $error = $ban->insert;
+    return ( $error ) if $error;
+
+  }
+
   grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
 }
 
+sub _banned_pay_hashref {
+  my $self = shift;
+
+  my %payby2ban = (
+    'CARD' => 'CARD',
+    'DCRD' => 'CARD',
+    'CHEK' => 'CHEK',
+    'DCHK' => 'CHEK'
+  );
+
+  {
+    'payby'   => $payby2ban{$self->payby},
+    'payinfo' => md5_base64($self->payinfo),
+    #'reason'  =>
+  };
+}
+
 =item agent
 
 Returns the agent (see L<FS::agent>) for this customer.
@@ -1586,6 +1672,9 @@ sub bill {
       } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
         my $days = $1;
         $mday += $days;
+      } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
+        my $hours = $1;
+        $hour += $hours;
       } else {
         $dbh->rollback if $oldAutoCommit;
         return "unparsable frequency: ". $part_pkg->freq;
@@ -2529,7 +2618,7 @@ sub realtime_refund_bop {
       or return "Unknown paynum $options{'paynum'}";
     $amount ||= $cust_pay->paid;
 
-    $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):([\w-]*)(:(\w+))?$/
+    $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-]*)(:([\w\-]+))?$/
       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
                 $cust_pay->paybatch;
     my $gatewaynum = '';