From 32db3ad86bcf04e4f34705a396b718061d333f20 Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 10 Mar 2009 16:14:11 +0000 Subject: merge webpay support in with autoselection of old realtime_bop and realtime_refund_bop --- FS/FS/ClientAPI/MyAccount.pm | 35 ++ FS/FS/ClientAPI/Signup.pm | 141 ++++- FS/FS/Conf.pm | 12 + FS/FS/Schema.pm | 4 + FS/FS/agent.pm | 102 +++ FS/FS/cust_main.pm | 1410 ++++++++++++++++++++++++++++++++++++++---- FS/FS/cust_pay_pending.pm | 13 + FS/FS/cust_pkg.pm | 4 +- FS/FS/payby.pm | 15 + FS/FS/payment_gateway.pm | 49 +- 10 files changed, 1663 insertions(+), 122 deletions(-) (limited to 'FS') 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); @@ -619,6 +620,17 @@ worry that config_items is freeside-specific and icky. 'type' => 'textarea', }, + { + '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', 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 -- cgit v1.2.1 From d3c80e75b62421ea742cbe4547305227f8c10bea Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 11 Mar 2009 09:41:51 +0000 Subject: add cdr display with accountcode included, RT#4405 --- FS/FS/cdr.pm | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'FS') diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 67c5c1c2a..f0082279e 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -464,6 +464,10 @@ my %export_names = ( 'name' => 'Default with source', 'invoice_header' => 'Caller,Date,Time,Number,Destination,Duration,Price', }, + 'accountcode_default' => { + 'name' => 'Default plus accountcode', + 'invoice_header' => 'Caller,Date,Time,Number,Destination,Duration,Price', + }, ); my %export_formats = ( @@ -528,6 +532,11 @@ my %export_formats = ( ], ); $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ]; +$export_formats{'accountcode_default'} = + [ @{ $export_formats{'default'} }[0,1], + 'accountcode', + @{ $export_formats{'default'} }[2..5], + ]; sub downstream_csv { my( $self, %opt ) = @_; -- cgit v1.2.1 From 2ea58badb5e0e2985a796f8b614912db9b6e43bf Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 11 Mar 2009 10:04:00 +0000 Subject: add previous_balance-summary_only config, RT#4404 --- FS/FS/Conf.pm | 7 +++++++ FS/FS/cust_bill.pm | 52 ++++++++++++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 24 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 3921afdaa..3f150e1e7 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2485,6 +2485,13 @@ worry that config_items is freeside-specific and icky. 'type' => 'checkbox', }, + { + 'key' => 'previous_balance-summary_only', + 'section' => 'billing', + 'description' => 'Only show a single line summarizing the total previous balance rather than one line per invoice.', + 'type' => 'checkbox', + }, + { 'key' => 'usps_webtools-userid', 'section' => 'UI', diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 704b3504a..1f78d72f4 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2080,33 +2080,37 @@ sub print_generic { push @sections, { 'description' => '', 'subtotal' => '' }; } - foreach my $line_item ( $conf->exists('disable_previous_balance') - ? () - : $self->_items_previous - ) + unless ( $conf->exists('disable_previous_balance') + || $conf->exists('previous_balance-summary_only') + ) { - my $detail = { - ext_description => [], - }; - $detail->{'ref'} = $line_item->{'pkgnum'}; - $detail->{'quantity'} = 1; - $detail->{'section'} = $previous_section; - $detail->{'description'} = &$escape_function($line_item->{'description'}); - if ( exists $line_item->{'ext_description'} ) { - @{$detail->{'ext_description'}} = map { - &$escape_function($_); - } @{$line_item->{'ext_description'}}; + + foreach my $line_item ( $self->_items_previous ) { + + my $detail = { + ext_description => [], + }; + $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'quantity'} = 1; + $detail->{'section'} = $previous_section; + $detail->{'description'} = &$escape_function($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + @{$detail->{'ext_description'}} = map { + &$escape_function($_); + } @{$line_item->{'ext_description'}}; + } + $detail->{'amount'} = ( $old_latex ? '' : $money_char). + $line_item->{'amount'}; + $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; + + push @detail_items, $detail; + push @buf, [ $detail->{'description'}, + $money_char. sprintf("%10.2f", $line_item->{'amount'}), + ]; } - $detail->{'amount'} = ( $old_latex ? '' : $money_char). - $line_item->{'amount'}; - $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; - - push @detail_items, $detail; - push @buf, [ $detail->{'description'}, - $money_char. sprintf("%10.2f", $line_item->{'amount'}), - ]; + } - + if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) { push @buf, ['','-----------']; push @buf, [ 'Total Previous Balance', -- cgit v1.2.1 From 64916ad920572e19351df79127d4822fab57f621 Mon Sep 17 00:00:00 2001 From: jeff Date: Fri, 13 Mar 2009 18:22:21 +0000 Subject: prevent more duplicate MACs from sneaking in in the interim --- FS/FS/svc_broadband.pm | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'FS') diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index b808527a1..eb2964635 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -217,6 +217,10 @@ sub check { ; return $error if $error; + #redundant, but prevents further problems until column constraint in place + return "MAC already in use" + if scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) ); + if($self->speed_up < 0) { return 'speed_up must be positive'; } if($self->speed_down < 0) { return 'speed_down must be positive'; } -- cgit v1.2.1 From dae7ad7ff2a06ae047de9fd6d88e74edc526b4ad Mon Sep 17 00:00:00 2001 From: ivan Date: Sat, 14 Mar 2009 23:44:38 +0000 Subject: fix emailed logos to come from db config, not old files, RT#3093 / RT#4963 --- FS/FS/cust_bill.pm | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 1f78d72f4..c8384c009 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -660,21 +660,22 @@ sub generate_email { my $from = $1 || 'example.com'; my $content_id = join('.', rand()*(2**32), $$, time). "\@$from"; - my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc"; - my $file; + my $logo; + my $agentnum = $self->cust_main->agentnum; if ( defined($args{'template'}) && length($args{'template'}) - && -e "$path/logo_". $args{'template'}. ".png" + && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum ) ) { - $file = "$path/logo_". $args{'template'}. ".png"; + $logo = 'logo_'. $args{'template'}. '.png'; } else { - $file = "$path/logo.png"; + $logo = "logo.png"; } + my $image_data = $conf->config_binary( $logo, $agentnum); my $image = build MIME::Entity 'Type' => 'image/png', 'Encoding' => 'base64', - 'Path' => $file, + 'Data' => $image_data, 'Filename' => 'logo.png', 'Content-ID' => "<$content_id>", ; -- cgit v1.2.1 From 5cf0508a1ec3dafaeef8a4bd206ef5e5b51235c8 Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 15 Mar 2009 07:33:06 +0000 Subject: cust_main::payment_info, for ClientAPI::MyAccount --- FS/FS/cust_main.pm | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 2bad5ec3e..69126956a 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -6032,6 +6032,86 @@ sub in_transit_payments { sprintf( "%.2f", $in_transit_payments ); } +=item payment_info + +Returns a hash of useful information for making a payment. + +=over 4 + +=item balance + +Current balance. + +=item payby + +'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand), +'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand), +'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free). + +=back + +For credit card transactions: + +=over 4 + +=item card_type 1 + +=item payname + +Exact name on card + +=back + +For electronic check transactions: + +=over 4 + +=item stateid_state + +=back + +=cut + +sub payment_info { + my $self = shift; + + my %return = (); + + $return{balance} = $self->balance; + + $return{payname} = $self->payname + || ( $self->first. ' '. $self->get('last') ); + + $return{$_} = $self->get($_) for qw(address1 address2 city state zip); + + $return{payby} = $self->payby; + $return{stateid_state} = $self->stateid_state; + + if ( $self->payby =~ /^(CARD|DCRD)$/ ) { + $return{card_type} = cardtype($self->payinfo); + $return{payinfo} = $self->paymask; + + @return{'month', 'year'} = $self->paydate_monthyear; + + } + + if ( $self->payby =~ /^(CHEK|DCHK)$/ ) { + my ($payinfo1, $payinfo2) = split '@', $self->paymask; + $return{payinfo1} = $payinfo1; + $return{payinfo2} = $payinfo2; + $return{paytype} = $self->paytype; + $return{paystate} = $self->paystate; + + } + + #doubleclick protection + my $_date = time; + $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; + + %return; + +} + =item paydate_monthyear Returns a two-element list consisting of the month and year of this customer's -- cgit v1.2.1 From dc24b4b7e2e41dbb3039e9ce367b018fef299ade Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 15 Mar 2009 10:30:26 +0000 Subject: we're not a disk drive manufacturer, don't use halfass base-10 megs/gigs --- FS/FS/UI/bytecount.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/UI/bytecount.pm b/FS/FS/UI/bytecount.pm index 0891e6d68..0ddc7545c 100644 --- a/FS/FS/UI/bytecount.pm +++ b/FS/FS/UI/bytecount.pm @@ -32,9 +32,9 @@ sub bytecount_unexact { return("$bc bytes") if ($bc < 1000); return(sprintf("%.2f Kbytes", $bc/1024)) - if ($bc < 1000000); + if ($bc < 1048576); return(sprintf("%.2f Mbytes", $bc/1048576)) - if ($bc < 1000000000); + if ($bc < 1073741824); return(sprintf("%.2f Gbytes", $bc/1073741824)); } -- cgit v1.2.1 From fd1bf67102a849280d78a041eff33e87c3cc7179 Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 15 Mar 2009 10:46:31 +0000 Subject: fix application of data fields from prepaid cards in addition to time field --- FS/FS/cust_main.pm | 95 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 32 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 69126956a..27a617e1a 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -23,6 +23,7 @@ use FS::UID qw( getotaker dbh driver_name ); use FS::Record qw( qsearchs qsearch dbdef ); use FS::Misc qw( generate_email send_email generate_ps do_print ); use FS::Msgcat qw(gettext); +use FS::payby; use FS::cust_pkg; use FS::cust_svc; use FS::cust_bill; @@ -392,7 +393,7 @@ sub insert { my $dbh = dbh; my $prepay_identifier = ''; - my( $amount, $seconds ) = ( 0, 0 ); + my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0); my $payby = ''; if ( $self->payby eq 'PREPAY' ) { @@ -403,7 +404,13 @@ sub insert { warn " looking up prepaid card $prepay_identifier\n" if $DEBUG > 1; - my $error = $self->get_prepay($prepay_identifier, \$amount, \$seconds); + my $error = $self->get_prepay( $prepay_identifier, + 'amount_ref' => \$amount, + 'seconds_ref' => \$seconds, + 'upbytes_ref' => \$upbytes, + 'downbytes_ref' => \$downbytes, + 'totalbytes_ref' => \$totalbytes, + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; #return "error applying prepaid card (transaction rolled back): $error"; @@ -465,7 +472,13 @@ sub insert { warn " ordering packages\n" if $DEBUG > 1; - $error = $self->order_pkgs($cust_pkgs, \$seconds, %options); + $error = $self->order_pkgs( $cust_pkgs, + %options, + 'seconds_ref' => \$seconds, + 'upbytes_ref' => \$upbytes, + 'downbytes_ref' => \$downbytes, + 'totalbytes_ref' => \$totalbytes, + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -475,6 +488,10 @@ sub insert { $dbh->rollback if $oldAutoCommit; return "No svc_acct record to apply pre-paid time"; } + if ( $upbytes || $downbytes || $totalbytes ) { + $dbh->rollback if $oldAutoCommit; + return "No svc_acct record to apply pre-paid data"; + } if ( $amount ) { warn " inserting initial $payby payment of $amount\n" @@ -701,7 +718,6 @@ sub order_pkg { if $DEBUG; my $cust_pkg = $opt->{'cust_pkg'}; - my $seconds = $opt->{'seconds'}; my $svcs = $opt->{'svcs'} || []; my %svc_options = (); @@ -745,9 +761,12 @@ sub order_pkg { $error = $new_cust_svc->replace($old_cust_svc); } else { $svc_something->pkgnum( $cust_pkg->pkgnum ); - if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) { - $svc_something->seconds( $svc_something->seconds + $$seconds ); - $$seconds = 0; + if ( $svc_something->isa('FS::svc_acct') ) { + foreach ( grep { $opt->{$_.'_ref'} && ${ $opt->{$_.'_ref'} } } + qw( seconds upbytes downbytes totalbytes ) ) { + $svc_something->$_( $svc_something->$_() + ${ $opt->{$_.'_ref'} } ); + ${ $opt->{$_.'_ref'} } = 0; + } } $error = $svc_something->insert(%svc_options); } @@ -762,7 +781,8 @@ sub order_pkg { } -=item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ] +#deprecated #=item order_pkgs HASHREF [ , SECONDSREF ] [ , OPTION => VALUE ... ] +=item order_pkgs HASHREF [ , OPTION => VALUE ... ] Like the insert method on an existing record, this method orders multiple packages and included services atomicaly. Pass a Tie::RefHash data structure @@ -776,12 +796,13 @@ example: $cust_pkg => [ $svc_acct ], ... ); - $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 ); + $cust_main->order_pkgs( \%hash, 'noexport'=>1 ); Services can be new, in which case they are inserted, or existing unaudited services, in which case they are linked to the newly-created package. -Currently available options are: I and I. +Currently available options are: I, I, I, +I, I, and I. If I is set, all provisioning jobs will have a dependancy on the supplied jobnum (they will not run until the specific job completes). @@ -794,13 +815,18 @@ the B method for each cust_pkg object. Using the B method on the cust_main object is not recommended, as existing services will also be reexported.) +If I, I, I, or I is +provided, the scalars (provided by references) will be incremented by the +values of the prepaid card.` + =cut sub order_pkgs { my $self = shift; my $cust_pkgs = shift; - my $seconds = shift; + my $seconds_ref = ref($_[0]) ? shift : ''; #deprecated my %options = @_; + $seconds_ref ||= $options{'seconds_ref'}; warn "$me order_pkgs called with options ". join(', ', map { "$_: $options{$_}" } keys %options ). "\n" @@ -821,11 +847,14 @@ sub order_pkgs { foreach my $cust_pkg ( keys %$cust_pkgs ) { - my $error = $self->order_pkg( 'cust_pkg' => $cust_pkg, - 'svcs' => $cust_pkgs->{$cust_pkg}, - 'seconds' => $seconds, - 'depend_jobnum' => $options{'depend_jobnum'}, - ); + my $error = $self->order_pkg( + 'cust_pkg' => $cust_pkg, + 'svcs' => $cust_pkgs->{$cust_pkg}, + 'seconds_ref' => $seconds_ref, + map { $_ => $options{$_} } qw( upbytes_ref downbytes_ref totalbytes_ref + depend_jobnum + ) + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -844,13 +873,14 @@ L), specified either by I or as an FS::prepay_credit object. If there is an error, returns the error, otherwise returns false. -Optionally, four scalar references can be passed as well. They will have their -values filled in with the amount, number of seconds, and number of upload and -download bytes applied by this prepaid -card. +Optionally, five scalar references can be passed as well. They will have their +values filled in with the amount, number of seconds, and number of upload, +download, and total bytes applied by this prepaid card. =cut +#the ref bullshit here should be refactored like get_prepay. MyAccount.pm is +#the only place that uses these args sub recharge_prepay { my( $self, $prepay_credit, $amountref, $secondsref, $upbytesref, $downbytesref, $totalbytesref ) = @_; @@ -868,8 +898,13 @@ sub recharge_prepay { my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 ); - my $error = $self->get_prepay($prepay_credit, \$amount, - \$seconds, \$upbytes, \$downbytes, \$totalbytes) + my $error = $self->get_prepay( $prepay_credit, + 'amount_ref' => \$amount, + 'seconds_ref' => \$seconds, + 'upbytes_ref' => \$upbytes, + 'downbytes_ref' => \$downbytes, + 'totalbytes_ref' => \$totalbytes, + ) || $self->increment_seconds($seconds) || $self->increment_upbytes($upbytes) || $self->increment_downbytes($downbytes) @@ -896,13 +931,13 @@ sub recharge_prepay { } -=item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF +=item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ] Looks up and deletes a prepaid card (see L), specified either by I or as an FS::prepay_credit object. -References to I and I scalars should be passed as arguments -and will be incremented by the values of the prepaid card. +Available options are: I, I, I, I, and I. The scalars (provided by references) will be +incremented by the values of the prepaid card. If the prepaid card specifies an I (see L), it is used to check or set this customer's I. @@ -913,8 +948,7 @@ If there is an error, returns the error, otherwise returns false. sub get_prepay { - my( $self, $prepay_credit, $amountref, $secondsref, - $upref, $downref, $totalref) = @_; + my( $self, $prepay_credit, %opt ) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -959,11 +993,8 @@ sub get_prepay { return "removing prepay_credit (transaction rolled back): $error"; } - $$amountref += $prepay_credit->amount; - $$secondsref += $prepay_credit->seconds; - $$upref += $prepay_credit->upbytes; - $$downref += $prepay_credit->downbytes; - $$totalref += $prepay_credit->totalbytes; + ${ $opt{$_.'_ref'} } += $prepay_credit->$_() + for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes ); $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; -- cgit v1.2.1 From fcd4ede322d50422e38426908c4ef62611043435 Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 15 Mar 2009 19:42:33 +0000 Subject: should give better performance if we search for what we want instead of using a string match --- FS/FS/cust_pkg_reason.pm | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_pkg_reason.pm b/FS/FS/cust_pkg_reason.pm index 403751369..bb0542be2 100644 --- a/FS/FS/cust_pkg_reason.pm +++ b/FS/FS/cust_pkg_reason.pm @@ -136,12 +136,15 @@ sub reasontext { use FS::h_cust_pkg; use FS::h_cust_pkg_reason; +use FS::Schema qw(dbdef); sub _upgrade_data { # class method my ($class, %opts) = @_; - my $test_cust_pkg_reason = new FS::cust_pkg_reason; - return '' unless $test_cust_pkg_reason->dbdef_table->column('action'); + return '' unless dbdef->table('cust_pkg_reason')->column('action'); + + my $action_replace = + " AND ( history_action = 'replace_old' OR history_action = 'replace_new' )"; my $count = 0; my @unmigrated = qsearch('cust_pkg_reason', { 'action' => '' } ); @@ -151,27 +154,24 @@ sub _upgrade_data { # class method next unless scalar(@history_cust_pkg_reason) == 1; - my %action_value = ( op => 'LIKE', - value => 'replace_%', - ); my $hashref = { pkgnum => $_->pkgnum, history_date => $history_cust_pkg_reason[0]->history_date, - history_action => { %action_value }, }; - my @history = qsearch({ table => 'h_cust_pkg', - hashref => $hashref, - order_by => 'ORDER BY history_action', + my @history = qsearch({ table => 'h_cust_pkg', + hashref => $hashref, + extra_sql => $action_replace, + order_by => 'ORDER BY history_action', }); my $fuzz = 0; while (scalar(@history) < 2 && $fuzz < 3) { $hashref->{history_date}++; - $hashref->{history_action} = { %action_value }; # qsearch distorts this! $fuzz++; - push @history, qsearch({ table => 'h_cust_pkg', - hashref => $hashref, - order_by => 'ORDER BY history_action', + push @history, qsearch({ table => 'h_cust_pkg', + hashref => $hashref, + extra_sql => $action_replace, + order_by => 'ORDER BY history_action', }); } @@ -226,26 +226,23 @@ sub _upgrade_data { # class method }); foreach ( @unmigrated ) { - my %action_value = ( op => 'LIKE', - value => 'replace_%', - ); my $hashref = { pkgnum => $_->pkgnum, history_date => $_->date, - history_action => { %action_value }, }; - my @history = qsearch({ table => 'h_cust_pkg', - hashref => $hashref, - order_by => 'ORDER BY history_action', + my @history = qsearch({ table => 'h_cust_pkg', + hashref => $hashref, + extra_sql => $action_replace, + order_by => 'ORDER BY history_action', }); my $fuzz = 0; while (scalar(@history) < 2 && $fuzz < 3) { $hashref->{history_date}++; - $hashref->{history_action} = { %action_value }; # qsearch distorts this! $fuzz++; push @history, qsearch({ table => 'h_cust_pkg', hashref => $hashref, + extra_sql => $action_replace, order_by => 'ORDER BY history_action', }); } -- cgit v1.2.1 From 1c4c7cf9e2c1b4baae0453d844e23391cda7dfbb Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 15 Mar 2009 22:33:09 +0000 Subject: don't throw 'Use of uninitialized value in addition (+) at /usr/local/share/perl/5.8.8/FS/cust_svc.pm line 626.' error when using attribute_since_sqlradacct --- FS/FS/cust_svc.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index 30b23908c..320f78aa0 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -629,7 +629,8 @@ sub attribute_since_sqlradacct { ) or die $dbh->errstr; $sth->execute($username, $start, $end) or die $sth->errstr; - $sum += $sth->fetchrow_arrayref->[0]; + my $row = $sth->fetchrow_arrayref; + $sum += $row->[0] if defined($row->[0]); warn "$mes done SUMing sessions\n" if $DEBUG; -- cgit v1.2.1 From 8d419aa7155fd8edd1f2f2338265ec372a8eea5d Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 16 Mar 2009 00:57:59 +0000 Subject: comment change --- FS/FS/svc_broadband.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index eb2964635..d4d41018a 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -217,7 +217,7 @@ sub check { ; return $error if $error; - #redundant, but prevents further problems until column constraint in place + #redundant, but better error message return "MAC already in use" if scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) ); -- cgit v1.2.1 From 7f37738733c36b6af0f421779addac3e9af9a809 Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 16 Mar 2009 04:22:42 +0000 Subject: use part_svc_router --- FS/FS/svc_broadband.pm | 1 + 1 file changed, 1 insertion(+) (limited to 'FS') diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index d4d41018a..d9a25b487 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -6,6 +6,7 @@ use FS::Record qw( qsearchs qsearch dbh ); use FS::svc_Common; use FS::cust_svc; use FS::addr_block; +use FS::part_svc_router; use NetAddr::IP; @ISA = qw( FS::svc_Common ); -- cgit v1.2.1 From 59a12152b6521065b8a2a82a48575445b6cd61d8 Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 16 Mar 2009 08:08:44 +0000 Subject: get the dup checking right --- FS/FS/svc_broadband.pm | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index d9a25b487..6007c7083 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -111,6 +111,8 @@ sub table_info { sub table { 'svc_broadband'; } +sub table_dupcheck_fields { ( 'mac_addr' ); } + =item search_sql STRING Class method which returns an SQL fragment to search for the given string. @@ -218,10 +220,6 @@ sub check { ; return $error if $error; - #redundant, but better error message - return "MAC already in use" - if scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) ); - if($self->speed_up < 0) { return 'speed_up must be positive'; } if($self->speed_down < 0) { return 'speed_down must be positive'; } @@ -293,6 +291,18 @@ sub check { $self->SUPER::check; } +sub _check_duplicate { + my $self = shift; + + return "MAC already in use" + if ( $self->mac_addr && + scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) ) + ); + + ''; +} + + =item NetAddr Returns a NetAddr::IP object containing the IP address of this service. The netmask -- cgit v1.2.1 From d3784b40552e9f00c9d803ac0833e09828c73d96 Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 16 Mar 2009 17:06:40 +0000 Subject: have prizm use service data rather than package data to select a profile RT#4853 --- FS/FS/Schema.pm | 1 + FS/FS/part_export/prizm.pm | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 65f7a7f40..4ffeaa2e8 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1666,6 +1666,7 @@ sub tables_hashref { 'longitude', 'decimal', 'NULL', '', '', '', 'altitude', 'decimal', 'NULL', '', '', '', 'vlan_profile', 'varchar', 'NULL', $char_d, '', '', + 'performance_profile', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'svcnum', 'unique' => [ [ 'mac_addr' ] ], diff --git a/FS/FS/part_export/prizm.pm b/FS/FS/part_export/prizm.pm index 2d4d8584c..97054408f 100644 --- a/FS/FS/part_export/prizm.pm +++ b/FS/FS/part_export/prizm.pm @@ -200,6 +200,9 @@ sub _export_insert { # } # } + my $performance_profile = $svc->performance_profile; + $performance_profile ||= $svc->cust_svc->cust_pkg->part_pkg->pkg; + my $element_name_length = 50; $element_name_length = $1 if $self->option('element_name_length') =~ /^\s*(\d+)\s*$/; @@ -211,7 +214,7 @@ sub _export_insert { $location, $contact, sprintf("%032X", $svc->authkey), - $svc->cust_svc->cust_pkg->part_pkg->pkg, + $performance_profile, $svc->vlan_profile, ($self->option('ems') ? 1 : 0 ), ); @@ -256,7 +259,7 @@ sub _export_insert { $err_or_som = $self->prizm_command('NetworkIfService', 'setElementConfigSet', [ $element ], - $svc->cust_svc->cust_pkg->part_pkg->pkg, + $performance_profile, 0, 1, ); @@ -395,9 +398,12 @@ sub _export_replace { return $err_or_som unless ref($err_or_som); + my $performance_profile = $new->performance_profile; + $performance_profile ||= $new->cust_svc->cust_pkg->part_pkg->pkg; + $err_or_som = $self->prizm_command('NetworkIfService', 'setElementConfigSet', [ $element ], - $new->cust_svc->cust_pkg->part_pkg->pkg, + $performance_profile, 0, 1, ); -- cgit v1.2.1 From 541207eb5505eee6eafd25e861230bdb36ac5fb3 Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 16 Mar 2009 23:28:28 +0000 Subject: avoid the need for approximate comparisons RT#4903 --- FS/FS/Schema.pm | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) (limited to 'FS') diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 4ffeaa2e8..7307165da 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -320,6 +320,8 @@ sub tables_hashref { my @perl_type = ( 'text', 'NULL', '' ); my @money_type = ( 'decimal', '', '10,2' ); my @money_typen = ( 'decimal', 'NULL', '10,2' ); + my @taxrate_type = ( 'decimal', '', '14,8' ); # requires pg 8 for + my @taxrate_typen = ( 'decimal', 'NULL', '14,8' ); # fs-upgrade to work my $username_len = 32; #usernamemax config file @@ -770,17 +772,17 @@ sub tables_hashref { 'location', 'varchar', 'NULL', $char_d, '', '',#provided by tax authority 'taxclassnum', 'int', '', '', '', '', 'effective_date', @date_type, '', '', - 'tax', 'real', '', '', '', '', # tax % - 'excessrate', 'real', 'NULL','', '', '', # second tax % + 'tax', @taxrate_type, '', '', # tax % + 'excessrate', @taxrate_typen, '', '', # second tax % 'taxbase', @money_typen, '', '', # amount at first tax rate 'taxmax', @money_typen, '', '', # maximum about at both rates - 'usetax', 'real', 'NULL', '', '', '', # tax % when non-local - 'useexcessrate', 'real', 'NULL', '', '', '', # second tax % when non-local + 'usetax', @taxrate_typen, '', '', # tax % when non-local + 'useexcessrate', @taxrate_typen, '', '', # second tax % when non-local 'unittype', 'int', 'NULL', '', '', '', # for fee - 'fee', 'real', 'NULL', '', '', '', # amount tax per unit - 'excessfee', 'real', 'NULL', '', '', '', # second amount tax per unit - 'feebase', 'real', 'NULL', '', '', '', # units taxed at first rate - 'feemax', 'real', 'NULL', '', '', '', # maximum number of unit taxed + 'fee', @taxrate_typen, '', '', # amount tax per unit + 'excessfee', @taxrate_typen, '', '', # second amount tax per unit + 'feebase', @taxrate_typen, '', '', # units taxed at first rate + 'feemax', @taxrate_typen, '', '', # maximum number of unit taxed 'maxtype', 'int', 'NULL', '', '', '', # indicator of how thresholds accumulate 'taxname', 'varchar', 'NULL', $char_d, '', '', # may appear on invoice 'taxauth', 'int', 'NULL', '', '', '', # tax authority -- cgit v1.2.1 From 2755d4a4810600f4392eaf73f362b4f358adeec6 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 09:58:17 +0000 Subject: add eps preview to config, for RT#5025 --- FS/FS/Conf.pm | 2 +- FS/FS/Mason.pm | 1 + FS/FS/Misc/eps2png.pm | 278 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 FS/FS/Misc/eps2png.pm (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 3f150e1e7..8ea57304c 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2304,7 +2304,7 @@ worry that config_items is freeside-specific and icky. 'key' => 'logo.eps', 'section' => 'billing', #? 'description' => 'Company logo for printed and PDF invoices, in EPS format.', - 'type' => 'binary', + 'type' => 'image', 'per_agent' => 1, #XXX as above, kinda }, diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index ee777a485..bcdc2fe8e 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -91,6 +91,7 @@ Initializes the Mason environment, loads all Freeside and RT libraries, etc. use FS::UI::bytecount; use FS::Msgcat qw(gettext geterror); use FS::Misc qw( send_email send_fax states_hash counties state_label ); + use FS::Misc::eps2png qw( eps2png ); use FS::Report::Table::Monthly; use FS::TicketSystem; use FS::Tron qw( tron_lint ); diff --git a/FS/FS/Misc/eps2png.pm b/FS/FS/Misc/eps2png.pm new file mode 100644 index 000000000..49c1d5682 --- /dev/null +++ b/FS/FS/Misc/eps2png.pm @@ -0,0 +1,278 @@ +package FS::Misc::eps2png; + +#based on eps2png by Johan Vromans +#Copyright 1994,2008 by Johan Vromans. +#This program is free software; you can redistribute it and/or +#modify it under the terms of the Perl Artistic License or the +#GNU General Public License as published by the Free Software +#Foundation; either version 2 of the License, or (at your option) any +#later version. + +use strict; +use vars qw( @ISA @EXPORT_OK ); +use Exporter; +use File::Temp; +use File::Slurp qw( slurp ); +#use FS::UID; + +@ISA = qw( Exporter ); +@EXPORT_OK = qw( eps2png ); + +################ Program parameters ################ + +# Some GhostScript programs can produce GIF directly. +# If not, we need the PBM package for the conversion. +# NOTE: This will be changed upon install. +my $use_pbm = 0; + +my $res = 82; # default resolution +my $scale = 1; # default scaling +my $mono = 0; # produce BW images if non-zero +my $format; # output format +my $gs_format; # GS output type +my $output; # output, defaults to STDOUT +my $antialias = 8; #4; # antialiasing +my $DEF_width; # desired widht +my $DEF_height; # desired height +#my $DEF_width = 90; # desired widht +#my $DEF_height = 36; # desired height + +my ($verbose,$trace,$test,$debug) = (0,0,0,1); +#handle_options (); +set_out_type ('png'); # unless defined $format; +warn "Producing $format ($gs_format) image.\n" if $verbose; + +$trace |= $test | $debug; +$verbose |= $trace; + +################ Presets ################ + +################ The Process ################ + +my $err = 0; + +sub eps2png { + my( $eps, %options ) = @_; #well, no options yet + + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + my $eps_file = new File::Temp( TEMPLATE => 'image.XXXXXXXX', + DIR => $dir, + SUFFIX => '.eps', + #UNLINK => 0, + ) or die "can't open temp file: $!\n"; + print $eps_file $eps; + close $eps_file; + + my @eps = split(/\r?\n/, $eps); + + warn "converting eps (". length($eps). " bytes, ". scalar(@eps). " lines)\n" + ;#if $verbose; + + my $line = shift @eps; #; + unless ( $eps =~ /^%!PS-Adobe.*EPSF-/ ) { + warn "not EPS file (no %!PS-Adobe header)\n"; + return; #empty png file? + } + + my $ps = ""; # PostScript input data + my $xscale; + my $yscale; + my $gotbb; + + # Prevent derived values from propagating. + my $width = $DEF_width; + my $height = $DEF_height; + + while ( @eps ) { + + $line = shift(@eps)."\n"; + + # Search for BoundingBox. + if ( $line =~ /^%%BoundingBox:\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/i ) { + $gotbb++; + warn "$eps_file: x0=$1, y0=$2, w=", $3-$1, ", h=", $4-$2 + if $verbose; + + if ( defined $width ) { + $res = 72; + $xscale = $width / ($3 - $1); + if ( defined $height ) { + $yscale = $height / ($4 - $2); + } + else { + $yscale = $xscale; + $height = ($4 - $2) * $yscale; + } + } + elsif ( defined $height ) { + $res = 72; + $yscale = $height / ($4 - $2); + if ( defined $width ) { + $xscale = $width / ($3 - $1); + } + else { + $xscale = $yscale; + $width = ($3 - $1) * $xscale; + } + } + unless ( defined $xscale ) { + $xscale = $yscale = $scale; + # Calculate actual width. + $width = $3 - $1; + $height = $4 - $2; + # Normal PostScript resolution is 72. + $width *= $res/72 * $xscale; + $height *= $res/72 * $yscale; + # Round up. + $width = int ($width + 0.5) + 1; + $height = int ($height + 0.5) + 1; + } + warn ", width=$width, height=$height\n" if $verbose; + + # Scale. + $ps .= "$xscale $yscale scale\n" + if $xscale != 1 || $yscale != 1; + + # Create PostScript code to translate coordinates. + $ps .= (0-$1) . " " . (0-$2) . " translate\n" + unless $1 == 0 && $2 == 0; + + # Include the image, show and quit. + $ps .= "($eps_file) run\n". + "showpage\n". + "quit\n"; + + last; + } + elsif ( $line =~ /^%%EndComments/i ) { + last; + } + } + + unless ( $gotbb ) { + warn "No bounding box in $eps_file\n"; + return; + } + + #it would be better to ask gs to spit out files on stdout, but c'est la vie + + #my $out_file; # output file + #my $pbm_file; # temporary file for PBM conversion + + my $out_file = new File::Temp( TEMPLATE => 'image.XXXXXXXX', + DIR => $dir, + SUFFIX => '.png', + #UNLINK => 0, + ) or die "can't open temp file: $!\n"; + + my $pbm_file = new File::Temp( TEMPLATE => 'image.XXXXXXXX', + DIR => $dir, + SUFFIX => '.pbm', + #UNLINK => 0, + ) or die "can't open temp file: $!\n"; + + # Note the temporary PBM file is created where the output file is + # located, since that will guarantee accessibility (and a valid + # filename). + warn "Creating $out_file\n" if $verbose; + + my $gs0 = "gs -q -dNOPAUSE -r$res -g${width}x$height"; + my $gs1 = "-"; + $gs0 .= " -dTextAlphaBits=$antialias -dGraphicsAlphaBits=$antialias" + if $antialias; + if ( $format eq 'png' ) { + mysystem ("$gs0 -sDEVICE=". ($mono ? "pngmono" : $gs_format). + " -sOutputFile=$out_file $gs1", $ps); + } + elsif ( $format eq 'jpg' ) { + mysystem ("$gs0 -sDEVICE=". ($mono ? "jpeggray" : $gs_format). + " -sOutputFile=$out_file $gs1", $ps); + } + elsif ( $format eq 'gif' ) { + if ( $use_pbm ) { + # Convert to PPM and use some of the PBM converters. + mysystem ("$gs0 -sDEVICE=". ($mono ? "pbm" : "ppm"). + " -sOutputFile=$pbm_file $gs1", $ps); + # mysystem ("pnmcrop $pbm_file | ppmtogif > $out_file"); + mysystem ("ppmtogif $pbm_file > $out_file"); + unlink ($pbm_file); + } + else { + # GhostScript has GIF drivers built-in. + mysystem ("$gs0 -sDEVICE=". ($mono ? "gifmono" : "gif8"). + " -sOutputFile=$out_file $gs1", $ps); + } + } + else { + warn "ASSERT ERROR: Unhandled output type: $format\n"; + exit (1); + } + +# unless ( -s $out_file ) { +# warn "Problem creating $out_file for $eps_file\n"; +# $err++; +# } + + slurp($out_file); + +} + +exit 1 if $err; + +################ Subroutines ################ + +sub mysystem { + my ($cmd, $data) = @_; + warn "+ $cmd\n" if $trace; + if ( $data ) { + if ( $trace ) { + my $dp = ">> " . $data; + $dp =~ s/\n(.)/\n>> $1/g; + warn "$dp"; + } + open (CMD, "|$cmd") or die ("$cmd: $!\n"); + print CMD $data; + close CMD or die ("$cmd close: $!\n"); + } + else { + system ($cmd); + } +} + +sub set_out_type { + my ($opt) = lc (shift (@_)); + if ( $opt =~ /^png(mono|gray|16|256|16m|alpha)?$/ ) { + $format = 'png'; + $gs_format = $format.(defined $1 ? $1 : '16m'); + } + elsif ( $opt =~ /^gif(mono)?$/ ) { + $format = 'gif'; + $gs_format = $format.(defined $1 ? $1 : ''); + } + elsif ( $opt =~ /^(jpg|jpeg)(gray)?$/ ) { + $format = 'jpg'; + $gs_format = 'jpeg'.(defined $2 ? $2 : ''); + } + else { + warn "ASSERT ERROR: Invalid value to set_out_type: $opt\n"; + exit (1); + } +} + +# 'antialias|aa=i' => \$antialias, +# 'noantialias|noaa' => sub { $antialias = 0 }, +# 'scale=f' => \$scale, +# 'width=i' => \$width, +# 'height=i' => \$height, +# 'resolution=i' => \$res, + +# die ("Antialias value must be 0, 1, 2, 4, or 8\n") + +# -width XXX desired with +# -height XXX desired height +# -resolution XXX resolution (default = $res) +# -scale XXX scaling factor +# -antialias XX antialias factor (must be 0, 1, 2, 4 or 8; default: 4) +# -noantialias no antialiasing (same as -antialias 0) + +1; -- cgit v1.2.1 From 455342c5657299275c14741ad459ff2ce4f03559 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 09:59:30 +0000 Subject: less debugging --- FS/FS/Misc/eps2png.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/Misc/eps2png.pm b/FS/FS/Misc/eps2png.pm index 49c1d5682..c175b3be8 100644 --- a/FS/FS/Misc/eps2png.pm +++ b/FS/FS/Misc/eps2png.pm @@ -37,7 +37,7 @@ my $DEF_height; # desired height #my $DEF_width = 90; # desired widht #my $DEF_height = 36; # desired height -my ($verbose,$trace,$test,$debug) = (0,0,0,1); +my ($verbose,$trace,$test,$debug) = (0,0,0,0); #handle_options (); set_out_type ('png'); # unless defined $format; warn "Producing $format ($gs_format) image.\n" if $verbose; @@ -66,7 +66,7 @@ sub eps2png { my @eps = split(/\r?\n/, $eps); warn "converting eps (". length($eps). " bytes, ". scalar(@eps). " lines)\n" - ;#if $verbose; + if $verbose; my $line = shift @eps; #; unless ( $eps =~ /^%!PS-Adobe.*EPSF-/ ) { -- cgit v1.2.1 From 4445165860112be241b1a51c50436ad0828a297a Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 12:01:25 +0000 Subject: agent-virt invoice_*{notes,footer,smallfooter,coupon}, RT#5025 --- FS/FS/Conf.pm | 6 ++++++ FS/FS/cust_bill.pm | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 8ea57304c..67ce56c6d 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -818,6 +818,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'billing', 'description' => 'Notes section for HTML invoices. Defaults to the same data in invoice_latexnotes if not specified.', 'type' => 'textarea', + 'per_agent' => 1, }, { @@ -825,6 +826,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'billing', 'description' => 'Footer for HTML invoices. Defaults to the same data in invoice_latexfooter if not specified.', 'type' => 'textarea', + 'per_agent' => 1, }, { @@ -846,6 +848,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'billing', 'description' => 'Notes section for LaTeX typeset PostScript invoices.', 'type' => 'textarea', + 'per_agent' => 1, }, { @@ -853,6 +856,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'billing', 'description' => 'Footer for LaTeX typeset PostScript invoices.', 'type' => 'textarea', + 'per_agent' => 1, }, { @@ -860,6 +864,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'billing', 'description' => 'Remittance coupon for LaTeX typeset PostScript invoices.', 'type' => 'textarea', + 'per_agent' => 1, }, { @@ -874,6 +879,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'billing', 'description' => 'Optional small footer for multi-page LaTeX typeset PostScript invoices.', 'type' => 'textarea', + 'per_agent' => 1, }, { diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index c8384c009..6fac0a946 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1990,15 +1990,18 @@ sub print_generic { $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); $invoice_data{'balance'} = sprintf("%.2f", $balance_due); + my $agentnum = $self->cust_main->agentnum; + #do variable substitution in notes, footer, smallfooter foreach my $include (qw( notes footer smallfooter coupon )) { my $inc_file = $conf->key_orbase("invoice_${format}$include", $template); my @inc_src; - if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) { + if ( $conf->exists($inc_file, $agentnum) + && length( $conf->config($inc_file, $agentnum) ) ) { - @inc_src = $conf->config($inc_file); + @inc_src = $conf->config($inc_file, $agentnum); } else { @@ -2010,7 +2013,7 @@ sub print_generic { s/--\@\]/$delimiters{$format}[1]/g; $_; } - &$convert_map( $conf->config($inc_file) ); + &$convert_map( $conf->config($inc_file, $agentnum) ); } -- cgit v1.2.1 From e20a3591dee00904c2def9cd68207d941eaf43b4 Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 17 Mar 2009 16:06:31 +0000 Subject: column upgrade for tax_rate RT#4903) --- FS/FS/Upgrade.pm | 3 +++ FS/FS/tax_rate.pm | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 97f24d403..5abfa6940 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -126,6 +126,9 @@ sub upgrade_data { #fixup access rights 'access_right' => [], + #change tax_rate column types + 'tax_rate' => [], + ; \%hash; diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 0d9156b43..3323e0060 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -3,11 +3,14 @@ package FS::tax_rate; use strict; use vars qw( @ISA $DEBUG $me %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities - %tax_passtypes ); + %tax_passtypes %GetInfoType ); use Date::Parse; use Storable qw( thaw ); use MIME::Base64; -use FS::Record qw( qsearch qsearchs dbh ); +use DBIx::DBSchema; +use DBIx::DBSchema::Table; +use DBIx::DBSchema::Column; +use FS::Record qw( qsearch qsearchs dbh dbdef ); use FS::tax_class; use FS::cust_bill_pkg; use FS::cust_tax_location; @@ -1063,6 +1066,67 @@ sub browse_queries { return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql"); } +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. +# +# + +sub _upgrade_data { # class method + my ($self, %opts) = @_; + my $dbh = dbh; + + warn "$me upgrading $self\n" if $DEBUG; + + my @column = qw ( tax excessrate usetax useexcessrate fee excessfee + feebase feemax ); + + if ( $dbh->{Driver}->{Name} eq 'Pg' ) { + + eval "use DBI::Const::GetInfoType;"; + die $@ if $@; + + my $major_version = 0; + $dbh->get_info( $GetInfoType{SQL_DBMS_VER} ) =~ /^(\d{2})/ + && ( $major_version = sprintf("%d", $1) ); + + if ( $major_version > 7 ) { + + # ideally this would be supported in DBIx-DBSchema and friends + + foreach my $column ( @column ) { + my $columndef = dbdef->table($self->table)->column($column); + unless ($columndef->type eq 'numeric') { + + warn "updating tax_rate column $column to numeric\n" if $DEBUG; + my $sql = "ALTER TABLE tax_rate ALTER $column TYPE numeric(14,8)"; + my $sth = $dbh->prepare($sql) or die $dbh->errstr; + $sth->execute or die $sth->errstr; + + warn "updating h_tax_rate column $column to numeric\n" if $DEBUG; + $sql = "ALTER TABLE h_tax_rate ALTER $column TYPE numeric(14,8)"; + $sth = $dbh->prepare($sql) or die $dbh->errstr; + $sth->execute or die $sth->errstr; + + } + } + + } else { + + warn "WARNING: tax_rate table upgrade unsupported for this Pg version\n"; + + } + + } else { + + warn "WARNING: tax_rate table upgrade only supported for Pg 8+\n"; + + } + + ''; + +} + =back =head1 BUGS -- cgit v1.2.1 From 968936c184fd0ba1d00ce5939569e7e0bbcb6c24 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 19:48:32 +0000 Subject: adding ClientAPI/SGNG.pm --- FS/FS/ClientAPI/SGNG.pm | 256 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 FS/FS/ClientAPI/SGNG.pm (limited to 'FS') diff --git a/FS/FS/ClientAPI/SGNG.pm b/FS/FS/ClientAPI/SGNG.pm new file mode 100644 index 000000000..6f74e23a0 --- /dev/null +++ b/FS/FS/ClientAPI/SGNG.pm @@ -0,0 +1,256 @@ +#this stuff is SG-specific (i.e. multi-customer company username hack) + +package FS::ClientAPI::SGNG; + +use strict; +use vars qw( $cache $DEBUG ); +use Time::Local qw(timelocal timelocal_nocheck); +use Business::CreditCard; +use FS::Record qw( qsearch qsearchs ); +use FS::cust_main; +use FS::cust_pkg; +use FS::ClientAPI::MyAccount; #qw( payment_info process_payment ) + +$DEBUG = 0; + +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', #yes, share session_ids + } ); +} + +#this might almost be general-purpose +sub decompify_pkgs { + 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" }; + + return { 'error' => 'Not a complimentary customer' } + unless $cust_main->payby eq 'COMP'; + + my $paydate = + $cust_main->paydate =~ /^\S+$/ ? $cust_main->paydate : '2037-12-31'; + + my ($payyear,$paymonth,$payday) = split (/-/,$paydate); + + my $date = timelocal(0,0,0,$payday,--$paymonth,$payyear); + + foreach my $cust_pkg ( + qsearch({ 'table' => 'cust_pkg', + 'hashref' => { 'custnum' => $custnum, + 'bill' => '', + }, + 'extra_sql' => ' AND '. FS::cust_pkg->active_sql, + }) + ) { + $cust_pkg->set('bill', $date); + my $error = $cust_pkg->replace; + return { 'error' => $error } if $error; + } + + return { 'error' => '' }; + +} + +#find old payment info +# (should work just like MyAccount::payment_info, except returns previous info +# too) +# definitly sg-specific, no one else stores past customer records like this +sub previous_payment_info { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $payment_info = FS::ClientAPI::MyAccount::payment_info($p); + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + #? + return $payment_info if $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/; + + foreach my $prev_cust_main ( + reverse _previous_cust_main( 'custnum' => $custnum, + 'username' => $cust_main->company, + 'with_payments' => 1, + ) + ) { + + next unless $prev_cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/; + + if ( $prev_cust_main->payby =~ /^(CARD|DCRD)$/ ) { + + #card expired? + my ($payyear,$paymonth,$payday) = split (/-/, $cust_main->paydate); + + my $expdate = timelocal_nocheck(0,0,0,1,$paymonth,$payyear); + + next if $expdate < time; + + } elsif ( $prev_cust_main->payby =~ /^(CHEK|DCHK)$/ ) { + + #any check? or just skip these in favor of cards? + + } + + return { %$payment_info, + #$prev_cust_main->payment_info + _cust_main_payment_info( $prev_cust_main ), + 'previous_custnum' => $prev_cust_main->custnum, + }; + + } + + #still nothing? return an error? + return $payment_info; + +} + +#this is really FS::cust_main::payment_info, but here for now +sub _cust_main_payment_info { + my $self = shift; + + my %return = (); + + $return{balance} = $self->balance; + + $return{payname} = $self->payname + || ( $self->first. ' '. $self->get('last') ); + + $return{$_} = $self->get($_) for qw(address1 address2 city state zip); + + $return{payby} = $self->payby; + $return{stateid_state} = $self->stateid_state; + + if ( $self->payby =~ /^(CARD|DCRD)$/ ) { + $return{card_type} = cardtype($self->payinfo); + $return{payinfo} = $self->paymask; + + @return{'month', 'year'} = $self->paydate_monthyear; + + } + + if ( $self->payby =~ /^(CHEK|DCHK)$/ ) { + my ($payinfo1, $payinfo2) = split '@', $self->paymask; + $return{payinfo1} = $payinfo1; + $return{payinfo2} = $payinfo2; + $return{paytype} = $self->paytype; + $return{paystate} = $self->paystate; + + } + + #doubleclick protection + my $_date = time; + $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; + + %return; + +} + +#find old cust_main records (with payments) +sub _previous_cust_main { + my %opt = @_; + my $custnum = $opt{'custnum'}; + my $username = $opt{'username'}; + + my %search = (); + if ( $opt{'with_payments'} ) { + $search{'extra_sql'} = + ' AND 0 < ( SELECT COUNT(*) FROM cust_pay + WHERE cust_pay.custnum = cust_main.custnum + ) + '; + } + + qsearch( { + 'table' => 'cust_main', + 'hashref' => { 'company' => { op => 'ILIKE', value => $opt{'username'} }, + 'custnum' => { op => '!=', value => $opt{'custnum'} }, + }, + 'order_by' => 'ORDER BY custnum', + %search, + } ); + +} + +#since we could be passing masked old CC data, need to look that up and +#replace it (like regular process_payment does) w/info from old customer record +sub previous_process_payment { + my $p = shift; + + return FS::ClientAPI::MyAccount::process_payment($p) + unless $p->{'previous_custnum'} + && ( ( $p->{'payby'} =~ /^(CARD|DCRD)$/ && $p->{'payinfo'} =~ /x/i ) + || ( $p->{'payby'} =~ /^(CHEK|DCHK)$/ && $p->{'payinfo1'} =~ /x/i ) + ); + + 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" }; + + #make sure this is really a previous custnum of this customer + my @previous_cust_main = + grep { $_->custnum == $p->{'previous_custnum'} } + _previous_cust_main( 'custnum' => $custnum, + 'username' => $cust_main->company, + 'with_payments' => 1, + ); + + my $previous_cust_main = $previous_cust_main[0]; + + #causes problems with old data w/old masking method + #if $previous_cust_main->paymask eq $payinfo; + + if ( $p->{'payby'} =~ /^(CHEK|DCHK)$/ && $p->{'payinfo1'} =~ /x/i ) { + ( $p->{'payinfo1'}, $p->{'payinfo2'} ) = + split('@', $previous_cust_main->payinfo); + } elsif ( $p->{'payby'} =~ /^(CARD|DCRD)$/ && $p->{'payinfo'} =~ /x/i ) { + $p->{'payinfo'} = $previous_cust_main->payinfo; + } + + FS::ClientAPI::MyAccount::process_payment($p); + +} + +sub previous_process_payment_order_pkg { + my $p = shift; + + my $hr = previous_process_payment($p); + return $hr if $hr->{'error'}; + + order_pkg($p); +} + +sub previous_process_payment_change_pkg { + my $p = shift; + + my $hr = previous_process_payment($p); + return $hr if $hr->{'error'}; + + change_pkg($p); +} + +sub previous_process_payment_order_renew { + my $p = shift; + + my $hr = previous_process_payment($p); + return $hr if $hr->{'error'}; + + order_renew($p); +} + +1; + -- cgit v1.2.1 From d8942379e744c35b50c3f43ef68ef78afbfad243 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 20:04:16 +0000 Subject: apacheip isn't actually deprecated yet --- FS/FS/Conf.pm | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 67ce56c6d..9197bb153 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -573,8 +573,11 @@ worry that config_items is freeside-specific and icky. { 'key' => 'apacheip', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, add an apache export instead. Used to be the current IP address to assign to new virtual hosts', + #not actually deprecated yet + #'section' => 'deprecated', + #'description' => 'DEPRECATED, add an apache export instead. Used to be the current IP address to assign to new virtual hosts', + 'section' => '', + 'description' => 'IP address to assign to new virtual hosts', 'type' => 'text', }, @@ -686,8 +689,11 @@ worry that config_items is freeside-specific and icky. { 'key' => 'deletecredits', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, now controlled by ACLs. Used to enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted.', + #not actually deprecated yet + #'section' => 'deprecated', + #'description' => 'DEPRECATED, now controlled by ACLs. Used to enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted.', + 'section' => '', + 'description' => 'One or more comma-separated email addresses to be notified when a credit is deleted.', 'type' => [qw( checkbox text )], }, -- cgit v1.2.1 From 82e3d70fea83fb776473e082097e2978226e3960 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 20:13:26 +0000 Subject: add a conf switch to enable sg multicust stuff, since it could be dangerous --- FS/FS/Conf.pm | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 9197bb153..f765fc721 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2705,6 +2705,13 @@ worry that config_items is freeside-specific and icky. 'type' => 'checkbox', }, + { + 'key' => 'sg-multicustomer_hack', + 'section' => '', + 'description' => "Don't use this.", + 'type' => 'checkbox', + }, + ); 1; -- cgit v1.2.1 From c72e127acc79a61703223e6c8b504abd234ca8b3 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 17 Mar 2009 21:38:39 +0000 Subject: add payment_info_renew_info method to ClientAPI/MyAccount and SG-equivalent previous_payment_info_renew_info to ClientAPI/SGNG --- FS/FS/ClientAPI/MyAccount.pm | 22 +++++++++++++++++----- FS/FS/ClientAPI/SGNG.pm | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index c6a4e0058..564acb1a6 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1162,11 +1162,14 @@ sub renew_info { $total += $_->part_pkg->base_recur; my $renew_date = $_->part_pkg->add_freq($_->bill); { - 'bill_date' => $_->bill, - 'bill_date_pretty' => time2str('%x', $_->bill), - 'renew_date' => $renew_date, - 'renew_date_pretty' => time2str('%x', $renew_date), - 'amount' => sprintf('%.2f', $total), + 'pkgnum' => $_->pkgnum, + 'amount' => sprintf('%.2f', $total), + 'bill_date' => $_->bill, + 'bill_date_pretty' => time2str('%x', $_->bill), + 'renew_date' => $renew_date, + 'renew_date_pretty' => time2str('%x', $renew_date), + 'expire_date' => $_->expire, + 'expire_date_pretty' => time2str('%x', $_->expire), }; } @cust_pkg; @@ -1175,6 +1178,15 @@ sub renew_info { } +sub payment_info_renew_info { + my $p = shift; + my $renew_info = renew_info($p); + my $payment_info = payment_info($p); + return { %$renew_info, + %$payment_info, + }; +} + sub order_renew { my $p = shift; diff --git a/FS/FS/ClientAPI/SGNG.pm b/FS/FS/ClientAPI/SGNG.pm index 6f74e23a0..872148abd 100644 --- a/FS/FS/ClientAPI/SGNG.pm +++ b/FS/FS/ClientAPI/SGNG.pm @@ -7,6 +7,7 @@ use vars qw( $cache $DEBUG ); use Time::Local qw(timelocal timelocal_nocheck); use Business::CreditCard; use FS::Record qw( qsearch qsearchs ); +use FS::Conf; use FS::cust_main; use FS::cust_pkg; use FS::ClientAPI::MyAccount; #qw( payment_info process_payment ) @@ -158,6 +159,10 @@ sub _cust_main_payment_info { #find old cust_main records (with payments) sub _previous_cust_main { + + #safety check! return nothing unless we're enabled explicitly + return () unless FS::Conf->new->exists('sg-multicustomer_hack'); + my %opt = @_; my $custnum = $opt{'custnum'}; my $username = $opt{'username'}; @@ -225,6 +230,15 @@ sub previous_process_payment { } +sub previous_payment_info_renew_info { + my $p = shift; + my $renew_info = renew_info($p); + my $payment_info = previous_payment_info($p); + return { %$renew_info, + %$payment_info, + }; +} + sub previous_process_payment_order_pkg { my $p = shift; -- cgit v1.2.1 From 037c3acd250f637182019d0a74361d5420f29c52 Mon Sep 17 00:00:00 2001 From: jeff Date: Wed, 18 Mar 2009 00:30:43 +0000 Subject: hide unused usage columns --- FS/FS/part_pkg/flat.pm | 11 ++++++++--- FS/FS/svc_acct.pm | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 3ac44c4e2..8fd028650 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -195,12 +195,17 @@ sub is_prepaid { 0; #no, we're postpaid } +sub usage_valuehash { + my $self = shift; + map { $_, $self->option($_) } + grep { $self->option($_, 'hush') } + qw(seconds upbytes downbytes totalbytes); +} + sub reset_usage { my($self, $cust_pkg, %opt) = @_; warn " resetting usage counters" if $opt{debug} > 1; - my %values = map { $_, $self->option($_) } - grep { $self->option($_, 'hush') } - qw(seconds upbytes downbytes totalbytes); + my %values = $self->usage_valuehash; if ($self->option('usage_rollover', 1)) { $cust_pkg->recharge(\%values); }else{ diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 6f1105148..57ef615f9 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -26,6 +26,7 @@ use FS::Conf; use FS::Record qw( qsearch qsearchs fields dbh dbdef ); use FS::Msgcat qw(gettext); use FS::UI::bytecount; +use FS::part_pkg; use FS::svc_Common; use FS::cust_svc; use FS::part_svc; @@ -277,6 +278,7 @@ sub table_info { type => 'text', disable_inventory => 1, disable_select => 1, + disable_part_svc_column => 1, }, 'upbytes' => { label => 'Upload', type => 'text', @@ -284,6 +286,7 @@ sub table_info { disable_select => 1, 'format' => \&FS::UI::bytecount::display_bytecount, 'parse' => \&FS::UI::bytecount::parse_bytecount, + disable_part_svc_column => 1, }, 'downbytes' => { label => 'Download', type => 'text', @@ -291,6 +294,7 @@ sub table_info { disable_select => 1, 'format' => \&FS::UI::bytecount::display_bytecount, 'parse' => \&FS::UI::bytecount::parse_bytecount, + disable_part_svc_column => 1, }, 'totalbytes'=> { label => 'Total up and download', type => 'text', @@ -298,11 +302,13 @@ sub table_info { disable_select => 1, 'format' => \&FS::UI::bytecount::display_bytecount, 'parse' => \&FS::UI::bytecount::parse_bytecount, + disable_part_svc_column => 1, }, 'seconds_threshold' => { label => 'Seconds threshold', type => 'text', disable_inventory => 1, disable_select => 1, + disable_part_svc_column => 1, }, 'upbytes_threshold' => { label => 'Upload threshold', type => 'text', @@ -310,6 +316,7 @@ sub table_info { disable_select => 1, 'format' => \&FS::UI::bytecount::display_bytecount, 'parse' => \&FS::UI::bytecount::parse_bytecount, + disable_part_svc_column => 1, }, 'downbytes_threshold' => { label => 'Download threshold', type => 'text', @@ -317,6 +324,7 @@ sub table_info { disable_select => 1, 'format' => \&FS::UI::bytecount::display_bytecount, 'parse' => \&FS::UI::bytecount::parse_bytecount, + disable_part_svc_column => 1, }, 'totalbytes_threshold'=> { label => 'Total up and download threshold', type => 'text', @@ -324,6 +332,7 @@ sub table_info { disable_select => 1, 'format' => \&FS::UI::bytecount::display_bytecount, 'parse' => \&FS::UI::bytecount::parse_bytecount, + disable_part_svc_column => 1, }, 'last_login'=> { label => 'Last login', @@ -503,6 +512,26 @@ sub insert { $self->svcpart($cust_svc->svcpart); } + # set usage fields and thresholds if unset but set in a package def + if ( $self->pkgnum ) { + my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } ); + my $part_pkg = $cust_pkg->part_pkg if $cust_pkg; + if ( $part_pkg && $part_pkg->can('usage_valuehash') ) { + + my %values = $part_pkg->usage_valuehash; + my $multiplier = $conf->exists('svc_acct-usage_threshold') + ? 1 - $conf->config('svc_acct-usage_threshold')/100 + : 0.20; + + foreach ( keys %values ) { + next if $self->getfield($_); + $self->setfield( $_, $values{$_} ); + $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) ); + } + + } + } + my @jobnums; $error = $self->SUPER::insert( 'jobnums' => \@jobnums, -- cgit v1.2.1 From b8a433b04fc02ae5b246be8f05a0d3b4f8b79d18 Mon Sep 17 00:00:00 2001 From: jeff Date: Wed, 18 Mar 2009 15:11:03 +0000 Subject: more error information --- FS/FS/part_pkg_taxrate.pm | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_pkg_taxrate.pm b/FS/FS/part_pkg_taxrate.pm index 6d1414a18..197bf0fc4 100644 --- a/FS/FS/part_pkg_taxrate.pm +++ b/FS/FS/part_pkg_taxrate.pm @@ -295,10 +295,18 @@ sub batch_import { delete($hash->{actionflag}); my $part_pkg_taxrate = qsearchs('part_pkg_taxrate', $hash); - return "Can't find part_pkg_taxrate to delete: ". - #join(" ", map { "$_ => ". $hash->{$_} } @fields) - join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) ) - unless $part_pkg_taxrate; + unless ( $part_pkg_taxrate ) { + if ( $hash->{taxproductnum} ) { + my $taxproduct = + qsearchs( 'part_pkg_taxproduct', + { 'taxproductnum' => $hash->{taxproductnum} } + ); + $hash->{taxproductnum} .= ' ( '. $taxproduct->taxproduct. ' )' + if $taxproduct; + } + return "Can't find part_pkg_taxrate to delete: ". + join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) ); + } my $error = $part_pkg_taxrate->delete; return $error if $error; -- cgit v1.2.1 From 06366964387677bd819ff9603a25d4399d9ad84d Mon Sep 17 00:00:00 2001 From: ivan Date: Sat, 21 Mar 2009 22:14:37 +0000 Subject: freeside-check local monitoring, RT#4610 --- FS/FS/Cron/check.pm | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++ FS/bin/freeside-check | 24 ++++++++++ 2 files changed, 147 insertions(+) create mode 100644 FS/FS/Cron/check.pm create mode 100644 FS/bin/freeside-check (limited to 'FS') diff --git a/FS/FS/Cron/check.pm b/FS/FS/Cron/check.pm new file mode 100644 index 000000000..39dd168b4 --- /dev/null +++ b/FS/FS/Cron/check.pm @@ -0,0 +1,123 @@ +package FS::Cron::check; + +use strict; +use vars qw( @ISA @EXPORT_OK $DEBUG $FS_RUN $error_msg + $SELFSERVICE_USER $SELFSERVICE_MACHINES @SELFSERVICE_MACHINES + ); +use Exporter; +use LWP::UserAgent; +use FS::Conf; +use FS::Record qw(qsearch); +use FS::cust_pay_pending; + +@ISA = qw( Exporter ); +@EXPORT_OK = qw( + check_queued check_selfservice check_apache check_bop_failures + alert error_msg +); + +$DEBUG = 0; + +$FS_RUN = '/var/run'; + +sub check_queued { + _check_fsproc('queued'); +} + +$SELFSERVICE_USER = '%%%SELFSERVICE_USER%%%'; + +$SELFSERVICE_MACHINES = '%%%SELFSERVICE_MACHINES%%%'; #substituted by Makefile +$SELFSERVICE_MACHINES =~ s/^\s+//; +$SELFSERVICE_MACHINES =~ s/\s+$//; +@SELFSERVICE_MACHINES = split(/\s+/, $SELFSERVICE_MACHINES); +@SELFSERVICE_MACHINES = () + if scalar(@SELFSERVICE_MACHINES) == 1 + && $SELFSERVICE_MACHINES[0] eq '%%%'.'SELFSERVICE_MACHINES'.'%%%'; + +sub check_selfservice { + foreach my $machine ( @SELFSERVICE_MACHINES ) { + unless ( _check_fsproc("selfservice-server.$SELFSERVICE_USER.$machine") ) { + $error_msg = "Self-service daemon not running for $machine"; + return 0; + } + } + return 1; +} + +sub _check_fsproc { + my $arg = shift; + _check_pidfile( "freeside-$arg.pid" ); +} + +sub _check_pidfile { + my $pidfile = shift; + open(PID, "$FS_RUN/$pidfile") or return 0; + chomp( my $pid = scalar() ); + close PID; # or return 0; + + $pid && kill 0, $pid; +} + +sub check_apache { + my $ua = new LWP::UserAgent; + $ua->agent("FreesideCronCheck/0.1 " . $ua->agent); + + my $req = new HTTP::Request GET => 'https://localhost/'; + my $res = $ua->request($req); + + return 1 if $res->is_success; + $error_msg = $res->status_line; + return 0; + +} + +#and now for something entirely different... +my $num_consecutive_bop_failures = 10; +sub check_bop_failures { + + return 1 if grep { $_->statustext eq 'captured' } + qsearch({ + 'table' => 'cust_pay_pending', + 'hashref' => { 'status' => 'done' }, + 'order_by' => 'ORDER BY paypendingnum DESC'. + " LIMIT $num_consecutive_bop_failures", + }); + $error_msg = "Last $num_consecutive_bop_failures real-time payments failed"; + return 0; +} + +# + +sub error_msg { + $error_msg; +} + +sub alert { + my( $alert, @emails ) = @_; + + my $conf = new FS::Conf; + my $smtpmachine = $conf->config('smtpmachine'); + my $company_name = $conf->config('company_name'); + + foreach my $email (@emails) { + warn "warning $email about $alert\n" if $DEBUG; + + my $message = <<"__MESSAGE__"; +From: support\@freeside.biz +To: $email +Subject: FREESIDE ALERT for $company_name + +FREESIDE ALERT: $alert + +__MESSAGE__ + + my $sender = Email::Send->new({ mailer => 'SMTP' }); + $sender->mailer_args([ Host => $smtpmachine ]); + $sender->send($message); + + } + +} + +1; + diff --git a/FS/bin/freeside-check b/FS/bin/freeside-check new file mode 100644 index 000000000..09c9aa4f1 --- /dev/null +++ b/FS/bin/freeside-check @@ -0,0 +1,24 @@ +#!/usr/bin/perl -w + +use strict; +use Email::Send; +use FS::UID qw( adminsuidsetup ); +use FS::Cron::check qw( + check_queued check_selfservice check_apache check_bop_failures + alert error_msg +); + +my $user = shift or die &usage; +my @emails = @ARGV; +#die "no notification email given" unless @emails; + +eval { adminsuidsetup $user }; + +if ( $@ ) { alert("Database down: $@", @emails); exit; } + +check_queued or alert("Queue daemon not running", @emails); +check_selfservice or alert(error_msg(), @emails); +check_apache or alert("Apache not running: ". error_msg(), @emails); + +check_bop_failures or alert(error_msg(), @emails); + -- cgit v1.2.1 From b690d3b4c830c78eb9d3bbf87de399823eaac867 Mon Sep 17 00:00:00 2001 From: ivan Date: Sat, 21 Mar 2009 23:32:27 +0000 Subject: 403 forbidden is okay, at lest the server's up --- FS/FS/Cron/check.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/Cron/check.pm b/FS/FS/Cron/check.pm index 39dd168b4..9a415bd9e 100644 --- a/FS/FS/Cron/check.pm +++ b/FS/FS/Cron/check.pm @@ -65,7 +65,7 @@ sub check_apache { my $req = new HTTP::Request GET => 'https://localhost/'; my $res = $ua->request($req); - return 1 if $res->is_success; + return 1 if $res->is_success || $res->status_line =~ /^403/; $error_msg = $res->status_line; return 0; -- cgit v1.2.1 From dcd8225db25059c0f6fa0c743601eb8945f07692 Mon Sep 17 00:00:00 2001 From: ivan Date: Sat, 21 Mar 2009 23:37:14 +0000 Subject: Locale::SubCountry warnings clogging up cron output not useful --- FS/bin/freeside-check | 1 + 1 file changed, 1 insertion(+) (limited to 'FS') diff --git a/FS/bin/freeside-check b/FS/bin/freeside-check index 09c9aa4f1..f2d596b80 100644 --- a/FS/bin/freeside-check +++ b/FS/bin/freeside-check @@ -1,3 +1,4 @@ +#!/usr/bin/perl #!/usr/bin/perl -w use strict; -- cgit v1.2.1 From 8089deb0915306ba481def7118deb27fe51735ab Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 22 Mar 2009 03:33:40 +0000 Subject: 10 is too few, throwing false positives --- FS/FS/Cron/check.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/Cron/check.pm b/FS/FS/Cron/check.pm index 9a415bd9e..e23d62bb4 100644 --- a/FS/FS/Cron/check.pm +++ b/FS/FS/Cron/check.pm @@ -72,7 +72,7 @@ sub check_apache { } #and now for something entirely different... -my $num_consecutive_bop_failures = 10; +my $num_consecutive_bop_failures = 30; sub check_bop_failures { return 1 if grep { $_->statustext eq 'captured' } -- cgit v1.2.1 From 12af08e945ac5c1f593d2bf9cf2d6df09f35228c Mon Sep 17 00:00:00 2001 From: ivan Date: Mon, 23 Mar 2009 17:03:03 +0000 Subject: add name (svc_acct.finger) to bulk billing detail, RT#3519 --- FS/FS/part_pkg/bulk.pm | 2 +- FS/FS/svc_Common.pm | 5 +++++ FS/FS/svc_acct.pm | 17 ++++++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_pkg/bulk.pm b/FS/FS/part_pkg/bulk.pm index 63d344d6d..809559509 100644 --- a/FS/FS/part_pkg/bulk.pm +++ b/FS/FS/part_pkg/bulk.pm @@ -54,7 +54,7 @@ sub calc_recur { # END START foreach my $h_svc ( $cust_pkg->h_cust_svc( $$sdate, $last_bill ) ) { - my @label = $h_svc->label( $$sdate, $last_bill ); + my @label = $h_svc->label_long( $$sdate, $last_bill ); die "fatal: no historical label found, wtf?" unless scalar(@label); #? #my $svc_details = $label[0].': '. $label[1]. ': '; my $svc_details = $label[1]. ': '; diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index da1cfe135..869ab5831 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -151,6 +151,11 @@ sub label { $self->svcnum; } +sub label_long { + my $self = shift; + $self->label(@_); +} + =item check Checks the validity of fields in this record. diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 57ef615f9..fcd73ac3c 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -256,7 +256,7 @@ sub table_info { disable_inventory => 1, disable_select => 1, }, - 'finger' => 'Real name (GECOS)', + 'finger' => 'Real name', # (GECOS)', 'domsvc' => { label => 'Domain', #def_label => 'svcnum from svc_domain', @@ -446,8 +446,23 @@ sub label { $self->email(@_); } +=item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ] + +Returns a longer string label for this acccount ("Real Name " +if available, or "username@domain"). + +END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with +history records. + =cut +sub label_long { + my $self = shift; + ( $self->finger =~ /\S/ ) + ? $self->finger. ' <'.$self->label(@_).'>' + : $self->label(@_); +} + =item insert [ , OPTION => VALUE ... ] Adds this account to the database. If there is an error, returns the error, -- cgit v1.2.1 From 42682206b4ba30de01c82743042f5fb9d48a93ed Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 23 Mar 2009 23:33:04 +0000 Subject: more DTRT with usage on service transfer between packages and recharges RT #2884, #5040 + #4995 fallout --- FS/FS/cust_pkg.pm | 19 +++++++++++++++++-- FS/FS/part_pkg/flat.pm | 2 +- FS/FS/svc_acct.pm | 13 +++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 7c8656c09..f2c3cccf5 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1135,6 +1135,21 @@ sub change { return "Unable to transfer all services from package ". $self->pkgnum; } + #reset usage if changing pkgpart + if ($self->pkgpart != $cust_pkg->pkgpart) { + my $part_pkg = $cust_pkg->part_pkg; + $error = $part_pkg->reset_usage($cust_pkg, $part_pkg->is_prepaid + ? () + : ( 'null' => 1 ) + ) + if $part_pkg->can('reset_usage'); + + if ($error) { + $dbh->rollback if $oldAutoCommit; + return "Error setting usage values: $error"; + } + } + #Good to go, cancel old package. $error = $self->cancel( quiet=>1 ); if ($error) { @@ -2712,11 +2727,11 @@ All svc_accts which are part of this package have their values reset. =cut sub set_usage { - my ($self, $valueref) = @_; + my ($self, $valueref, %opt) = @_; foreach my $cust_svc ($self->cust_svc){ my $svc_x = $cust_svc->svc_x; - $svc_x->set_usage($valueref) + $svc_x->set_usage($valueref, %opt) if $svc_x->can("set_usage"); } } diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 8fd028650..009e54c78 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -209,7 +209,7 @@ sub reset_usage { if ($self->option('usage_rollover', 1)) { $cust_pkg->recharge(\%values); }else{ - $cust_pkg->set_usage(\%values); + $cust_pkg->set_usage(\%values, %opt); } } diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index fcd73ac3c..c6857e29a 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -1821,7 +1821,7 @@ sub _op_usage { } sub set_usage { - my( $self, $valueref ) = @_; + my( $self, $valueref, %options ) = @_; warn "$me set_usage called for svcnum ". $self->svcnum. ' ('. $self->email. "): ". @@ -1842,6 +1842,11 @@ sub set_usage { my $reset = 0; my %handyhash = (); + if ( $options{null} ) { + %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) } + qw( seconds upbytes downbytes totalbytes ) + ); + } foreach my $field (keys %$valueref){ $reset = 1 if $valueref->{$field}; $self->setfield($field, $valueref->{$field}); @@ -1860,8 +1865,8 @@ sub set_usage { #die $error if $error; #services not explicity changed via the UI my $sql = "UPDATE svc_acct SET " . - join (',', map { "$_ = ?" } (keys %handyhash) ). - " WHERE svcnum = ?"; + join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ). + " WHERE svcnum = ". $self->svcnum; warn "$me $sql\n" if $DEBUG; @@ -1869,7 +1874,7 @@ sub set_usage { if (scalar(keys %handyhash)) { my $sth = $dbh->prepare( $sql ) or die "Error preparing $sql: ". $dbh->errstr; - my $rv = $sth->execute((values %handyhash), $self->svcnum); + my $rv = $sth->execute(); die "Error executing $sql: ". $sth->errstr unless defined($rv); die "Can't update usage for svcnum ". $self->svcnum -- cgit v1.2.1 From 9006e983ebf98b2000a812ab01f99dcb2335534c Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 24 Mar 2009 02:36:31 +0000 Subject: bulk price plan: label as Name , supress extraneous service list, RT#3519 --- FS/FS/Record.pm | 4 ++-- FS/FS/cust_bill.pm | 22 +++++++++++++--------- FS/FS/cust_svc.pm | 21 ++++++++++++++++----- FS/FS/h_cust_svc.pm | 12 +++++++++--- FS/FS/part_pkg.pm | 3 +++ FS/FS/part_pkg/bulk.pm | 20 +++++++++++++------- 6 files changed, 56 insertions(+), 26 deletions(-) (limited to 'FS') diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 2d0263b22..0e8275b72 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -1944,7 +1944,7 @@ sub ut_money { =item ut_text COLUMN Check/untaint text. Alphanumerics, spaces, and the following punctuation -symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] +symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / = [ ] < > May not be null. If there is an error, returns the error, otherwise returns false. @@ -1956,7 +1956,7 @@ sub ut_text { #warn "notexist ". \¬exist. "\n"; #warn "AUTOLOAD ". \&AUTOLOAD. "\n"; $self->getfield($field) - =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]]+)$/ + =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>]+)$/ or return gettext('illegal_or_empty_text'). " $field: ". $self->getfield($field); $self->setfield($field,$1); diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 6fac0a946..77f0dd30f 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2832,8 +2832,10 @@ sub _items_cust_bill_pkg { my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; - my @d = map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date); + my @d = (); + push @d, map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date) + unless $cust_pkg->part_pkg->hide_svc_detail; push @d, $cust_bill_pkg->details(%details_opt) if $cust_bill_pkg->recur == 0; @@ -2862,16 +2864,18 @@ sub _items_cust_bill_pkg { " - ". time2str("%x", $cust_bill_pkg->edate). ")"; } + my @d = (); + #at least until cust_bill_pkg has "past" ranges in addition to #the "future" sdate/edate ones... see #3032 - my @d = (); push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date) - #$cust_bill_pkg->edate, - #$cust_bill_pkg->sdate), - ; - - @d = () if ($cust_bill_pkg->itemdesc || $is_summary); + $cust_pkg->h_labels_short($self->_date) + #$cust_bill_pkg->edate, + #$cust_bill_pkg->sdate) + unless $cust_pkg->part_pkg->hide_svc_detail + || $cust_bill_pkg->itemdesc + || $is_summary; + push @d, $cust_bill_pkg->details(%details_opt) unless ($is_summary || $type && $type eq 'R'); diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index 320f78aa0..c4a75f77a 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -375,23 +375,34 @@ Usage example: my($label, $value, $svcdb) = $cust_svc->label; +=item label_long + +Like the B