From: jeff Date: Tue, 10 Mar 2009 16:14:11 +0000 (+0000) Subject: merge webpay support in with autoselection of old realtime_bop and realtime_refund_bop X-Git-Tag: root_of_svc_elec_features~1394 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=32db3ad86bcf04e4f34705a396b718061d333f20 merge webpay support in with autoselection of old realtime_bop and realtime_refund_bop --- diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index c0586af00..c6a4e0058 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -353,6 +353,9 @@ sub payment_info { 'paytypes' => [ @FS::cust_main::paytypes ], 'paybys' => [ $conf->config('signup_server-payby') ], + 'cust_paybys' => [ map { FS::payby->payby2payment($_) } + $conf->config('signup_server-payby') + ], 'stateid_label' => FS::Msgcat::_gettext('stateid'), 'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'), @@ -375,6 +378,18 @@ sub payment_info { my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; + $return{hide_payment_fields} = + [ + map { FS::payby->realtime($_) && + $cust_main + ->agent + ->payment_gateway( 'method' => FS::payby->payby2bop($_) ) + ->gateway_namespace + eq 'Business::OnlineThirdPartyPayment' + } + @{ $return{cust_paybys} } + ]; + $return{balance} = $cust_main->balance; $return{payname} = $cust_main->payname @@ -531,6 +546,26 @@ sub process_payment { } +sub realtime_collect { + + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $error = $cust_main->realtime_collect( 'method' => $p->{'method'}, + 'session_id' => $p->{'session_id'}, + ); + return { 'error' => $error } unless ref( $error ); + + return { 'error' => '', amount => $cust_main->balance, %$error }; +} + sub process_payment_order_pkg { my $p = shift; diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index 5569dfbde..02aa5800b 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -6,6 +6,7 @@ use Data::Dumper; use Tie::RefHash; use FS::Conf; use FS::Record qw(qsearch qsearchs dbdef); +use FS::CGI qw(popurl); use FS::Msgcat qw(gettext); use FS::Misc qw(card_types); use FS::ClientAPI_SessionCache; @@ -20,6 +21,7 @@ use FS::svc_phone; use FS::acct_snarf; use FS::queue; use FS::reg_code; +use FS::payby; $DEBUG = 0; $me = '[FS::ClientAPI::Signup]'; @@ -276,6 +278,29 @@ sub signup_info { if ( $agentnum ) { + warn "$me setting agent-specific payment flag\n" if $DEBUG > 1; + my $agent = qsearchs('agent', { 'agentnum' => $agentnum } ); + warn "$me has agent $agent\n" if $DEBUG > 1; + if ( $agent ) { #else complain loudly? + $signup_info->{'hide_payment_fields'} = []; + foreach my $payby (@{$signup_info->{payby}}) { + warn "$me checking $payby payment fields\n" if $DEBUG > 1; + my $hide = 0; + if (FS::payby->realtime($payby)) { + my $payment_gateway = + $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby) ); + if ($payment_gateway->gateway_namespace eq + 'Business::OnlineThirdPartyPayment' + ) { + warn "$me hiding $payby payment fields\n" if $DEBUG > 1; + $hide = 1; + } + } + push @{$signup_info->{'hide_payment_fields'}}, $hide; + } + } + warn "$me done setting agent-specific payment flag\n" if $DEBUG > 1; + warn "$me setting agent-specific package list\n" if $DEBUG > 1; $signup_info->{'part_pkg'} = $signup_info->{'agentnum2part_pkg'}{$agentnum} unless @{ $signup_info->{'part_pkg'} }; @@ -295,8 +320,6 @@ sub signup_info { ]; warn "$me done setting agent-specific adv. source list\n" if $DEBUG > 1; - my $agent = qsearchs('agent', { 'agentnum' => $agentnum } ); - $signup_info->{'agent_name'} = $agent->agent; $signup_info->{'company_name'} = $conf->config('company_name', $agentnum); @@ -436,6 +459,23 @@ sub new_customer { unless grep { $_ eq $packet->{'payby'} } $conf->config('signup_server-payby'); + if (FS::payby->realtime($packet->{payby})) { + my $payby = $packet->{payby}; + + my $agent = qsearchs('agent', { 'agentnum' => $agentnum }); + return { 'error' => "Unknown reseller" } + unless $agent; + + my $payment_gateway = + $agent->payment_gateway( 'method' => FS::payby->payby2bop($payby) ); + + if ($payment_gateway->gateway_namespace eq + 'Business::OnlineThirdPartyPayment' + ) { + $cust_main->payby('BILL'); # MCRD better? + } + } + $cust_main->payinfo($cust_main->daytime) if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo; @@ -547,10 +587,26 @@ sub new_customer { # " new customer: $bill_error" # if $bill_error; - $bill_error = $cust_main->collect('realtime' => 1); + if ($cust_main->_new_bop_required()) { + $bill_error = $cust_main->realtime_collect( + method => FS::payby->payby2bop( $packet->{payby} ), + depend_jobnum => $placeholder->jobnum, + ); + } else { + $bill_error = $cust_main->collect('realtime' => 1); + } #warn "[fs_signup_server] error collecting from new customer: $bill_error" # if $bill_error; + if ($bill_error && ref($bill_error) eq 'HASH') { + return { 'error' => '_collect', + ( map { $_ => $bill_error->{$_} } + qw(popup_url reference collectitems) + ), + amount => $cust_main->balance, + }; + } + if ( $cust_main->balance > 0 ) { #this makes sense. credit is "un-doing" the invoice @@ -600,4 +656,83 @@ sub new_customer { } +sub capture_payment { + my $packet = shift; + + warn "$me capture_payment called on $packet\n" if $DEBUG; + + ### + # identify processor/gateway from called back URL + ### + + my $conf = new FS::Conf; + + my $url = $packet->{url}; + my $payment_gateway = + qsearchs('payment_gateway', { 'gateway_callback_url' => popurl(0, $url) } ); + + unless ($payment_gateway) { + + my ( $processor, $login, $password, $action, @bop_options ) = + $conf->config('business-onlinepayment'); + $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; + + $payment_gateway = new FS::payment_gateway( { + gateway_namespace => $conf->config('business-onlinepayment-namespace'), + gateway_module => $processor, + gateway_username => $login, + gateway_password => $password, + gateway_action => $action, + options => [ ( @bop_options ) ], + }); + + } + + die "No real-time third party processor is enabled - ". + "did you set the business-onlinepayment configuration value?\n*" + unless $payment_gateway->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; + + ### + # locate pending transaction + ### + + eval "use Business::OnlineThirdPartyPayment"; + die $@ if $@; + + my $transaction = + new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module, + @{ [ $payment_gateway->options ] }, + ); + + my $paypendingnum = $transaction->reference($packet->{data}); + + my $cust_pay_pending = + qsearchs('cust_pay_pending', { paypendingnum => $paypendingnum } ); + + unless ($cust_pay_pending) { + my $bill_error = "No payment is being processed with id $paypendingnum". + "; Transaction aborted."; + return { error => '_decline', bill_error => $bill_error }; + } + + if ($cust_pay_pending->status ne 'pending') { + my $bill_error = "Payment with id $paypendingnum is not pending, but ". + $cust_pay_pending->status. "; Transaction aborted."; + return { error => '_decline', bill_error => $bill_error }; + } + + my $cust_main = $cust_pay_pending->cust_main; + my $bill_error = + $cust_main->realtime_botpp_capture( $cust_pay_pending, %{$packet->{data}} ); + + return { 'error' => ( $bill_error->{bill_error} ? '_decline' : '' ), + %$bill_error, + }; + +} + 1; diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index b86930255..3921afdaa 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -8,6 +8,7 @@ use MIME::Base64; use FS::ConfItem; use FS::ConfDefaults; use FS::Conf_compat17; +use FS::payby; use FS::conf; use FS::Record qw(qsearch qsearchs); use FS::UID qw(dbh datasrc use_confcompat); @@ -620,6 +621,17 @@ worry that config_items is freeside-specific and icky. }, { + 'key' => 'business-onlinepayment-namespace', + 'section' => 'billing', + 'description' => 'Specifies which perl module namespace (which group of collection routines) is used by default.', + 'type' => 'select', + 'select_hash' => [ + 'Business::OnlinePayment' => 'Direct API (Business::OnlinePayment)', + 'Business::OnlineThirdPartyPayment' => 'Web API (Business::ThirdPartyPayment)', + ], + }, + + { 'key' => 'business-onlinepayment-description', 'section' => 'billing', 'description' => 'String passed as the description field to Business::OnlinePayment. Evaluated as a double-quoted perl string, with the following variables available: $agent (the agent name), and $pkgs (a comma-separated list of packages for which these charges apply)', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 885eaaa28..65f7a7f40 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -845,10 +845,12 @@ sub tables_hashref { 'payunique', 'varchar', 'NULL', $char_d, '', '', #separate paybatch "unique" functions from current usage 'status', 'varchar', '', $char_d, '', '', + 'session_id', 'varchar', 'NULL', $char_d, '', '', #only need 32 'statustext', 'text', 'NULL', '', '', '', 'gatewaynum', 'int', 'NULL', '', '', '', #'cust_balance', @money_type, '', '', 'paynum', 'int', 'NULL', '', '', '', + 'jobnum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'paypendingnum', 'unique' => [ [ 'payunique' ] ], @@ -1857,10 +1859,12 @@ sub tables_hashref { 'payment_gateway' => { 'columns' => [ 'gatewaynum', 'serial', '', '', '', '', + 'gateway_namespace','varchar', 'NULL', $char_d, '', '', 'gateway_module', 'varchar', '', $char_d, '', '', 'gateway_username', 'varchar', 'NULL', $char_d, '', '', 'gateway_password', 'varchar', 'NULL', $char_d, '', '', 'gateway_action', 'varchar', 'NULL', $char_d, '', '', + 'gateway_callback_url', 'varchar', 'NULL', $char_d, '', '', 'disabled', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'gatewaynum', diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index ff0a2b1f6..e471e04a5 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -3,12 +3,14 @@ package FS::agent; use strict; use vars qw( @ISA ); #use Crypt::YAPassGen; +use Business::CreditCard 0.28; use FS::Record qw( dbh qsearch qsearchs ); use FS::cust_main; use FS::cust_pkg; use FS::agent_type; use FS::reg_code; use FS::TicketSystem; +use FS::Conf; @ISA = qw( FS::m2m_Common FS::Record ); @@ -200,6 +202,106 @@ sub ticketing_queue { FS::TicketSystem->queue($self->ticketing_queueid); }; +=item payment_gateway [ OPTION => VALUE, ... ] + +Returns a payment gateway object (see L) for this agent. + +Currently available options are I, I, and I. + +If I is set to the number of an invoice (see L) then +an attempt will be made to select a gateway suited for the taxes paid on +the invoice. + +The I and I options can be used to influence the choice +as well. Presently only 'CC' and 'ECHECK' methods are meaningful. + +When the I is 'CC' then the card number in I can direct +this routine to route to a gateway suited for that type of card. + +=cut + +sub payment_gateway { + my ( $self, %options ) = @_; + + my $taxclass = ''; + if ( $options{invnum} ) { + my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } ); + die "invnum ". $options{'invnum'}. " not found" unless $cust_bill; + my @taxclasses = + map { $_->part_pkg->taxclass } + grep { $_ } + map { $_->cust_pkg } + $cust_bill->cust_bill_pkg; + unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are + #different taxclasses $taxclass = $taxclasses[0]; + } + } + + #look for an agent gateway override first + my $cardtype; + if ( $options{method} && $options{method} eq 'CC' ) { + $cardtype = cardtype($options{payinfo}); + } elsif ( $options{method} && $options{method} eq 'ECHECK' ) { + $cardtype = 'ACH'; + } else { + $cardtype = $options{method} || ''; + } + + my $override = + qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, + cardtype => $cardtype, + taxclass => $taxclass, } ) + || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, + cardtype => '', + taxclass => $taxclass, } ) + || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, + cardtype => $cardtype, + taxclass => '', } ) + || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum, + cardtype => '', + taxclass => '', } ); + + my $payment_gateway = new FS::payment_gateway; + if ( $override ) { #use a payment gateway override + + $payment_gateway = $override->payment_gateway; + + } else { #use the standard settings from the config + # the standard settings from the config could be moved to a null agent + # agent_payment_gateway referenced payment_gateway + + my $conf = new FS::Conf; + die "Real-time processing not enabled\n" + unless $conf->exists('business-onlinepayment'); + + #load up config + my $bop_config = 'business-onlinepayment'; + $bop_config .= '-ach' + if ( $options{method} + && $options{method} =~ /^(ECHECK|CHEK)$/ + && $conf->exists($bop_config. '-ach') + ); + my ( $processor, $login, $password, $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; + + $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') || + 'Business::OnlinePayment'); + $payment_gateway->gateway_module($processor); + $payment_gateway->gateway_username($login); + $payment_gateway->gateway_password($password); + $payment_gateway->gateway_action($action); + $payment_gateway->set('options', [ @bop_options ]); + + } + + $payment_gateway; +} + =item num_prospect_cust_main Returns the number of prospects (customers with no packages ever ordered) for diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 865632f6c..2bad5ec3e 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3368,6 +3368,10 @@ sub retry_realtime { } +# some horrid false laziness here to avoid refactor fallout +# eventually realtime realtime_bop and realtime_refund_bop should go +# away and be replaced by _new_realtime_bop and _new_realtime_refund_bop + =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ] Runs a realtime credit card, ACH (electronic check) or phone bill transaction @@ -3401,7 +3405,12 @@ I is a unique identifier for this payment. =cut sub realtime_bop { - my( $self, $method, $amount, %options ) = @_; + my $self = shift; + + return $self->_new_realtime_bop(@_) + if $self->_new_bop_required(); + + my( $method, $amount, %options ) = @_; if ( $DEBUG ) { warn "$me realtime_bop: $method $amount\n"; warn " $_ => $options{$_}\n" foreach keys %options; @@ -3942,119 +3951,6 @@ sub realtime_bop { } -=item fake_bop - -=cut - -sub fake_bop { - my( $self, $method, $amount, %options ) = @_; - - if ( $options{'fake_failure'} ) { - return "Error: No error; test failure requested with fake_failure"; - } - - my %method2payby = ( - 'CC' => 'CARD', - 'ECHECK' => 'CHEK', - 'LEC' => 'LECB', - ); - - #my $paybatch = ''; - #if ( $payment_gateway ) { # agent override - # $paybatch = $payment_gateway->gatewaynum. '-'; - #} - # - #$paybatch .= "$processor:". $transaction->authorization; - # - #$paybatch .= ':'. $transaction->order_number - # if $transaction->can('order_number') - # && length($transaction->order_number); - - my $paybatch = 'FakeProcessor:54:32'; - - my $cust_pay = new FS::cust_pay ( { - 'custnum' => $self->custnum, - 'invnum' => $options{'invnum'}, - 'paid' => $amount, - '_date' => '', - 'payby' => $method2payby{$method}, - #'payinfo' => $payinfo, - 'payinfo' => '4111111111111111', - 'paybatch' => $paybatch, - #'paydate' => $paydate, - 'paydate' => '2012-05-01', - } ); - $cust_pay->payunique( $options{payunique} ) if length($options{payunique}); - - my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () ); - - if ( $error ) { - $cust_pay->invnum(''); #try again with no specific invnum - my $error2 = $cust_pay->insert( $options{'manual'} ? - ( 'manual' => 1 ) : () - ); - if ( $error2 ) { - # gah, even with transactions. - my $e = 'WARNING: Card/ACH debited but database not updated - '. - "error inserting (fake!) payment: $error2". - " (previously tried insert with invnum #$options{'invnum'}" . - ": $error )"; - warn $e; - return $e; - } - } - - if ( $options{'paynum_ref'} ) { - ${ $options{'paynum_ref'} } = $cust_pay->paynum; - } - - return ''; #no error - -} - -=item default_payment_gateway - -=cut - -sub default_payment_gateway { - my( $self, $method ) = @_; - - die "Real-time processing not enabled\n" - unless $conf->exists('business-onlinepayment'); - - #load up config - my $bop_config = 'business-onlinepayment'; - $bop_config .= '-ach' - if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach'); - my ( $processor, $login, $password, $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; - - ( $processor, $login, $password, $action, @bop_options ) -} - -=item remove_cvv - -Removes the I field from the database directly. - -If there is an error, returns the error, otherwise returns false. - -=cut - -sub remove_cvv { - my $self = shift; - my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?") - or return dbh->errstr; - $sth->execute($self->custnum) - or return $sth->errstr; - $self->paycvv(''); - ''; -} - =item realtime_refund_bop METHOD [ OPTION => VALUE ... ] Refunds a realtime credit card, ACH (electronic check) or phone bill transaction @@ -4094,7 +3990,12 @@ gateway is attempted. #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 ) = @_; + my $self = shift; + + return $self->_new_realtime_refund_bop(@_) + if $self->_new_bop_required(); + + my( $method, %options ) = @_; if ( $DEBUG ) { warn "$me realtime_refund_bop: $method refund\n"; warn " $_ => $options{$_}\n" foreach keys %options; @@ -4373,6 +4274,1285 @@ sub realtime_refund_bop { } +# does the configuration indicate the new bop routines are required? + +sub _new_bop_required { + my $self = shift; + + my $botpp = 'Business::OnlineThirdPartyPayment'; + + return 1 + if ( $conf->config('business-onlinepayment-namespace') eq $botpp || + scalar( grep { $_->gateway_namespace eq $botpp } + qsearch( 'payment_gateway', { 'disabled' => '' } ) + ) + ) + ; + + ''; +} + + +=item realtime_collect [ OPTION => VALUE ... ] + +Runs a realtime credit card, ACH (electronic check) or phone bill transaction +via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime +gateway. See L and +L for supported gateways. + +On failure returns an error message. + +Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url. + +Available options are: I, I, I, I, I, I, I, I + +I is one of: I, I and I. If none is specified +then it is deduced from the customer record. + +If no I is specified, then the customer balance is used. + +The additional options I, I, I, I, I, +I, I and I are also available. Any of these options, +if set, will override the value from the customer record. + +I is a free-text field passed to the gateway. It defaults to +"Internet services". + +If an I is specified, this payment (if successful) is applied to the +specified invoice. If you don't specify an I you might want to +call the B method. + +I can be set true to surpress email decline notices. + +I can be set to a scalar reference. It will be filled in with the +resulting paynum, if any. + +I is a unique identifier for this payment. + +I is a session identifier associated with this payment. + +I allows payment capture to unlock export jobs + +=cut + +sub realtime_collect { + my( $self, %options ) = @_; + + if ( $DEBUG ) { + warn "$me realtime_collect:\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + $options{amount} = $self->balance unless exists( $options{amount} ); + $options{method} = FS::payby->payby2bop($self->payby) + unless exists( $options{method} ); + + return $self->realtime_bop({%options}); + +} + +=item _realtime_bop { [ ARG => VALUE ... ] } + +Runs a realtime credit card, ACH (electronic check) or phone bill transaction +via a Business::OnlinePayment realtime gateway. See +L for supported gateways. + +Required arguments in the hashref are I, and I + +Available methods are: I, I and I + +Available optional arguments are: I, I, I, I, I, I + +The additional options I, I, I, I, I, +I, I and I are also available. Any of these options, +if set, will override the value from the customer record. + +I is a free-text field passed to the gateway. It defaults to +"Internet services". + +If an I is specified, this payment (if successful) is applied to the +specified invoice. If you don't specify an I you might want to +call the B method. + +I can be set true to surpress email decline notices. + +I can be set to a scalar reference. It will be filled in with the +resulting paynum, if any. + +I is a unique identifier for this payment. + +I is a session identifier associated with this payment. + +I allows payment capture to unlock export jobs + +(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too) + +=cut + +# some helper routines +sub _payment_gateway { + my ($self, $options) = @_; + + $options->{payment_gateway} = $self->agent->payment_gateway( %$options ) + unless exists($options->{payment_gateway}); + + $options->{payment_gateway}; +} + +sub _bop_auth { + my ($self, $options) = @_; + + ( + 'login' => $options->{payment_gateway}->gateway_username, + 'password' => $options->{payment_gateway}->gateway_password, + ); +} + +sub _bop_options { + my ($self, $options) = @_; + + $options->{payment_gateway}->gatewaynum + ? $options->{payment_gateway}->options + : @{ $options->{payment_gateway}->get('options') }; +} + +sub _bop_defaults { + my ($self, $options) = @_; + + $options->{description} ||= 'Internet services'; + $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} ); + $options->{invnum} ||= ''; + $options->{payname} = $self->payname unless exists( $options->{payname} ); +} + +sub _bop_content { + my ($self, $options) = @_; + my %content = (); + + $content{address} = exists($options->{'address1'}) + ? $options->{'address1'} + : $self->address1; + my $address2 = exists($options->{'address2'}) + ? $options->{'address2'} + : $self->address2; + $content{address} .= ", ". $address2 if length($address2); + + my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip; + $content{customer_ip} = $payip if length($payip); + + $content{invoice_number} = $options->{'invnum'} + if exists($options->{'invnum'}) && length($options->{'invnum'}); + + $content{email_customer} = + ( $conf->exists('business-onlinepayment-email_customer') + || $conf->exists('business-onlinepayment-email-override') ); + + $content{payfirst} = $self->getfield('first'); + $content{paylast} = $self->getfield('last'); + + $content{account_name} = "$content{payfirst} $content{paylast}" + if $options->{method} eq 'ECHECK'; + + $content{name} = $options->{payname}; + $content{name} = $content{account_name} if exists($content{account_name}); + + $content{city} = exists($options->{city}) + ? $options->{city} + : $self->city; + $content{state} = exists($options->{state}) + ? $options->{state} + : $self->state; + $content{zip} = exists($options->{zip}) + ? $options->{'zip'} + : $self->zip; + $content{country} = exists($options->{country}) + ? $options->{country} + : $self->country; + $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/ + $content{phone} = $self->daytime || $self->night; + + (%content); +} + +my %bop_method2payby = ( + 'CC' => 'CARD', + 'ECHECK' => 'CHEK', + 'LEC' => 'LECB', +); + +sub _new_realtime_bop { + my $self = shift; + + my %options = (); + if (ref($_[0]) eq 'HASH') { + %options = %{$_[0]}; + } else { + my ( $method, $amount ) = ( shift, shift ); + %options = @_; + $options{method} = $method; + $options{amount} = $amount; + } + + if ( $DEBUG ) { + warn "$me realtime_bop (new): $options{method} $options{amount}\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + return $self->fake_bop(%options) if $options{'fake'}; + + $self->_bop_defaults(\%options); + + ### + # select a gateway + ### + + my $payment_gateway = $self->_payment_gateway( \%options ); + my $namespace = $payment_gateway->gateway_namespace; + + eval "use $namespace"; + die $@ if $@; + + ### + # check for banned credit card/ACH + ### + + my $ban = qsearchs('banned_pay', { + 'payby' => $bop_method2payby{$options{method}}, + 'payinfo' => md5_base64($options{payinfo}), + } ); + return "Banned credit card" if $ban; + + ### + # massage data + ### + + my (%bop_content) = $self->_bop_content(\%options); + + if ( $options{method} ne 'ECHECK' ) { + $options{payname} =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ + or return "Illegal payname $options{payname}"; + ($bop_content{payfirst}, $bop_content{paylast}) = ($1, $2); + } + + my @invoicing_list = $self->invoicing_list_emailonly; + if ( $conf->exists('emailinvoiceautoalways') + || $conf->exists('emailinvoiceauto') && ! @invoicing_list + || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { + push @invoicing_list, $self->all_emails; + } + + my $email = ($conf->exists('business-onlinepayment-email-override')) + ? $conf->config('business-onlinepayment-email-override') + : $invoicing_list[0]; + + my $paydate = ''; + my %content = (); + if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) { + + $content{card_number} = $options{payinfo}; + $paydate = exists($options{'paydate'}) + ? $options{'paydate'} + : $self->paydate; + $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; + $content{expiration} = "$2/$1"; + + my $paycvv = exists($options{'paycvv'}) + ? $options{'paycvv'} + : $self->paycvv; + $content{cvv2} = $paycvv + if length($paycvv); + + my $paystart_month = exists($options{'paystart_month'}) + ? $options{'paystart_month'} + : $self->paystart_month; + + my $paystart_year = exists($options{'paystart_year'}) + ? $options{'paystart_year'} + : $self->paystart_year; + + $content{card_start} = "$paystart_month/$paystart_year" + if $paystart_month && $paystart_year; + + my $payissue = exists($options{'payissue'}) + ? $options{'payissue'} + : $self->payissue; + $content{issue_number} = $payissue if $payissue; + + $content{recurring_billing} = 'YES' + if qsearch('cust_pay', { 'custnum' => $self->custnum, + 'payby' => 'CARD', + 'payinfo' => $options{payinfo}, + } ) + || qsearch('cust_pay', { 'custnum' => $self->custnum, + 'payby' => 'CARD', + 'paymask' => $self->mask_payinfo('CARD', $options{payinfo}), + } ); + + + } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){ + ( $content{account_number}, $content{routing_code} ) = + split('@', $options{payinfo}); + $content{bank_name} = $options{payname}; + $content{bank_state} = exists($options{'paystate'}) + ? $options{'paystate'} + : $self->getfield('paystate'); + $content{account_type} = exists($options{'paytype'}) + ? uc($options{'paytype'}) || 'CHECKING' + : uc($self->getfield('paytype')) || 'CHECKING'; + $content{customer_org} = $self->company ? 'B' : 'I'; + $content{state_id} = exists($options{'stateid'}) + ? $options{'stateid'} + : $self->getfield('stateid'); + $content{state_id_state} = exists($options{'stateid_state'}) + ? $options{'stateid_state'} + : $self->getfield('stateid_state'); + $content{customer_ssn} = exists($options{'ss'}) + ? $options{'ss'} + : $self->ss; + } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) { + $content{phone} = $options{payinfo}; + } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) { + #move along + } else { + #die an evil death + } + + ### + # run transaction(s) + ### + + my $balance = exists( $options{'balance'} ) + ? $options{'balance'} + : $self->balance; + + $self->select_for_update; #mutex ... just until we get our pending record in + + #the checks here are intended to catch concurrent payments + #double-form-submission prevention is taken care of in cust_pay_pending::check + + #check the balance + return "The customer's balance has changed; $options{method} transaction aborted." + if $self->balance < $balance; + #&& $self->balance < $options{amount}; #might as well anyway? + + #also check and make sure there aren't *other* pending payments for this cust + + my @pending = qsearch('cust_pay_pending', { + 'custnum' => $self->custnum, + 'status' => { op=>'!=', value=>'done' } + }); + return "A payment is already being processed for this customer (". + join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ). + "); $options{method} transaction aborted." + if scalar(@pending); + + #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out + + my $cust_pay_pending = new FS::cust_pay_pending { + 'custnum' => $self->custnum, + #'invnum' => $options{'invnum'}, + 'paid' => $options{amount}, + '_date' => '', + 'payby' => $bop_method2payby{$options{method}}, + 'payinfo' => $options{payinfo}, + 'paydate' => $paydate, + 'status' => 'new', + 'gatewaynum' => $payment_gateway->gatewaynum || '', + 'session_id' => $options{session_id} || '', + 'jobnum' => $options{depend_jobnum} || '', + }; + $cust_pay_pending->payunique( $options{payunique} ) + if defined($options{payunique}) && length($options{payunique}); + my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted + return $cpp_new_err if $cpp_new_err; + + my( $action1, $action2 ) = + split( /\s*\,\s*/, $payment_gateway->gateway_action ); + + my $transaction = new $namespace( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + + $transaction->content( + 'type' => $options{method}, + $self->_bop_auth(\%options), + 'action' => $action1, + 'description' => $options{'description'}, + 'amount' => $options{amount}, + #'invoice_number' => $options{'invnum'}, + 'customer_id' => $self->custnum, + %bop_content, + 'reference' => $cust_pay_pending->paypendingnum, #for now + 'email' => $email, + %content, #after + ); + + $cust_pay_pending->status('pending'); + my $cpp_pending_err = $cust_pay_pending->replace; + return $cpp_pending_err if $cpp_pending_err; + + #config? + my $BOP_TESTING = 0; + my $BOP_TESTING_SUCCESS = 1; + + unless ( $BOP_TESTING ) { + $transaction->submit(); + } else { + if ( $BOP_TESTING_SUCCESS ) { + $transaction->is_success(1); + $transaction->authorization('fake auth'); + } else { + $transaction->is_success(0); + $transaction->error_message('fake failure'); + } + } + + if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) { + + return { reference => $cust_pay_pending->paypendingnum, + map { $_ => $transaction->$_ } qw ( popup_url collectitems ) }; + + } elsif ( $transaction->is_success() && $action2 ) { + + $cust_pay_pending->status('authorized'); + my $cpp_authorized_err = $cust_pay_pending->replace; + return $cpp_authorized_err if $cpp_authorized_err; + + my $auth = $transaction->authorization; + my $ordernum = $transaction->can('order_number') + ? $transaction->order_number + : ''; + + my $capture = + new Business::OnlinePayment( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + + my %capture = ( + %content, + type => $options{method}, + action => $action2, + $self->_bop_auth(\%options), + order_number => $ordernum, + amount => $options{amount}, + authorization => $auth, + description => $options{'description'}, + ); + + foreach my $field (qw( authorization_source_code returned_ACI + transaction_identifier validation_code + transaction_sequence_num local_transaction_date + local_transaction_time AVS_result_code )) { + $capture{$field} = $transaction->$field() if $transaction->can($field); + } + + $capture->content( %capture ); + + $capture->submit(); + + unless ( $capture->is_success ) { + my $e = "Authorization successful but capture failed, custnum #". + $self->custnum. ': '. $capture->result_code. + ": ". $capture->error_message; + warn $e; + return $e; + } + + } + + ### + # remove paycvv after initial transaction + ### + + #false laziness w/misc/process/payment.cgi - check both to make sure working + # correctly + if ( defined $self->dbdef_table->column('paycvv') + && length($self->paycvv) + && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save') + ) { + my $error = $self->remove_cvv; + if ( $error ) { + warn "WARNING: error removing cvv: $error\n"; + } + } + + ### + # result handling + ### + + $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options ); + +} + +=item fake_bop + +=cut + +sub fake_bop { + my $self = shift; + + my %options = (); + if (ref($_[0]) eq 'HASH') { + %options = %{$_[0]}; + } else { + my ( $method, $amount ) = ( shift, shift ); + %options = @_; + $options{method} = $method; + $options{amount} = $amount; + } + + if ( $options{'fake_failure'} ) { + return "Error: No error; test failure requested with fake_failure"; + } + + #my $paybatch = ''; + #if ( $payment_gateway->gatewaynum ) { # agent override + # $paybatch = $payment_gateway->gatewaynum. '-'; + #} + # + #$paybatch .= "$processor:". $transaction->authorization; + # + #$paybatch .= ':'. $transaction->order_number + # if $transaction->can('order_number') + # && length($transaction->order_number); + + my $paybatch = 'FakeProcessor:54:32'; + + my $cust_pay = new FS::cust_pay ( { + 'custnum' => $self->custnum, + 'invnum' => $options{'invnum'}, + 'paid' => $options{amount}, + '_date' => '', + 'payby' => $bop_method2payby{$options{method}}, + #'payinfo' => $payinfo, + 'payinfo' => '4111111111111111', + 'paybatch' => $paybatch, + #'paydate' => $paydate, + 'paydate' => '2012-05-01', + } ); + $cust_pay->payunique( $options{payunique} ) if length($options{payunique}); + + my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () ); + + if ( $error ) { + $cust_pay->invnum(''); #try again with no specific invnum + my $error2 = $cust_pay->insert( $options{'manual'} ? + ( 'manual' => 1 ) : () + ); + if ( $error2 ) { + # gah, even with transactions. + my $e = 'WARNING: Card/ACH debited but database not updated - '. + "error inserting (fake!) payment: $error2". + " (previously tried insert with invnum #$options{'invnum'}" . + ": $error )"; + warn $e; + return $e; + } + } + + if ( $options{'paynum_ref'} ) { + ${ $options{'paynum_ref'} } = $cust_pay->paynum; + } + + return ''; #no error + +} + + +# item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ] +# +# Wraps up processing of a realtime credit card, ACH (electronic check) or +# phone bill transaction. + +sub _realtime_bop_result { + my( $self, $cust_pay_pending, $transaction, %options ) = @_; + if ( $DEBUG ) { + warn "$me _realtime_bop_result: pending transaction ". + $cust_pay_pending->paypendingnum. "\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + my $payment_gateway = $options{payment_gateway} + or return "no payment gateway in arguments to _realtime_bop_result"; + + $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined'); + my $cpp_captured_err = $cust_pay_pending->replace; + return $cpp_captured_err if $cpp_captured_err; + + if ( $transaction->is_success() ) { + + my $paybatch = ''; + if ( $payment_gateway->gatewaynum ) { # agent override + $paybatch = $payment_gateway->gatewaynum. '-'; + } + + $paybatch .= $payment_gateway->gateway_module. ":". + $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'}, + 'paid' => $cust_pay_pending->paid, + '_date' => '', + 'payby' => $cust_pay_pending->payby, + #'payinfo' => $payinfo, + 'paybatch' => $paybatch, + 'paydate' => $cust_pay_pending->paydate, + } ); + #doesn't hurt to know, even though the dup check is in cust_pay_pending now + $cust_pay->payunique( $options{payunique} ) + if defined($options{payunique}) && length($options{payunique}); + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction + + my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () ); + + if ( $error ) { + $cust_pay->invnum(''); #try again with no specific invnum + my $error2 = $cust_pay->insert( $options{'manual'} ? + ( 'manual' => 1 ) : () + ); + if ( $error2 ) { + # gah. but at least we have a record of the state we had to abort in + # from cust_pay_pending now. + my $e = "WARNING: $options{method} captured but payment not recorded -". + " error inserting payment (". $payment_gateway->gateway_module. + "): $error2". + " (previously tried insert with invnum #$options{'invnum'}" . + ": $error ) - pending payment saved as paypendingnum ". + $cust_pay_pending->paypendingnum. "\n"; + warn $e; + return $e; + } + } + + my $jobnum = $cust_pay_pending->jobnum; + if ( $jobnum ) { + my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } ); + + unless ( $placeholder ) { + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + my $e = "WARNING: $options{method} captured but job $jobnum not ". + "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n"; + warn $e; + return $e; + } + + $error = $placeholder->delete; + + if ( $error ) { + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + my $e = "WARNING: $options{method} captured but could not delete ". + "job $jobnum for paypendingnum ". + $cust_pay_pending->paypendingnum. ": $error\n"; + warn $e; + return $e; + } + + } + + if ( $options{'paynum_ref'} ) { + ${ $options{'paynum_ref'} } = $cust_pay->paynum; + } + + $cust_pay_pending->status('done'); + $cust_pay_pending->statustext('captured'); + $cust_pay_pending->paynum($cust_pay->paynum); + my $cpp_done_err = $cust_pay_pending->replace; + + if ( $cpp_done_err ) { + + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + my $e = "WARNING: $options{method} captured but payment not recorded - ". + "error updating status for paypendingnum ". + $cust_pay_pending->paypendingnum. ": $cpp_done_err \n"; + warn $e; + return $e; + + } else { + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return ''; #no error + + } + + } else { + + my $perror = $payment_gateway->gateway_module. " error: ". + $transaction->error_message; + + my $jobnum = $cust_pay_pending->jobnum; + if ( $jobnum ) { + my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } ); + + if ( $placeholder ) { + my $error = $placeholder->depended_delete; + $error ||= $placeholder->delete; + warn "error removing provisioning jobs after declined paypendingnum ". + $cust_pay_pending->paypendingnum. "\n"; + } else { + my $e = "error finding job $jobnum for declined paypendingnum ". + $cust_pay_pending->paypendingnum. "\n"; + warn $e; + } + + } + + unless ( $transaction->error_message ) { + + my $t_response; + if ( $transaction->can('response_page') ) { + $t_response = { + 'page' => ( $transaction->can('response_page') + ? $transaction->response_page + : '' + ), + 'code' => ( $transaction->can('response_code') + ? $transaction->response_code + : '' + ), + 'headers' => ( $transaction->can('response_headers') + ? $transaction->response_headers + : '' + ), + }; + } else { + $t_response .= + "No additional debugging information available for ". + $payment_gateway->gateway_module; + } + + $perror .= "No error_message returned from ". + $payment_gateway->gateway_module. " -- ". + ( ref($t_response) ? Dumper($t_response) : $t_response ); + + } + + if ( !$options{'quiet'} && !$realtime_bop_decline_quiet + && $conf->exists('emaildecline') + && grep { $_ ne 'POST' } $self->invoicing_list + && ! grep { $transaction->error_message =~ /$_/ } + $conf->config('emaildecline-exclude') + ) { + my @templ = $conf->config('declinetemplate'); + my $template = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", @templ ], + ) or return "($perror) can't create template: $Text::Template::ERROR"; + $template->compile() + or return "($perror) can't compile template: $Text::Template::ERROR"; + + my $templ_hash = { error => $transaction->error_message }; + + my $error = send_email( + 'from' => $conf->config('invoice_from', $self->agentnum ), + 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ], + 'subject' => 'Your payment could not be processed', + 'body' => [ $template->fill_in(HASH => $templ_hash) ], + ); + + $perror .= " (also received error sending decline notification: $error)" + if $error; + + } + + $cust_pay_pending->status('done'); + $cust_pay_pending->statustext("declined: $perror"); + my $cpp_done_err = $cust_pay_pending->replace; + if ( $cpp_done_err ) { + my $e = "WARNING: $options{method} declined but pending payment not ". + "resolved - error updating status for paypendingnum ". + $cust_pay_pending->paypendingnum. ": $cpp_done_err \n"; + warn $e; + $perror = "$e ($perror)"; + } + + return $perror; + } + +} + +=item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ] + +Verifies successful third party processing of a realtime credit card, +ACH (electronic check) or phone bill transaction via a +Business::OnlineThirdPartyPayment realtime gateway. See +L for supported gateways. + +Available options are: I, I, I, I, I + +The additional options I, I, I, +I, I and I are also available. Any of these options, +if set, will override the value from the customer record. + +I is a free-text field passed to the gateway. It defaults to +"Internet services". + +If an I is specified, this payment (if successful) is applied to the +specified invoice. If you don't specify an I you might want to +call the B method. + +I can be set true to surpress email decline notices. + +I can be set to a scalar reference. It will be filled in with the +resulting paynum, if any. + +I is a unique identifier for this payment. + +Returns a hashref containing elements bill_error (which will be undefined +upon success) and session_id of any associated session. + +=cut + +sub realtime_botpp_capture { + my( $self, $cust_pay_pending, %options ) = @_; + if ( $DEBUG ) { + warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + eval "use Business::OnlineThirdPartyPayment"; + die $@ if $@; + + ### + # select the gateway + ### + + my $method = FS::payby->payby2bop($cust_pay_pending->payby); + + my $payment_gateway = $cust_pay_pending->gatewaynum + ? qsearchs( 'payment_gateway', + { gatewaynum => $cust_pay_pending->gatewaynum } + ) + : $self->agent->payment_gateway( 'method' => $method, + # 'invnum' => $cust_pay_pending->invnum, + # 'payinfo' => $cust_pay_pending->payinfo, + ); + + $options{payment_gateway} = $payment_gateway; # for the helper subs + + ### + # massage data + ### + + my @invoicing_list = $self->invoicing_list_emailonly; + if ( $conf->exists('emailinvoiceautoalways') + || $conf->exists('emailinvoiceauto') && ! @invoicing_list + || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { + push @invoicing_list, $self->all_emails; + } + + my $email = ($conf->exists('business-onlinepayment-email-override')) + ? $conf->config('business-onlinepayment-email-override') + : $invoicing_list[0]; + + my %content = (); + + $content{email_customer} = + ( $conf->exists('business-onlinepayment-email_customer') + || $conf->exists('business-onlinepayment-email-override') ); + + ### + # run transaction(s) + ### + + my $transaction = + new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module, + $self->_bop_options(\%options), + ); + + $transaction->reference({ %options }); + + $transaction->content( + 'type' => $method, + $self->_bop_auth(\%options), + 'action' => 'Post Authorization', + 'description' => $options{'description'}, + 'amount' => $cust_pay_pending->paid, + #'invoice_number' => $options{'invnum'}, + 'customer_id' => $self->custnum, + 'referer' => 'http://cleanwhisker.420.am/', + 'reference' => $cust_pay_pending->paypendingnum, + 'email' => $email, + 'phone' => $self->daytime || $self->night, + %content, #after + # plus whatever is required for bogus capture avoidance + ); + + $transaction->submit(); + + my $error = + $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options ); + + { + bill_error => $error, + session_id => $cust_pay_pending->session_id, + } + +} + +=item default_payment_gateway DEPRECATED -- use agent->payment_gateway + +=cut + +sub default_payment_gateway { + my( $self, $method ) = @_; + + die "Real-time processing not enabled\n" + unless $conf->exists('business-onlinepayment'); + + #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n"; + + #load up config + my $bop_config = 'business-onlinepayment'; + $bop_config .= '-ach' + if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach'); + my ( $processor, $login, $password, $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; + + ( $processor, $login, $password, $action, @bop_options ) +} + +=item remove_cvv + +Removes the I field from the database directly. + +If there is an error, returns the error, otherwise returns false. + +=cut + +sub remove_cvv { + my $self = shift; + my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?") + or return dbh->errstr; + $sth->execute($self->custnum) + or return $sth->errstr; + $self->paycvv(''); + ''; +} + +=item _new_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 for supported gateways. + +Available methods are: I, I and I + +Available options are: I, I, I, I + +Most gateways require a reference to an original payment transaction to refund, +so you probably need to specify a I. + +I defaults to the original amount of the payment if not specified. + +I specifies a reason for the refund. + +I specifies the expiration date for a credit card overriding the +value from the customer record or the payment record. Specified as yyyy-mm-dd + +Implementation note: If I 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, I, I, I, I, +#I, I and I are also available. Any of these options, +#if set, will override the value from the customer record. + +#If an I is specified, this payment (if successful) is applied to the +#specified invoice. If you don't specify an I you might want to +#call the B 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 _new_realtime_refund_bop { + my $self = shift; + + my %options = (); + if (ref($_[0]) ne 'HASH') { + %options = %{$_[0]}; + } else { + my $method = shift; + %options = @_; + $options{method} = $method; + } + + if ( $DEBUG ) { + warn "$me realtime_refund_bop (new): $options{method} refund\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + ### + # look up the original payment and optionally a gateway for that payment + ### + + my $cust_pay = ''; + my $amount = $options{'amount'}; + + my( $processor, $login, $password, @bop_options, $namespace ) ; + my( $auth, $order_number ) = ( '', '', '' ); + + if ( $options{'paynum'} ) { + + warn " paynum: $options{paynum}\n" if $DEBUG > 1; + $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } ) + or return "Unknown paynum $options{'paynum'}"; + $amount ||= $cust_pay->paid; + + $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/ + or return "Can't parse paybatch for paynum $options{'paynum'}: ". + $cust_pay->paybatch; + my $gatewaynum = ''; + ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 ); + + if ( $gatewaynum ) { #gateway for the payment to be refunded + + my $payment_gateway = + qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } ); + die "payment gateway $gatewaynum not found" + unless $payment_gateway; + + $processor = $payment_gateway->gateway_module; + $login = $payment_gateway->gateway_username; + $password = $payment_gateway->gateway_password; + $namespace = $payment_gateway->gateway_namespace; + @bop_options = $payment_gateway->options; + + } else { #try the default gateway + + my $conf_processor; + my $payment_gateway = + $self->agent->payment_gateway('method' => $options{method}); + + ( $conf_processor, $login, $password, $namespace ) = + map { my $method = "gateway_$_"; $payment_gateway->$method } + qw( module username password namespace ); + + @bop_options = $payment_gateway->gatewaynum + ? $payment_gateway->options + : @{ $payment_gateway->get('options') }; + + return "processor of payment $options{'paynum'} $processor does not". + " match default processor $conf_processor" + unless $processor eq $conf_processor; + + } + + + } else { # didn't specify a paynum, so look for agent gateway overrides + # like a normal transaction + + my $payment_gateway = + $self->agent->payment_gateway( 'method' => $options{method}, + #'payinfo' => $payinfo, + ); + my( $processor, $login, $password, $namespace ) = + map { my $method = "gateway_$_"; $payment_gateway->$method } + qw( module username password namespace ); + + my @bop_options = $payment_gateway->gatewaynum + ? $payment_gateway->options + : @{ $payment_gateway->get('options') }; + + } + return "neither amount nor paynum specified" unless $amount; + + eval "use $namespace"; + die $@ if $@; + + my %content = ( + 'type' => $options{method}, + 'login' => $login, + 'password' => $password, + 'order_number' => $order_number, + 'amount' => $amount, + 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/ + ); + $content{authorization} = $auth + if length($auth); #echeck/ACH transactions have an order # but no auth + #(at least with authorize.net) + + my $disable_void_after; + if ($conf->exists('disable_void_after') + && $conf->config('disable_void_after') =~ /^(\d+)$/) { + $disable_void_after = $1; + } + + #first try void if applicable + if ( $cust_pay && $cust_pay->paid == $amount + && ( + ( not defined($disable_void_after) ) + || ( time < ($cust_pay->_date + $disable_void_after ) ) + ) + ) { + warn " attempting void\n" if $DEBUG > 1; + my $void = new Business::OnlinePayment( $processor, @bop_options ); + $void->content( 'action' => 'void', %content ); + $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; + } + warn " void successful\n" if $DEBUG > 1; + return ''; + } + } + + warn " void unsuccessful, trying refund\n" + if $DEBUG > 1; + + #massage data + my $address = $self->address1; + $address .= ", ". $self->address2 if $self->address2; + + my($payname, $payfirst, $paylast); + if ( $self->payname && $options{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 @invoicing_list = $self->invoicing_list_emailonly; + if ( $conf->exists('emailinvoiceautoalways') + || $conf->exists('emailinvoiceauto') && ! @invoicing_list + || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { + push @invoicing_list, $self->all_emails; + } + + my $email = ($conf->exists('business-onlinepayment-email-override')) + ? $conf->config('business-onlinepayment-email-override') + : $invoicing_list[0]; + + my $payip = exists($options{'payip'}) + ? $options{'payip'} + : $self->payip; + $content{customer_ip} = $payip + if length($payip); + + my $payinfo = ''; + if ( $options{method} eq 'CC' ) { + + if ( $cust_pay ) { + $content{card_number} = $payinfo = $cust_pay->payinfo; + (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate) + =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ && + ($content{expiration} = "$2/$1"); # where available + } else { + $content{card_number} = $payinfo = $self->payinfo; + (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate) + =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; + $content{expiration} = "$2/$1"; + } + + } elsif ( $options{method} eq 'ECHECK' ) { + + if ( $cust_pay ) { + $payinfo = $cust_pay->payinfo; + } else { + $payinfo = $self->payinfo; + } + ( $content{account_number}, $content{routing_code} )= split('@', $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 ( $options{method} eq 'LEC' ) { + $content{phone} = $payinfo = $self->payinfo; + } + + #then try refund + my $refund = new Business::OnlinePayment( $processor, @bop_options ); + my %sub_content = $refund->content( + 'action' => 'credit', + '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, + 'email' => $email, + 'phone' => $self->daytime || $self->night, + %content, #after + ); + warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content ) + if $DEBUG > 1; + $refund->submit(); + + return "$processor error: ". $refund->error_message + unless $refund->is_success(); + + my $paybatch = "$processor:". $refund->authorization; + $paybatch .= ':'. $refund->order_number + if $refund->can('order_number') && $refund->order_number; + + while ( $cust_pay && $cust_pay->unapplied < $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' => $bop_method2payby{$options{method}}, + 'payinfo' => $payinfo, + 'paybatch' => $paybatch, + 'reason' => $options{'reason'} || 'card or ACH 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 batch_card OPTION => VALUE... Adds a payment for this invoice to the pending credit card batch (see diff --git a/FS/FS/cust_pay_pending.pm b/FS/FS/cust_pay_pending.pm index bbabd247e..fba19ea19 100644 --- a/FS/FS/cust_pay_pending.pm +++ b/FS/FS/cust_pay_pending.pm @@ -191,6 +191,7 @@ sub check { #|| $self->ut_textn('statustext') || $self->ut_anything('statustext') #|| $self->ut_money('cust_balance') + || $self->ut_hexn('session_id') || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' ) || $self->payinfo_check() #payby/payinfo/paymask/paydate ; @@ -215,6 +216,18 @@ sub check { $self->SUPER::check; } +=item cust_main + +Returns the associated L record if any. Otherwise returns false. + +=cut + +sub cust_main { + my $self = shift; + qsearchs('cust_main', { custnum => $self->custnum } ); +} + + #these two are kind-of false laziness w/cust_main::realtime_bop #(currently only used when resolving pending payments manually) diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index dd6db1be9..7c8656c09 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -439,9 +439,7 @@ replace methods. sub check { my $self = shift; - $self->locationnum('') - if defined($self->locationnum) && length($self->locationnum) - && ( $self->locationnum == 0 || $self->locationnum == -1 ); + $self->locationnum('') if !$self->locationnum || $self->locationnum == -1; my $error = $self->ut_numbern('pkgnum') diff --git a/FS/FS/payby.pm b/FS/FS/payby.pm index b54e5d938..30a03ddfe 100644 --- a/FS/FS/payby.pm +++ b/FS/FS/payby.pm @@ -48,28 +48,33 @@ tie %hash, 'Tie::IxHash', tinyname => 'card', shortname => 'Credit card', longname => 'Credit card (automatic)', + realtime => 1, }, 'DCRD' => { tinyname => 'card', shortname => 'Credit card', longname => 'Credit card (on-demand)', cust_pay => 'CARD', #this is a customer type only, payments are CARD... + realtime => 1, }, 'CHEK' => { tinyname => 'check', shortname => 'Electronic check', longname => 'Electronic check (automatic)', + realtime => 1, }, 'DCHK' => { tinyname => 'check', shortname => 'Electronic check', longname => 'Electronic check (on-demand)', cust_pay => 'CHEK', #this is a customer type only, payments are CHEK... + realtime => 1, }, 'LECB' => { tinyname => 'phone bill', shortname => 'Phone bill billing', longname => 'Phone bill billing', + realtime => 1, }, 'BILL' => { tinyname => 'billing', @@ -131,6 +136,15 @@ sub can_payby { return 1; } +sub realtime { # can use realtime payment facilities + my( $self, $payby ) = @_; + + return 0 unless $hash{$payby}; + return 0 unless exists( $hash{$payby}->{realtime} ); + + return $hash{$payby}->{realtime}; +} + sub payby2longname { my $self = shift; map { $_ => $hash{$_}->{longname} } $self->payby; @@ -157,6 +171,7 @@ sub longname { %payby2bop = ( 'CARD' => 'CC', 'CHEK' => 'ECHECK', + 'MCRD' => 'CC', ); sub payby2bop { diff --git a/FS/FS/payment_gateway.pm b/FS/FS/payment_gateway.pm index 35b4f0835..bc8b875c3 100644 --- a/FS/FS/payment_gateway.pm +++ b/FS/FS/payment_gateway.pm @@ -1,12 +1,14 @@ package FS::payment_gateway; use strict; -use vars qw( @ISA ); +use vars qw( @ISA $me $DEBUG ); use FS::Record qw( qsearch qsearchs dbh ); use FS::option_Common; use FS::agent_payment_gateway; @ISA = qw( FS::option_Common ); +$me = '[ FS::payment_gateway ]'; +$DEBUG=0; =head1 NAME @@ -37,6 +39,8 @@ currently supported: =item gatewaynum - primary key +=item gateway_namespace - Business::OnlinePayment or Business::OnlineThirdPartyPayment + =item gateway_module - Business::OnlinePayment:: module name =item gateway_username - payment gateway username @@ -110,8 +114,12 @@ sub check { my $error = $self->ut_numbern('gatewaynum') || $self->ut_alpha('gateway_module') + || $self->ut_enum('gateway_namespace', ['Business::OnlinePayment', + 'Business::OnlineThirdPartyPayment', + ] ) || $self->ut_textn('gateway_username') || $self->ut_anything('gateway_password') + || $self->ut_textn('gateway_callback_url') # a bit too permissive || $self->ut_enum('disabled', [ '', 'Y' ] ) #|| $self->ut_textn('gateway_action') ; @@ -131,6 +139,10 @@ sub check { $self->gateway_action('Normal Authorization'); } + # this little kludge mimics FS::CGI::popurl + $self->gateway_callback_url($self->gateway_callback_url. '/') + if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ ); + $self->SUPER::check; } @@ -186,6 +198,41 @@ sub disable { } +=item namespace_description + +returns a friendly name for the namespace + +=cut + +my %namespace2description = ( + '' => 'Direct', + 'Business::OnlinePayment' => 'Direct', + 'Business::OnlineThirdPartyPayment' => 'Hosted', +); + +sub namespace_description { + $namespace2description{shift->gateway_namespace} || 'Unknown'; +} + +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. +# +# + +sub _upgrade_data { + my ($class, %opts) = @_; + my $dbh = dbh; + + warn "$me upgrading $class\n" if $DEBUG; + + foreach ( qsearch( 'payment_gateway', { 'gateway_namespace' => '' } ) ) { + $_->gateway_namespace('Business::OnlinePayment'); #defaulting + my $error = $_->replace; + die "$class had error during upgrade replacement: $error" if $error; + } +} + =back =head1 BUGS diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index 580ca7334..3ede27cd9 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -39,6 +39,7 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'process_payment_order_pkg' => 'MyAccount/process_payment_order_pkg', 'process_payment_order_renew' => 'MyAccount/process_payment_order_renew', 'process_prepay' => 'MyAccount/process_prepay', + 'realtime_collect' => 'MyAccount/realtime_collect', 'list_pkgs' => 'MyAccount/list_pkgs', #add to ss (added?) 'list_svcs' => 'MyAccount/list_svcs', #add to ss (added?) 'list_svc_usage' => 'MyAccount/list_svc_usage', @@ -58,6 +59,7 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'signup_info' => 'Signup/signup_info', 'domain_select_hash' => 'Signup/domain_select_hash', # expose? 'new_customer' => 'Signup/new_customer', + 'capture_payment' => 'Signup/capture_payment', 'agent_login' => 'Agent/agent_login', 'agent_logout' => 'Agent/agent_logout', 'agent_info' => 'Agent/agent_info', diff --git a/fs_selfservice/FS-SelfService/cgi/change_pay.html b/fs_selfservice/FS-SelfService/cgi/change_pay.html index 2bea9550b..7283cb850 100644 --- a/fs_selfservice/FS-SelfService/cgi/change_pay.html +++ b/fs_selfservice/FS-SelfService/cgi/change_pay.html @@ -51,10 +51,11 @@ 'LECB' => qq/Phone Bill Billing/, 'BILL' => qq/Billing/, 'COMP' => qq/Complimentary/, + 'PREP' => qq/Prepaid Card/, 'PREPAY' => qq/Prepaid Card/, ); tie my %options, 'Tie::IxHash', (); - foreach my $payby_option ( @paybys ) { + foreach my $payby_option ( grep { exists( $payby_index{$_} ) } @paybys ) { $options{$payby_option} = $payby_index{$payby_option}; } $options{$payby} = $payby_index{$payby} diff --git a/fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html b/fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html new file mode 100755 index 000000000..042b8b37c --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html @@ -0,0 +1,38 @@ +My Account +MyAccount

+ + + + + + +<%= $url = "$selfurl?session=$session_id;action="; ''; %> +<%= include('myaccount_menu') %> + +Pay now

+ +<%= if ( $error ) { + $OUT .= qq!$error

!; +}else{ + $OUT .= <
+ Your transaction reference number is $reference

+
+EOF + + my %itemhash = @collectitems; + foreach my $input (keys %itemhash) { + $OUT .= qq!!; + } + + $OUT .= qq!!; + $OUT .= qq!
!; +} +%> + + diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount.html b/fs_selfservice/FS-SelfService/cgi/myaccount.html index cb5ed352e..c9ca0c5f0 100644 --- a/fs_selfservice/FS-SelfService/cgi/myaccount.html +++ b/fs_selfservice/FS-SelfService/cgi/myaccount.html @@ -8,7 +8,11 @@ Hello <%= $name %>!

<%= $small_custview %>
<%= if ( $balance > 0 ) { - $OUT .= qq! Make a payment

!; + if (scalar(grep $_, @hide_payment_field)) { + $OUT .= qq! Make a payment

!; + } else { + $OUT .= qq! Make a payment

!; + } } %> <%= if ( @open_invoices ) { diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html b/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html index ec5a8fa42..cc9f255ce 100644 --- a/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html +++ b/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html @@ -18,14 +18,34 @@ my @menu = ( if ( 1 ) { #XXXFIXME "enable selfservice prepay features" flag or something, eventually per-pkg or something really fancy - push @menu, ( - { title=>'Recharge my account with a credit card', - url=>'make_payment', indent=>2 }, - { title=>'Recharge my account with a check', - url=>'make_ach_payment', indent=>2 }, - { title=>'Recharge my account with a prepaid card', - url=>'recharge_prepay', indent=>2 }, - ); + #XXXFIXME still a bit sloppy for multi-gateway of differing namespace + my $i = 0; + while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CARD/; $i++ } + if ( $cust_paybys[$i] =~ /^CARD/ ) { + push @menu, { title => 'Recharge my account with a credit card', + url => $hide_payment_fields[$i] + ? 'make_thirdparty_payment&payby_method=CC' + : 'make_payment', + indent => 2, + } + } + + $i = 0; + while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CHEK/; $i++ } + if ( $cust_paybys[$i] =~ /^CHEK/ ) { + push @menu, { title => 'Recharge my account with a check', + url => $hide_payment_field[$i] + ? 'make_thirdparty_payment&payby_method=ECHECK' + : 'make_ach_payment', + indent => 2, + } + } + + push @menu, { title => 'Recharge my account with a prepaid card', + url => 'recharge_prepay', + indent => 2, + } + if grep(/^PREP/, @cust_paybys); } diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index 865b5cecd..bb3db12c6 100644 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -10,7 +10,7 @@ use HTML::Entities; use Date::Format; use Number::Format 1.50; use FS::SelfService qw( login_info login customer_info edit_info invoice - payment_info process_payment + payment_info process_payment realtime_collect process_prepay list_pkgs order_pkg signup_info order_recharge part_svc_info provision_acct provision_external @@ -72,7 +72,7 @@ $session_id = $cgi->param('session'); #order|pw_list XXX ??? $cgi->param('action') =~ - /^(myaccount|view_invoice|make_payment|make_ach_payment|payment_results|ach_payment_results|recharge_prepay|recharge_results|logout|change_bill|change_ship|change_pay|process_change_bill|process_change_ship|process_change_pay|customer_order_pkg|process_order_pkg|customer_change_pkg|process_change_pkg|process_order_recharge|provision|provision_svc|process_svc_acct|process_svc_external|delete_svc|view_usage|view_usage_details|view_support_details|change_password|process_change_password)$/ + /^(myaccount|view_invoice|make_payment|make_ach_payment|make_thirdparty_payment|payment_results|ach_payment_results|recharge_prepay|recharge_results|logout|change_bill|change_ship|change_pay|process_change_bill|process_change_ship|process_change_pay|customer_order_pkg|process_order_pkg|customer_change_pkg|process_change_pkg|process_order_recharge|provision|provision_svc|process_svc_acct|process_svc_external|delete_svc|view_usage|view_usage_details|view_support_details|change_password|process_change_password)$/ or die "unknown action ". $cgi->param('action'); my $action = $1; @@ -98,6 +98,7 @@ warn "processing template $action\n" do_template($action, { 'session_id' => $session_id, 'action' => $action, #so the menu knows what tab we're on... + %{ payment_info( 'session_id' => $session_id ) }, # cust_paybys for the menu %{$result} }); @@ -472,6 +473,12 @@ sub ach_payment_results { } +sub make_thirdparty_payment { + $cgi->param('payby_method') =~ /^(CC|ECHECK)$/ + or die "illegal payby method"; + realtime_collect( 'session_id' => $session_id, 'method' => $1 ); +} + sub recharge_prepay { customer_info( 'session_id' => $session_id ); } diff --git a/fs_selfservice/FS-SelfService/cgi/signup.cgi b/fs_selfservice/FS-SelfService/cgi/signup.cgi index 47857f0a7..12452e686 100755 --- a/fs_selfservice/FS-SelfService/cgi/signup.cgi +++ b/fs_selfservice/FS-SelfService/cgi/signup.cgi @@ -8,11 +8,12 @@ use vars qw( @payby $cgi $init_data $ieak_file $ieak_template $signup_html $signup_template $success_html $success_template + $collect_html $collect_template $decline_html $decline_template ); use subs qw( print_form print_okay print_decline - success_default decline_default + success_default collect_default decline_default ); use CGI; #use CGI::Carp qw(fatalsToBrowser); @@ -35,6 +36,9 @@ $signup_html = -e 'signup.html' $success_html = -e 'success.html' ? 'success.html' : '/usr/local/freeside/success.html'; +$collect_html = -e 'collect.html' + ? 'collect.html' + : '/usr/local/freeside/collect.html'; $decline_html = -e 'decline.html' ? 'decline.html' : '/usr/local/freeside/decline.html'; @@ -97,6 +101,24 @@ if ( -e $success_html ) { or die $Text::Template::ERROR; } +if ( -e $collect_html ) { + my $collect_txt = Text::Template::_load_text($collect_html) + or die $Text::Template::ERROR; + $collect_txt =~ /^(.*)$/s; #untaint the template source - it's trusted + $collect_txt = $1; + $collect_template = new Text::Template ( TYPE => 'STRING', + SOURCE => $collect_txt, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} else { + $collect_template = new Text::Template ( TYPE => 'STRING', + SOURCE => &collect_default, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} + if ( -e $decline_html ) { my $decline_txt = Text::Template::_load_text($decline_html) or die $Text::Template::ERROR; @@ -122,9 +144,10 @@ $init_data = signup_info( 'agentnum' => $agentnum, 'reg_code' => uc(scalar($cgi->param('reg_code'))), ); -if ( ( defined($cgi->param('magic')) && $cgi->param('magic') eq 'process' ) - || ( defined($cgi->param('action')) && $cgi->param('action') eq 'process_signup' ) - ) { +my $magic = $cgi->param('magic') || ''; +my $action = $cgi->param('action') || ''; + +if ( $magic eq 'process' || $action eq 'process_signup' ) { $error = ''; @@ -218,6 +241,10 @@ if ( ( defined($cgi->param('magic')) && $cgi->param('magic') eq 'process' ) if ( $error eq '_decline' ) { print_decline(); + } elsif ( $error eq '_collect' ) { + map { $cgi->param($_, $rv->{$_}) } + qw( popup_url reference collectitems amount ); + print_collect(); } elsif ( $error ) { #fudge the snarf info no strict 'refs'; @@ -230,6 +257,16 @@ if ( ( defined($cgi->param('magic')) && $cgi->param('magic') eq 'process' ) ); } +} elsif ( $magic eq 'success' || $action eq 'success' ) { + + $cgi->param('username', 'username'); #hmmm temp kludge + $cgi->param('_password', 'password'); + print_okay( map { /^([\w ]+)$/ ? ( $_ => $1 ) : () } $cgi->param ); #hmmm + +} elsif ( $magic eq 'decline' || $action eq 'decline' ) { + + print_decline(); + } else { $error = ''; print_form; @@ -258,6 +295,27 @@ sub print_form { ); } +sub print_collect { + + $error = "Error: $error" if $error; + + my $r = { + $cgi->Vars, + %{$init_data}, + 'error' => $error, + }; + + $r->{pkgpart} ||= $r->{default_pkgpart}; + + $r->{referral_custnum} = $r->{'ref'}; + $r->{self_url} = $cgi->self_url; + + print $cgi->header( '-expires' => 'now' ), + $collect_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi', + HASH => $r + ); +} + sub print_decline { print $cgi->header( '-expires' => 'now' ), $decline_template->fill_in(); @@ -369,6 +427,37 @@ Package: <%= $pkg %>
END } +sub collect_default { #html to use if there is a collect phase + <<'END'; +Pay now +Pay now

+ + + + + + +You are about to contact our payment processor to pay <%= $amount %> for +<%= $pkg %>.

+Your transaction reference number is <%= $reference %>

+
+<%= + my %itemhash = @collectitems; + foreach my $input (keys %itemhash) { + $OUT .= qq!!; + } +%> + +
+ +END +} + sub decline_default { #html to use if there is a decline <<'END'; Processing error diff --git a/fs_selfservice/FS-SelfService/cgi/signup.html b/fs_selfservice/FS-SelfService/cgi/signup.html index 1b97121c6..ae7b2226a 100755 --- a/fs_selfservice/FS-SelfService/cgi/signup.html +++ b/fs_selfservice/FS-SelfService/cgi/signup.html @@ -245,7 +245,7 @@ HTML::Widgets::SelectLayers->new( form_name => 'dummy', html_between => '', form_action => 'dummy.cgi', - layer_callback => sub { my $layer = shift; return $paybychecked{$layer}. ''; }, + layer_callback => sub { my $layer = shift; return ( shift @hide_payment_fields ? '' : $paybychecked{$layer} ) . ''; }, )->html; diff --git a/fs_selfservice/FS-SelfService/cgi/verify.cgi b/fs_selfservice/FS-SelfService/cgi/verify.cgi new file mode 100755 index 000000000..0f8bfccc8 --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/verify.cgi @@ -0,0 +1,175 @@ +#!/usr/bin/perl -T +#!/usr/bin/perl -Tw + +use strict; +use vars qw( $cgi $self_url $error + $verify_html $verify_template + $success_html $success_template + $decline_html $decline_template + ); + +use subs qw( print_verify print_okay print_decline + verify_default success_default decline_default + ); +use CGI; +use Text::Template; +use FS::SelfService qw( capture_payment ); + +$verify_html = -e 'verify.html' + ? 'verify.html' + : '/usr/local/freeside/verify.html'; +$success_html = -e 'verify_success.html' + ? 'success.html' + : '/usr/local/freeside/success.html'; +$decline_html = -e 'verify_decline.html' + ? 'decline.html' + : '/usr/local/freeside/decline.html'; + + +if ( -e $verify_html ) { + my $verify_txt = Text::Template::_load_text($verify_html) + or die $Text::Template::ERROR; + $verify_txt =~ /^(.*)$/s; #untaint the template source - it's trusted + $verify_txt = $1; + $verify_template = new Text::Template ( TYPE => 'STRING', + SOURCE => $verify_txt, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} else { + $verify_template = new Text::Template ( TYPE => 'STRING', + SOURCE => &verify_default, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} + +if ( -e $success_html ) { + my $success_txt = Text::Template::_load_text($success_html) + or die $Text::Template::ERROR; + $success_txt =~ /^(.*)$/s; #untaint the template source - it's trusted + $success_txt = $1; + $success_template = new Text::Template ( TYPE => 'STRING', + SOURCE => $success_txt, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} else { + $success_template = new Text::Template ( TYPE => 'STRING', + SOURCE => &success_default, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} + +if ( -e $decline_html ) { + my $decline_txt = Text::Template::_load_text($decline_html) + or die $Text::Template::ERROR; + $decline_txt =~ /^(.*)$/s; #untaint the template source - it's trusted + $decline_txt = $1; + $decline_template = new Text::Template ( TYPE => 'STRING', + SOURCE => $decline_txt, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} else { + $decline_template = new Text::Template ( TYPE => 'STRING', + SOURCE => &decline_default, + DELIMITERS => [ '<%=', '%>' ], + ) + or die $Text::Template::ERROR; +} + +$cgi = new CGI; + +my $rv = capture_payment( + data => { map { $_ => scalar($cgi->param($_)) } $cgi->param }, + url => $cgi->self_url, +); + +$error = $rv->{error}; + +if ( $error eq '_decline' ) { + print_decline(); +} elsif ( $error ) { + print_verify(); +} else { + print_okay(%$rv); +} + + +sub print_verify { + + $error = "Error: $error" if $error; + + my $r = { $cgi->Vars, 'error' => $error }; + + $r->{self_url} = $cgi->self_url; + + print $cgi->header( '-expires' => 'now' ), + $verify_template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi', + HASH => $r + ); +} + +sub print_decline { + print $cgi->header( '-expires' => 'now' ), + $decline_template->fill_in(); +} + +sub print_okay { + my %param = @_; + + my @success_url = split '/', $cgi->url(-path); + pop @success_url; + + my $success_url = join '/', @success_url; + if ($param{session_id}) { + my $session_id = lc($param{session_id}); + $success_url .= "/selfservice.cgi?action=myaccount&session=$session_id"; + } else { + $success_url .= '/signup.cgi?action=success'; + } + + print $cgi->header( '-expires' => 'now' ), + $success_template->fill_in( HASH => { success_url => $success_url } ); +} + +sub success_default { #html to use if you don't specify a success file + <<'END'; +Signup successful +Signup successful

+Thanks for signing up! +

+ + +END +} + +sub verify_default { #html to use for verification response + <<'END'; +Processing error +Processing error

+There has been an error processing your account. Please contact customer +support. + +END +} + +sub decline_default { #html to use if there is a decline + <<'END'; +Processing error +Processing error

+There has been an error processing your account. Please contact customer +support. + +END +} + +# subs for the templates... + +package FS::SelfService::_signupcgi; +use HTML::Entities; + diff --git a/httemplate/browse/payment_gateway.html b/httemplate/browse/payment_gateway.html index 848c58a82..a06e5cf7c 100644 --- a/httemplate/browse/payment_gateway.html +++ b/httemplate/browse/payment_gateway.html @@ -10,17 +10,21 @@ }, 'count_query' => $count_query, 'header' => [ '#', + 'Type', 'Gateway', 'Username', 'Password', 'Action', + 'URL', 'Options', ], 'fields' => [ 'gatewaynum', + 'namespace_description', $gateway_sub, 'gateway_username', sub { ' - '; }, 'gateway_action', + 'gateway_callback_url', $options_sub, ], ) diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html index e3893cf49..2b108f857 100644 --- a/httemplate/edit/payment_gateway.html +++ b/httemplate/edit/payment_gateway.html @@ -1,132 +1,122 @@ -<% include("/elements/header.html","$action Payment gateway", menubar( - 'View all payment gateways' => $p. 'browse/payment_gateway.html', -)) %> - -<% include('/elements/error.html') %> - -
- -Gateway #<% $payment_gateway->gatewaynum || "(NEW)" %> - -<% ntable('#cccccc', 2, '') %> - - - Gateway: - -% if ( $payment_gateway->gatewaynum ) { - - - <% $payment_gateway->gateway_module %> - -% } else { - - - +<% include( 'elements/edit.html', + 'table' => 'payment_gateway', + 'name_singular' => 'Payment gateway', + 'viewall_dir' => 'browse', + 'fields' => $fields, + 'field_callback' => $field_callback, + 'labels' => { + 'gatewaynum' => 'Gateway #', + 'gateway_module' => 'Gateway', + 'gateway_username' => 'Username', + 'gateway_password' => 'Password', + 'gateway_action' => 'Action', + 'gateway_options' => 'Options: (Name/Value pairs, one element per line)', + 'gateway_callback_url' => 'Callback URL', + }, + ) +%> + + + <%init> die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); -my $payment_gateway; -if ( $cgi->param('error') ) { - $payment_gateway = new FS::payment_gateway ( { - map { $_, scalar($cgi->param($_)) } fields('payment_gateway') - } ); -} elsif ( $cgi->keywords ) { - my($query) = $cgi->keywords; - $query =~ /^(\d+)$/; - $payment_gateway = qsearchs( 'payment_gateway', { 'gatewaynum' => $1 } ); -} else { #adding - $payment_gateway = new FS::payment_gateway {}; -} -my $action = $payment_gateway->gatewaynum ? 'Edit' : 'Add'; -#my $hashref = $payment_gateway->hashref; +my %modules = ( + '2CheckOut' => 'Business::OnlinePayment', + 'AuthorizeNet' => 'Business::OnlinePayment', + 'BankOfAmerica' => 'Business::OnlinePayment', + 'Beanstream' => 'Business::OnlinePayment', + 'Capstone' => 'Business::OnlinePayment', + 'Cardstream' => 'Business::OnlinePayment', + 'CashCow' => 'Business::OnlinePayment', + 'CyberSource' => 'Business::OnlinePayment', + 'eSec' => 'Business::OnlinePayment', + 'eSelectPlus' => 'Business::OnlinePayment', + 'Exact' => 'Business::OnlinePayment', + 'iAuthorizer' => 'Business::OnlinePayment', + 'Interswitchng' => 'Business::OnlineThirdPartyPayment', + 'IPaymentTPG' => 'Business::OnlinePayment', + 'Jettis' => 'Business::OnlinePayment', + 'LinkPoint' => 'Business::OnlinePayment', + 'MerchantCommerce' => 'Business::OnlinePayment', + 'Network1Financial' => 'Business::OnlinePayment', + 'OCV' => 'Business::OnlinePayment', + 'OpenECHO' => 'Business::OnlinePayment', + 'PayConnect' => 'Business::OnlinePayment', + 'PayflowPro' => 'Business::OnlinePayment', + 'PaymentsGateway' => 'Business::OnlinePayment', + 'PXPost' => 'Business::OnlinePayment', + 'SecureHostingUPG' => 'Business::OnlinePayment', + 'Skipjack' => 'Business::OnlinePayment', + 'StGeorge' => 'Business::OnlinePayment', + 'SurePay' => 'Business::OnlinePayment', + 'TCLink' => 'Business::OnlinePayment', + 'TransactionCentral' => 'Business::OnlinePayment', + 'TransFirsteLink' => 'Business::OnlinePayment', + 'VirtualNet' => 'Business::OnlinePayment', +); + +my @actions = ( + 'Normal Authorization', + 'Authorization Only', + 'Authorization Only, Post Authorization', + ); + +my $fields = [ + { + field => 'gateway_namespace', + type => 'hidden', + curr_value_callback => sub { my($cgi, $object, $fref) = @_; + $modules{$object->gateway_module} + || 'Business::OnlinePayment' + }, + }, + { + field => 'gateway_module', + type => 'select', + options => [ sort { lc($a) cmp lc ($b) } keys %modules ], + onchange => 'setNamespace', + }, + 'gateway_username', + 'gateway_password', + { + field => 'gateway_action', + type => 'select', + options => \@actions, + }, + 'gateway_callback_url', + { + field => 'gateway_options', + type => 'textarea', + curr_value_callback => sub { my($cgi, $object, $fref) = @_; + join("\r", $object->options ); + }, + }, + ]; + +my $field_callback = sub { + my ($cgi, $object, $field_hashref ) = @_; + if ($object->gatewaynum) { + if ( $field_hashref->{field} eq 'gateway_module' ) { + $field_hashref->{type} = 'fixed'; + } + } +}; diff --git a/httemplate/edit/process/payment_gateway.html b/httemplate/edit/process/payment_gateway.html index b16bc3d27..812c988c5 100644 --- a/httemplate/edit/process/payment_gateway.html +++ b/httemplate/edit/process/payment_gateway.html @@ -1,35 +1,22 @@ -%if ( $error ) { -% $cgi->param('error', $error); -<% $cgi->redirect(popurl(2). "payment_gateway.html?". $cgi->query_string ) %> -%} else { -<% $cgi->redirect(popurl(3). "browse/payment_gateway.html") %> -%} +<% include( 'elements/process.html', + 'table' => 'payment_gateway', + 'viewall_dir' => 'browse', + 'args_callback' => $args_callback, + ) +%> <%init> die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); -my $gatewaynum = $cgi->param('gatewaynum'); +my $args_callback = sub { + my ( $cgi, $new ) = @_; -my $old = qsearchs('payment_gateway',{'gatewaynum'=>$gatewaynum}) if $gatewaynum; + my @options = split(/\r?\n/, $cgi->param('gateway_options') ); + pop @options + if scalar(@options) % 2 && $options[-1] =~ /^\s*$/; + (@options) +}; -my $new = new FS::payment_gateway ( { - map { - $_, scalar($cgi->param($_)); - } fields('payment_gateway') -} ); - -my @options = split(/\r?\n/, $cgi->param('gateway_options') ); -pop @options - if scalar(@options) % 2 && $options[-1] =~ /^\s*$/; -my %options = @options; - -my $error; -if ( $gatewaynum ) { - $error=$new->replace($old, \%options); -} else { - $error=$new->insert(\%options); - $gatewaynum=$new->getfield('gatewaynum'); -} diff --git a/httemplate/elements/tr-textarea.html b/httemplate/elements/tr-textarea.html new file mode 100644 index 000000000..fb41ac38f --- /dev/null +++ b/httemplate/elements/tr-textarea.html @@ -0,0 +1,25 @@ +<% include('tr-td-label.html', @_ ) %> + + > + + + + + + + +<%init> + +my %opt = @_; + +my $onchange = $opt{'onchange'} + ? 'onChange="'. $opt{'onchange'}. '(this)"' + : ''; + +my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : ''; +my $curr_value = $opt{'curr_value'}; + +