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