RT# 81961 Repair broken links in POD documentation
[freeside.git] / FS / FS / cust_pay_batch.pm
index 9fa1459..3cc8a7c 100644 (file)
@@ -8,14 +8,20 @@ use FS::Record qw(dbh qsearch qsearchs);
 use FS::payinfo_Mixin;
 use FS::cust_main;
 use FS::cust_bill;
+use Storable qw( thaw );
+use MIME::Base64 qw( decode_base64 );
 
-@ISA = qw( FS::payinfo_Mixin FS::Record );
+
+@ISA = qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
 # 3 is even more information including possibly sensitive data
 $DEBUG = 0;
 
+#@encrypted_fields = ('payinfo');
+sub nohistory_fields { ('payinfo'); }
+
 =head1 NAME
 
 FS::cust_pay_batch - Object methods for batch cards
@@ -80,7 +86,9 @@ following fields are currently supported:
 
 =item country 
 
-=item status
+=item status - 'Approved' or 'Declined'
+
+=item error_message - the error returned by the gateway if any
 
 =back
 
@@ -125,6 +133,8 @@ and replace methods.
 sub check {
   my $self = shift;
 
+  my $conf = new FS::Conf;
+
   my $error = 
       $self->ut_numbern('paybatchnum')
     || $self->ut_numbern('trancode') #deprecated
@@ -133,7 +143,9 @@ sub check {
     || $self->ut_number('custnum')
     || $self->ut_text('address1')
     || $self->ut_textn('address2')
-    || $self->ut_text('city')
+    || ($conf->exists('cust_main-no_city_in_address') 
+        ? $self->ut_textn('city') 
+        : $self->ut_text('city'))
     || $self->ut_textn('state')
   ;
 
@@ -204,6 +216,35 @@ sub cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
 }
 
+=item expmmyy
+
+Returns the credit card expiration date in MMYY format.  If this is a 
+CHEK payment, returns an empty string.
+
+=cut
+
+sub expmmyy {
+  my $self = shift;
+  if ( $self->payby eq 'CARD' ) {
+    $self->get('exp') =~ /^(\d{4})-(\d{2})-(\d{2})$/;
+    return sprintf('%02u%02u', $2, ($1 % 100));
+  }
+  else {
+    return '';
+  }
+}
+
+=item pay_batch
+
+Returns the payment batch this payment belongs to (L<FS::pay_batch>).
+
+=cut
+
+sub pay_batch {
+  my $self = shift;
+  FS::pay_batch->by_key($self->batchnum);
+}
+
 #you know what, screw this in the new world of events.  we should be able to
 #get the event defs to retry (remove once.pm condition, add every.pm) without
 #mucking about with statuses of previous cust_event records.  right?
@@ -260,37 +301,48 @@ sub retriable {
   '';
 }
 
-=item approve PAYBATCH
+=item approve OPTIONS
 
 Approve this payment.  This will replace the existing record with the 
 same paybatchnum, set its status to 'Approved', and generate a payment 
 record (L<FS::cust_pay>).  This should only be called from the batch 
 import process.
 
+OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
+
 =cut
 
 sub approve {
   # to break up the Big Wall of Code that is import_results
   my $new = shift;
-  my $paybatch = shift;
+  my %opt = @_;
   my $paybatchnum = $new->paybatchnum;
   my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
-    or return "paybatchnum $paybatchnum not found";
-  return "paybatchnum $paybatchnum already resolved ('".$old->status."')" 
+    or return "cannot approve, paybatchnum $paybatchnum not found";
+  # leave these restrictions in place until TD EFT is converted over
+  # to B::BP
+  return "cannot approve paybatchnum $paybatchnum, already resolved ('".$old->status."')" 
     if $old->status;
   $new->status('Approved');
   my $error = $new->replace($old);
   if ( $error ) {
-    return "error updating status of paybatchnum $paybatchnum: $error\n";
+    return "error approving paybatchnum $paybatchnum: $error\n";
   }
   my $cust_pay = new FS::cust_pay ( {
       'custnum'   => $new->custnum,
       'payby'     => $new->payby,
-      'paybatch'  => $paybatch,
       'payinfo'   => $new->payinfo || $old->payinfo,
+      'paymask'   => $new->mask_payinfo,
       'paid'      => $new->paid,
       '_date'     => $new->_date,
+      'usernum'   => $new->usernum,
+      'batchnum'  => $new->batchnum,
+      'gatewaynum'    => $opt{'gatewaynum'},
+      'processor'     => $opt{'processor'},
+      'auth'          => $opt{'auth'},
+      'order_number'  => $opt{'order_number'} 
     } );
+
   $error = $cust_pay->insert;
   if ( $error ) {
     return "error inserting payment for paybatchnum $paybatchnum: $error\n";
@@ -299,25 +351,59 @@ sub approve {
   return;
 }
 
-=item decline
+=item decline [ REASON ]
 
 Decline this payment.  This will replace the existing record with the 
 same paybatchnum, set its status to 'Declined', and run collection events
 as appropriate.  This should only be called from the batch import process.
+
+REASON is a string description of the decline reason, defaulting to 
+'Returned payment'.
 
 =cut
+
 sub decline {
   my $new = shift;
+  my $reason = shift || 'Returned payment';
+  #my $conf = new FS::Conf;
+
   my $paybatchnum = $new->paybatchnum;
   my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
-    or return "paybatchnum $paybatchnum not found";
-  return "paybatchnum $paybatchnum already resolved ('".$old->status."')" 
-    if $old->status;
+    or return "cannot decline, paybatchnum $paybatchnum not found";
+  if ( $old->status ) {
+    # Handle the case where payments are rejected after the batch has been 
+    # approved.  FS::pay_batch::import_results won't allow results to be 
+    # imported to a closed batch unless batch-manual_approval is enabled, 
+    # so we don't check it here.
+#    if ( $conf->exists('batch-manual_approval') and
+    if ( lc($old->status) eq 'approved' ) {
+      # Void the payment
+      my $cust_pay = qsearchs('cust_pay', { 
+          custnum  => $new->custnum,
+          batchnum => $new->batchnum
+        });
+      # these should all be migrated over, but if it's not found, look for
+      # batchnum in the 'paybatch' field also
+      $cust_pay ||= qsearchs('cust_pay', { 
+          custnum  => $new->custnum,
+          paybatch => $new->batchnum
+        });
+      if ( !$cust_pay ) {
+        # should never happen...
+        return "failed to revoke paybatchnum $paybatchnum, payment not found";
+      }
+      $cust_pay->void($reason);
+    }
+    else {
+      # normal case: refuse to do anything
+      return "cannot decline paybatchnum $paybatchnum, already resolved ('".$old->status."')";
+    }
+  } # !$old->status
   $new->status('Declined');
+  $new->error_message($reason);
   my $error = $new->replace($old);
   if ( $error ) {
-    return "error updating status of paybatchnum $paybatchnum: $error\n";
+    return "error declining paybatchnum $paybatchnum: $error\n";
   }
   my $due_cust_event = $new->cust_main->due_cust_event(
     'eventtable'  => 'cust_pay_batch',
@@ -336,6 +422,144 @@ sub decline {
   return;
 }
 
+=item request_item [ OPTIONS ]
+
+Returns a L<Business::BatchPayment::Item> object for this batch payment
+entry.  This can be submitted to a processor.
+
+OPTIONS can be a list of key/values to append to the attributes.  The most
+useful case of this is "process_date" to set a processing date based on the
+date the batch is being submitted.
+
+=cut
+
+sub request_item {
+  local $@;
+  my $self = shift;
+
+  eval "use Business::BatchPayment;";
+  die "couldn't load Business::BatchPayment: $@" if $@;
+
+  my $cust_main = $self->cust_main;
+  my $location = $cust_main->bill_location;
+  my $pay_batch = $self->pay_batch;
+
+  my %payment;
+  $payment{payment_type} = FS::payby->payby2bop( $pay_batch->payby );
+  if ( $payment{payment_type} eq 'CC' ) {
+    $payment{card_number} = $self->payinfo,
+    $payment{expiration}  = $self->expmmyy,
+  } elsif ( $payment{payment_type} eq 'ECHECK' ) {
+    $self->payinfo =~ /(\d+)@(\d+)/; # or else what?
+    $payment{account_number} = $1;
+    $payment{routing_code} = $2;
+    $payment{account_type} = $cust_main->paytype;
+    # XXX what if this isn't their regular payment method?
+  } else {
+    die "unsupported BatchPayment method: ".$pay_batch->payby;
+  }
+
+  my $recurring;
+  if ( $cust_main->status =~ /^active|suspended|ordered$/ ) {
+    if ( $self->payinfo_used ) {
+      $recurring = 'S'; # subsequent
+    } else {
+      $recurring = 'F'; # first use
+    }
+  } else {
+    $recurring = 'N'; # non-recurring
+  }
+
+  Business::BatchPayment->create(Item =>
+    # required
+    action      => 'payment',
+    tid         => $self->paybatchnum,
+    amount      => $self->amount,
+
+    # customer info
+    customer_id => $self->custnum,
+    first_name  => $cust_main->first,
+    last_name   => $cust_main->last,
+    company     => $cust_main->company,
+    address     => $location->address1,
+    ( map { $_ => $location->$_ } qw(address2 city state country zip) ),
+    
+    invoice_number  => $self->invnum,
+    recurring_billing => $recurring,
+    %payment,
+  );
+}
+
+=item process_unbatch_and_delete
+
+L</unbatch_and_delete> run as a queued job, accepts I<$job> and I<$param>.
+
+=cut
+
+sub process_unbatch_and_delete {
+  my ($job, $param) = @_;
+  $param = thaw(decode_base64($param));
+  my $self = qsearchs('cust_pay_batch',{ 'paybatchnum' => scalar($param->{'paybatchnum'}) })
+    or die 'Could not find paybatchnum ' . $param->{'paybatchnum'};
+  my $error = $self->unbatch_and_delete;
+  die $error if $error;
+  return '';
+}
+
+=item unbatch_and_delete
+
+May only be called on a record with an empty status and an associated
+L<FS::pay_batch> with a status of 'O' (not yet in transit.)  Deletes all associated
+records from L<FS::cust_bill_pay_batch> and then deletes this record.
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub unbatch_and_delete {
+  my $self = shift;
+
+  return 'Cannot unbatch a cust_pay_batch with status ' . $self->status
+    if $self->status;
+
+  my $pay_batch = qsearchs('pay_batch',{ 'batchnum' => $self->batchnum })
+    or return 'Cannot find associated pay_batch record';
+
+  return 'Cannot unbatch from a pay_batch with status ' . $pay_batch->status
+    if $pay_batch->status ne 'O';
+
+  local $SIG{HUP} = 'IGNORE';
+  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;
+
+  # have not generated actual payments yet, so should be safe to delete
+  foreach my $cust_bill_pay_batch ( 
+    qsearch('cust_bill_pay_batch',{ 'paybatchnum' => $self->paybatchnum })
+  ) {
+    my $error = $cust_bill_pay_batch->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =back
 
 =head1 BUGS