fix zip parsing for batch results - don't want to abort processing because of an...
[freeside.git] / FS / FS / cust_pay_batch.pm
index e057334..6e261c6 100644 (file)
@@ -3,9 +3,11 @@ package FS::cust_pay_batch;
 use strict;
 use vars qw( @ISA $DEBUG );
 use FS::Record qw(dbh qsearch qsearchs);
-use Business::CreditCard;
+use FS::payinfo_Mixin;
+use FS::part_bill_event qw(due_events);
+use Business::CreditCard 0.28;
 
-@ISA = qw( FS::Record );
+@ISA = qw( FS::Record FS::payinfo_Mixin );
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -31,6 +33,8 @@ FS::cust_pay_batch - Object methods for batch cards
 
   $error = $record->check;
 
+  $error = $record->retriable;
+
 =head1 DESCRIPTION
 
 An FS::cust_pay_batch object represents a credit card transaction ready to be
@@ -112,7 +116,7 @@ returns the error, otherwise returns false.
 
 Checks all fields to make sure this is a valid transaction.  If there is
 an error, returns the error, otherwise returns false.  Called by the insert
-and repalce methods.
+and replace methods.
 
 =cut
 
@@ -121,7 +125,7 @@ sub check {
 
   my $error = 
       $self->ut_numbern('paybatchnum')
-    || $self->ut_numbern('trancode') #depriciated
+    || $self->ut_numbern('trancode') #deprecated
     || $self->ut_money('amount')
     || $self->ut_number('invnum')
     || $self->ut_number('custnum')
@@ -139,23 +143,8 @@ sub check {
   $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
   $self->first($1);
 
-  $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREP|CASH|WEST|MCRD)$/
-    or return "Illegal payby";
-  $self->payby($1);
-
-  # FIXME
-  # there is no point in false laziness here
-  # we will effectively set "check_payinfo to 0"
-  # we can change that when we finish the refactor
-  
-  #my $cardnum = $self->cardnum;
-  #$cardnum =~ s/\D//g;
-  #$cardnum =~ /^(\d{13,16})$/
-  #  or return "Illegal credit card number";
-  #$cardnum = $1;
-  #$self->cardnum($cardnum);
-  #validate($cardnum) or return "Illegal credit card number";
-  #return "Unknown card type" if cardtype($cardnum) eq "Unknown";
+  $error = $self->payinfo_check();
+  return $error if $error;
 
   if ( $self->exp eq '' ) {
     return "Expiration date required"
@@ -185,15 +174,16 @@ sub check {
     $self->payname($1);
   }
 
-  #$self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
-  #  or return "Illegal zip: ". $self->zip;
-  #$self->zip($1);
+  #we have lots of old zips in there... don't hork up batch results cause of em
+  $self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
+    or return "Illegal zip: ". $self->zip;
+  $self->zip($1);
 
   $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
   $self->country($1);
 
-  $error = $self->ut_zip('zip', $self->country);
-  return $error if $error;
+  #$error = $self->ut_zip('zip', $self->country);
+  #return $error if $error;
 
   #check invnum, custnum, ?
 
@@ -212,6 +202,54 @@ sub cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
 }
 
+=item retriable
+
+Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
+credit card payment as retriable.  Useful if the corresponding financial
+institution account was declined for temporary reasons and/or a manual 
+retry is desired.
+
+Implementation details: For the named customer's invoice, changes the
+statustext of the 'done' (without statustext) event to 'retriable.'
+
+=cut
+
+sub retriable {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';        #Hmm
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
+    or return "event $self->eventnum references nonexistant invoice $self->invnum";
+
+  warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
+  my @cust_bill_event =
+    sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
+      grep {
+        $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
+         && $_->status eq 'done'
+         && ! $_->statustext
+       }
+      $cust_bill->cust_bill_event;
+  # complain loudly if scalar(@cust_bill_event) > 1 ?
+  my $error = $cust_bill_event[0]->retriable;
+  if ($error ) {
+    # gah, even with transactions.
+    $dbh->commit if $oldAutoCommit; #well.
+    return "error marking invoice event retriable: $error";
+  }
+  '';
+}
+
 =back
 
 =head1 SUBROUTINES
@@ -306,6 +344,59 @@ sub import_results {
     };
 
 
+  }elsif ( $format eq 'csv-chase_canada-E-xactBatch' ) {
+
+    $filetype = "CSV";
+
+    @fields = (
+      '',            # Internal(bank) id of the transaction
+      '',            # Transaction Type:  00 - purchase,      01 - preauth,
+                     #                    02 - completion,    03 - forcepost,
+                     #                    04 - refund,        05 - auth,
+                     #                    06 - purchase corr, 07 - refund corr,
+                     #                    08 - void           09 - void return
+      '',            # gateway used to process this transaction
+      'paid',        # Amount:  Amount of the transaction.  Dollars and cents
+                     #          with decimal entered.
+      'auth',        # Auth#:  Authorization number (if approved)
+      'payinfo',     # Card Number:  Card number for the transaction
+      '',            # Expiry Date:  Expiry date of the card
+      '',            # Cardholder Name
+      'bankcode',    # Bank response code (3 alphanumeric)
+      'bankmess',    # Bank response message
+      'etgcode',     # ETG response code (2 alphanumeric)
+      'etgmess',     # ETG response message
+      '',            # Returned customer number for the transaction
+      'paybatchnum', # Reference#:  paybatch number of the transaction
+      '',            # Reference#:  Invoice number of the transaction
+      'result',      # Processing Result: Approved of Declined
+    );
+
+    $end_condition = sub {
+      '';
+    };
+
+    $hook = sub {
+      my $hash = shift;
+      my $cpb = shift;
+      $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'}); #hmmmm
+      $hash->{'_date'} = time;  # got a better one?
+      $hash->{'payinfo'} = $cpb->{'payinfo'}
+        if( substr($hash->{'payinfo'}, -4) eq substr($cpb->{'payinfo'}, -4) );
+    };
+
+    $approved_condition = sub {
+      my $hash = shift;
+      $hash->{'etgcode'} eq '00' && $hash->{'result'} eq "Approved";
+    };
+
+    $declined_condition = sub {
+      my $hash = shift;
+      $hash->{'etgcode'} ne '00' # internal processing error
+        || ( $hash->{'result'} eq "Declined" );
+    };
+
+
   }elsif ( $format eq 'PAP' ) {
 
     $filetype = "Fixed264";
@@ -440,7 +531,7 @@ sub import_results {
 
     my $new_cust_pay_batch = new FS::cust_pay_batch { $cust_pay_batch->hash };
 
-    &{$hook}(\%hash);
+    &{$hook}(\%hash, $cust_pay_batch->hashref);
 
     if ( &{$approved_condition}(\%hash) ) {
 
@@ -465,70 +556,19 @@ sub import_results {
 
       $new_cust_pay_batch->status('Declined');
 
-      #this should be configurable... if anybody else ever uses batches
-      # $cust_pay_batch->cust_main->suspend;
-
-      foreach my $part_bill_event (
-        sort {    $a->seconds   <=> $b->seconds
-               || $a->weight    <=> $b->weight
-               || $a->eventpart <=> $b->eventpart }
-          grep { ! qsearch( 'cust_bill_event', {
-                               'invnum'    => $cust_pay_batch->invnum,
-                               'eventpart' => $_->eventpart,
-                               'status'    => 'done',
-                                                                   } )
-               }
-            qsearch( {
-              'table'     => 'part_bill_event',
-              'hashref'   => { 'payby'    => 'DCLN',
-                               'disabled' => '',           },
-            } )
-      ) {
+      foreach my $part_bill_event ( due_events ( $new_cust_pay_batch,
+                                                 'DCLN',
+                                                '',
+                                                '') ) {
 
         # don't run subsequent events if balance<=0
         last if $cust_pay_batch->cust_main->balance <= 0;
 
-        warn "  calling invoice event (". $part_bill_event->eventcode. ")\n"
-          if $DEBUG > 1;
-        my $cust_main = $cust_pay_batch->cust_main; #for callback
-
-        my $error;
-        {
-          local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
-          $error = eval $part_bill_event->eventcode;
-        }
-
-        my $status = '';
-        my $statustext = '';
-        if ( $@ ) {
-          $status = 'failed';
-          $statustext = $@;
-        } elsif ( $error ) {
-          $status = 'done';
-          $statustext = $error;
-        } else {
-          $status = 'done'
-        }
-
-       #add cust_bill_event
-       my $cust_bill_event = new FS::cust_bill_event {
-         'invnum'     => $cust_pay_batch->invnum,
-         'eventpart'  => $part_bill_event->eventpart,
-         '_date'      => time,
-         'status'     => $status,
-         'statustext' => $statustext,
-       };
-       $error = $cust_bill_event->insert;
-       if ( $error ) {
+       if (my $error = $part_bill_event->do_event($new_cust_pay_batch)) {
          # gah, even with transactions.
          $dbh->commit if $oldAutoCommit; #well.
-          my $e = 'WARNING: Event run but database not updated - '.
-                  'error inserting cust_bill_event, invnum #'. $cust_pay_batch->invnum.
-                 ', eventpart '. $part_bill_event->eventpart.
-                  ": $error";
-          warn $e;
-          return $e;
-        }
+         return $error;
+       }
 
       }