freeside/FS/FS cust_main.pm,1.157,1.158 Conf.pm,1.101,1.102 cust_pay.pm,1.33,1.34

ivan ivan at pouncequick.420.am
Tue Jul 6 10:26:06 PDT 2004


Update of /home/cvs/cvsroot/freeside/FS/FS
In directory pouncequick:/tmp/cvs-serv4351/FS/FS

Modified Files:
	cust_main.pm Conf.pm cust_pay.pm 
Log Message:
payment voiding part deux & credit card refunds!

Index: cust_main.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cust_main.pm,v
retrieving revision 1.157
retrieving revision 1.158
diff -u -d -r1.157 -r1.158
--- cust_main.pm	1 Jul 2004 13:49:32 -0000	1.157
+++ cust_main.pm	6 Jul 2004 17:26:02 -0000	1.158
@@ -21,6 +21,7 @@
 use FS::cust_bill;
 use FS::cust_bill_pkg;
 use FS::cust_pay;
+use FS::cust_pay_void;
 use FS::cust_credit;
 use FS::cust_refund;
 use FS::part_referral;
@@ -1775,7 +1776,7 @@
   }
   my $email = $invoicing_list[0];
 
-  my %content;
+  my %content = ();
   if ( $method eq 'CC' ) { 
 
     $content{card_number} = $self->payinfo;
@@ -1808,8 +1809,7 @@
 
   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
 
-  my $transaction =
-    new Business::OnlinePayment( $processor, @bop_options );
+  my $transaction = new Business::OnlinePayment( $processor, @bop_options );
   $transaction->content(
     'type'           => $method,
     'login'          => $login,
@@ -1900,6 +1900,11 @@
       'LEC'    => 'LECB',
     );
 
+    my $paybatch = "$processor:". $transaction->authorization;
+    $paybatch .= ':'. $transaction->order_number
+      if $transaction->can('order_number')
+      && length($transaction->order_number);
+
     my $cust_pay = new FS::cust_pay ( {
        'custnum'  => $self->custnum,
        'invnum'   => $options{'invnum'},
@@ -1907,7 +1912,7 @@
        '_date'     => '',
        'payby'    => $method2payby{$method},
        'payinfo'  => $self->payinfo,
-       'paybatch' => "$processor:". $transaction->authorization,
+       'paybatch' => $paybatch,
     } );
     my $error = $cust_pay->insert;
     if ( $error ) {
@@ -1962,6 +1967,235 @@
 
 }
 
+=item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
+
+Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
+via a Business::OnlinePayment realtime gateway.  See
+L<http://420.am/business-onlinepayment> for supported gateways.
+
+Available methods are: I<CC>, I<ECHECK> and I<LEC>
+
+Available options are: I<amount>, I<reason>, I<paynum>
+
+Most gateways require a reference to an original payment transaction to refund,
+so you probably need to specify a I<paynum>.
+
+I<amount> defaults to the original amount of the payment if not specified.
+
+I<reason> specifies a reason for the refund.
+
+Implementation note: If I<amount> is unspecified or equal to the amount of the
+orignal payment, first an attempt is made to "void" the transaction via
+the gateway (to cancel a not-yet settled transaction) and then if that fails,
+the normal attempt is made to "refund" ("credit") the transaction via the
+gateway is attempted.
+
+#The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
+#I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
+#if set, will override the value from the customer record.
+
+#If an I<invnum> is specified, this payment (if sucessful) is applied to the
+#specified invoice.  If you don't specify an I<invnum> you might want to
+#call the B<apply_payments> method.
+
+=cut
+
+#some false laziness w/realtime_bop, not enough to make it worth merging
+#but some useful small subs should be pulled out
+sub realtime_refund_bop {
+  my( $self, $method, %options ) = @_;
+  if ( $DEBUG ) {
+    warn "$self $method refund\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
+  #pre-requisites
+  die "Real-time processing not enabled\n"
+    unless $conf->exists('business-onlinepayment');
+  eval "use Business::OnlinePayment";  
+  die $@ if $@;
+
+  ##overrides
+  #$self->set( $_ => $options{$_} )
+  #  foreach grep { exists($options{$_}) }
+  #          qw( payname address1 address2 city state zip payinfo paydate paycvv);
+
+  #load up config
+  my $bop_config = 'business-onlinepayment';
+  $bop_config .= '-ach'
+    if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
+  my ( $processor, $login, $password, $unused_action, @bop_options ) =
+    $conf->config($bop_config);
+  #$action ||= 'normal authorization';
+  pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+  die "No real-time processor is enabled - ".
+      "did you set the business-onlinepayment configuration value?\n"
+    unless $processor;
+
+  my $cust_pay = '';
+  my $amount = $options{'amount'};
+  my( $pay_processor, $auth, $order_number );
+  if ( $options{'paynum'} ) {
+    warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
+    $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
+      or return "Unknown paynum $options{'paynum'}";
+    $amount ||= $cust_pay->paid;
+    $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/
+      or return "Can't parse paybatch for paynum $options{'paynum'}: ".
+                $cust_pay->paybatch;
+    ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
+    return "processor of payment $options{'paynum'} $pay_processor does not".
+           " match current processor $processor"
+      unless $pay_processor eq $processor;
+  }
+  return "neither amount nor paynum specified" unless $amount;
+
+  #first try void if applicable
+  if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
+    my $void = new Business::OnlinePayment( $processor, @bop_options );
+    $void->content(
+      'type'           => $method,
+      'action'         => 'void',
+      'login'          => $login,
+      'password'       => $password,
+      'order_number'   => $order_number,
+      'amount'         => $amount,
+      'authorization'  => $auth,
+      'referer'        => 'http://cleanwhisker.420.am/',
+    );
+    $void->submit();
+    if ( $void->is_success ) {
+      my $error = $cust_pay->void($options{'reason'});
+      if ( $error ) {
+        # gah, even with transactions.
+        my $e = 'WARNING: Card/ACH voided but database not updated - '.
+                "error voiding payment: $error";
+        warn $e;
+        return $e;
+      }
+      return '';
+    }
+  }
+
+  #massage data
+  my $address = $self->address1;
+  $address .= ", ". $self->address2 if $self->address2;
+
+  my($payname, $payfirst, $paylast);
+  if ( $self->payname && $method ne 'ECHECK' ) {
+    $payname = $self->payname;
+    $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
+      or return "Illegal payname $payname";
+    ($payfirst, $paylast) = ($1, $2);
+  } else {
+    $payfirst = $self->getfield('first');
+    $paylast = $self->getfield('last');
+    $payname =  "$payfirst $paylast";
+  }
+
+  my %content = ();
+  if ( $method eq 'CC' ) { 
+
+    $content{card_number} = $self->payinfo;
+    $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+    $content{expiration} = "$2/$1";
+
+    #$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, } );
+
+  } elsif ( $method eq 'ECHECK' ) {
+    my($account_number,$routing_code) = $self->payinfo;
+    ( $content{account_number}, $content{routing_code} ) =
+      split('@', $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;
+  }
+
+  #then try refund
+  my $refund = new Business::OnlinePayment( $processor, @bop_options );
+  $refund->content(
+    'type'           => $method,
+    'action'         => 'credit',
+    'login'          => $login,
+    'password'       => $password,
+    'order_number'   => $order_number,
+    'amount'         => $amount,
+    'authorization'  => $auth,
+    'customer_id'    => $self->custnum,
+    'last_name'      => $paylast,
+    'first_name'     => $payfirst,
+    'name'           => $payname,
+    'address'        => $address,
+    'city'           => $self->city,
+    'state'          => $self->state,
+    'zip'            => $self->zip,
+    'country'        => $self->country,
+    'referer'        => 'http://cleanwhisker.420.am/',
+    %content, #after
+  );
+  $refund->submit();
+
+  return "$processor error: ". $refund->error_message
+    unless $refund->is_success();
+
+  my %method2payby = (
+    'CC'     => 'CARD',
+    'ECHECK' => 'CHEK',
+    'LEC'    => 'LECB',
+  );
+
+  my $paybatch = "$processor:". $refund->authorization;
+  $paybatch .= ':'. $refund->order_number
+    if $refund->can('order_number') && $refund->order_number;
+
+  while ( $cust_pay && $cust_pay->unappled < $amount ) {
+    my @cust_bill_pay = $cust_pay->cust_bill_pay;
+    last unless @cust_bill_pay;
+    my $cust_bill_pay = pop @cust_bill_pay;
+    my $error = $cust_bill_pay->delete;
+    last if $error;
+  }
+
+  my $cust_refund = new FS::cust_refund ( {
+    'custnum'  => $self->custnum,
+    'paynum'   => $options{'paynum'},
+    'refund'   => $amount,
+    '_date'    => '',
+    'payby'    => $method2payby{$method},
+    'payinfo'  => $self->payinfo,
+    'paybatch' => $paybatch,
+    'reason'   => $options{'reason'} || 'card refund',
+  } );
+  my $error = $cust_refund->insert;
+  if ( $error ) {
+    $cust_refund->paynum(''); #try again with no specific paynum
+    my $error2 = $cust_refund->insert;
+    if ( $error2 ) {
+      # gah, even with transactions.
+      my $e = 'WARNING: Card/ACH refunded but database not updated - '.
+              "error inserting refund ($processor): $error2".
+              " (previously tried insert with paynum #$options{'paynum'}" .
+              ": $error )";
+      warn $e;
+      return $e;
+    }
+  }
+
+  ''; #no error
+
+}
+
 =item total_owed
 
 Returns the total owed for this customer on all invoices
@@ -2515,6 +2749,19 @@
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
 }
+
+=item cust_pay_void
+
+Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
+
+=cut
+
+sub cust_pay_void {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
+}
+
 
 =item cust_refund
 

Index: Conf.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/Conf.pm,v
retrieving revision 1.101
retrieving revision 1.102
diff -u -d -r1.101 -r1.102
--- Conf.pm	30 Jun 2004 17:57:03 -0000	1.101
+++ Conf.pm	6 Jul 2004 17:26:02 -0000	1.102
@@ -543,7 +543,7 @@
   {
     'key'         => 'invoice_send_receipts',
     'section'     => 'deprecated',q
-    'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits.  See the payment_receipt_email and  instead.',
+    'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits.  See the payment_receipt_email and XXXX instead.',
     'type'        => 'checkbox',
   },
 
@@ -1269,7 +1269,12 @@
     'type'        => 'checkbox',
   },
 
-
+  {
+    'key'         => 'card_refund-days',
+    'section'     => 'billing',
+    'description' => 'After a payment, the number of days a refund link will be available for that payment.  Defaults to 120.',
+    'type'        => 'text',
+  },
 
 );
 

Index: cust_pay.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/cust_pay.pm,v
retrieving revision 1.33
retrieving revision 1.34
diff -u -d -r1.33 -r1.34
--- cust_pay.pm	6 Jul 2004 13:26:20 -0000	1.33
+++ cust_pay.pm	6 Jul 2004 17:26:02 -0000	1.34
@@ -396,7 +396,8 @@
 
 sub cust_bill_pay {
   my $self = shift;
-  sort { $a->_date <=> $b->_date }
+  sort {    $a->_date  <=> $b->_date
+         || $a->invnum <=> $b->invnum }
     qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
   ;
 }
@@ -431,6 +432,21 @@
   $amount -= $_->amount foreach ( $self->cust_pay_refund );
   sprintf("%.2f", $amount );
 }
+
+=item unrefunded
+
+Returns the amount of this payment that has not been refuned; which is
+paid minus all  refund applications (see L<FS::cust_pay_refund>).
+
+=cut
+
+sub unrefunded {
+  my $self = shift;
+  my $amount = $self->paid;
+  $amount -= $_->amount foreach ( $self->cust_pay_refund );
+  sprintf("%.2f", $amount );
+}
+
 
 =item cust_main
 




More information about the freeside-commits mailing list