diff options
Diffstat (limited to 'FS/FS')
-rw-r--r-- | FS/FS/ClientAPI/Freeside.pm | 22 | ||||
-rw-r--r-- | FS/FS/ClientAPI_XMLRPC.pm | 1 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 16 | ||||
-rw-r--r-- | FS/FS/ConfDefaults.pm | 3 | ||||
-rw-r--r-- | FS/FS/Cron/pay_batch.pm | 103 | ||||
-rw-r--r-- | FS/FS/Misc.pm | 11 | ||||
-rw-r--r-- | FS/FS/Setup.pm | 7 | ||||
-rw-r--r-- | FS/FS/Template_Mixin.pm | 3 | ||||
-rw-r--r-- | FS/FS/UI/Web.pm | 6 | ||||
-rw-r--r-- | FS/FS/UI/Web/small_custview.pm | 25 | ||||
-rw-r--r-- | FS/FS/contact.pm | 7 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 81 | ||||
-rw-r--r-- | FS/FS/cust_main/Billing.pm | 21 | ||||
-rw-r--r-- | FS/FS/cust_pay.pm | 24 | ||||
-rw-r--r-- | FS/FS/cust_pay_batch.pm | 1 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 33 | ||||
-rw-r--r-- | FS/FS/cust_pkg/Search.pm | 63 | ||||
-rw-r--r-- | FS/FS/msg_template/email.pm | 15 | ||||
-rw-r--r-- | FS/FS/part_export/broadband_sqlradius.pm | 2 | ||||
-rw-r--r-- | FS/FS/part_export/sqlradius.pm | 14 | ||||
-rw-r--r-- | FS/FS/part_pkg.pm | 69 | ||||
-rw-r--r-- | FS/FS/part_pkg/voip_cdr.pm | 12 | ||||
-rw-r--r-- | FS/FS/pay_batch.pm | 24 | ||||
-rw-r--r-- | FS/FS/pay_batch/paymentech.pm | 9 | ||||
-rw-r--r-- | FS/FS/payinfo_Mixin.pm | 5 | ||||
-rw-r--r-- | FS/FS/webservice_log.pm | 34 |
26 files changed, 482 insertions, 129 deletions
diff --git a/FS/FS/ClientAPI/Freeside.pm b/FS/FS/ClientAPI/Freeside.pm index 8aa61e6..42b9c42 100644 --- a/FS/FS/ClientAPI/Freeside.pm +++ b/FS/FS/ClientAPI/Freeside.pm @@ -44,16 +44,34 @@ sub freesideinc_service { return { 'error' => 'bad support-key' }; } + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + my $custnum = $cust_pkg->custnum; + + my $quantity = $packet->{'quantity'} || 1; + + #false laziness w/webservice_log.pm + my $color = 1.10; + my $page = 0.10; + #XXX check if some customers can use some API calls, rate-limiting, etc. # but for now, everybody can use everything + if ( $packet->{method} eq 'print' ) { + my $avail_credit = $cust_pkg->cust_main->credit_limit + - $color - $quantity * $page + - FS::webservice_log->price_print( + 'custnum' => $custnum, + ); + + return { 'error' => 'Over credit limit' } + if $avail_credit <= 0; + } #record it happened - my $custnum = $svc_acct->cust_svc->cust_pkg->custnum; my $webservice_log = new FS::webservice_log { 'custnum' => $custnum, 'svcnum' => $svc_acct->svcnum, 'method' => $packet->{'method'}, - 'quantity' => $packet->{'quantity'} || 1, + 'quantity' => $quantity, }; my $error = $webservice_log->insert; return { 'error' => $error } if $error; diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 3167aa0..e69a06e 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -181,6 +181,7 @@ sub ss2clientapi { 'reset_passwd' => 'MyAccount/reset_passwd', 'check_reset_passwd' => 'MyAccount/check_reset_passwd', 'process_reset_passwd' => 'MyAccount/process_reset_passwd', + 'validate_passwd' => 'MyAccount/validate_passwd', 'list_tickets' => 'MyAccount/list_tickets', 'create_ticket' => 'MyAccount/create_ticket', 'get_ticket' => 'MyAccount/get_ticket', diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index ec317ba..0d561a2 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -669,6 +669,7 @@ my %batch_gateway_options = ( ); map { $_->gatewaynum, $_->label } @gateways; }, + 'per_agent' => 1, ); my %invoice_mode_options = ( @@ -1565,6 +1566,14 @@ and customer address. Include units.', }, { + 'key' => 'invoice_omit_due_date', + 'section' => 'invoice_balances', + 'description' => 'Omit the "Please pay by (date)" from invoices.', + 'type' => 'checkbox', + 'per_agent' => 1, + }, + + { 'key' => 'invoice_sections', 'section' => 'invoicing', 'description' => 'Split invoice into sections and label according to package category when enabled.', @@ -2909,6 +2918,13 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-password_reset_hours', + 'section' => 'self-service', + 'description' => 'Numbers of hours an email password reset is valid. Defaults to 24.', + 'type' => 'text', + }, + + { 'key' => 'selfservice-password_reset_msgnum', 'section' => 'self-service', 'description' => 'Template to use for password reset emails.', diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index 2fa8344..2c24b13 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -33,6 +33,9 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Customer' => 'custnum | Status | Last, First or Company (Last, First)', + 'Agent | Agent Cust# or Cust# | Cust. Status | Customer' => + 'Agent | Agent Cust# | Status | Last, First or Company (Last, First)', + 'Customer | Day phone | Night phone | Mobile phone | Fax number' => 'Customer | (all phones)', 'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number' => diff --git a/FS/FS/Cron/pay_batch.pm b/FS/FS/Cron/pay_batch.pm index 9791749..1e110f2 100644 --- a/FS/FS/Cron/pay_batch.pm +++ b/FS/FS/Cron/pay_batch.pm @@ -22,6 +22,38 @@ $me = '[FS::Cron::pay_batch]'; # -r: Multi-process mode dry run option # -a: Only process customers with the specified agentnum +sub batch_gateways { + my $conf = FS::Conf->new; + # returns a list of arrayrefs: [ gateway, payby, agentnum ] + my %opt = @_; + my @agentnums; + if ( $conf->exists('batch-spoolagent') ) { + if ( $opt{a} ) { + @agentnums = split(',', $opt{a}); + } else { + @agentnums = map { $_->agentnum } qsearch('agent'); + } + } else { + @agentnums = (''); + if ( $opt{a} ) { + warn "Payment batch processing skipped in per-agent mode.\n" if $DEBUG; + return; + } + } + my @return; + foreach my $agentnum (@agentnums) { + my %gateways; + foreach my $payby ('CARD', 'CHEK') { + my $gatewaynum = $conf->config("batch-gateway-$payby", $agentnum); + next if !$gatewaynum; + my $gateway = FS::payment_gateway->by_key($gatewaynum) + or die "payment_gateway '$gatewaynum' not found\n"; + push @return, [ $gateway, $payby, $agentnum ]; + } + } + @return; +} + sub pay_batch_submit { my %opt = @_; local $DEBUG = ($opt{l} || 1) if $opt{v}; @@ -31,25 +63,14 @@ sub pay_batch_submit { my $dbh = dbh; warn "$me batch_submit\n" if $DEBUG; - my $conf = FS::Conf->new; - - # need to respect -a somehow, but for now none of this is per-agent - if ( $opt{a} ) { - warn "Payment batch processing skipped in per-agent mode.\n" if $DEBUG; - return; - } - my %gateways; - foreach my $payby ('CARD', 'CHEK') { - my $gatewaynum = $conf->config("batch-gateway-$payby"); - next if !$gatewaynum; - my $gateway = FS::payment_gateway->by_key($gatewaynum) - or die "payment_gateway '$gatewaynum' not found\n"; - + foreach my $config (batch_gateways(%opt)) { + my ($gateway, $payby, $agentnum) = @$config; if ( $gateway->batch_processor->can('default_transport') ) { - foreach my $pay_batch ( - qsearch('pay_batch', { status => 'O', payby => $payby }) - ) { + my $search = { status => 'O', payby => $payby }; + $search->{agentnum} = $agentnum if $agentnum; + + foreach my $pay_batch ( qsearch('pay_batch', $search) ) { warn "Exporting batch ".$pay_batch->batchnum."\n" if $DEBUG; eval { $pay_batch->export_to_gateway( $gateway, debug => $DEBUG ); }; @@ -80,38 +101,28 @@ sub pay_batch_receive { my $error; warn "$me batch_receive\n" if $DEBUG; - my $conf = FS::Conf->new; - # need to respect -a somehow, but for now none of this is per-agent - if ( $opt{a} ) { - warn "Payment batch processing skipped in per-agent mode.\n" if $DEBUG; - return; - } - my %gateways; - foreach my $payby ('CARD', 'CHEK') { - my $gatewaynum = $conf->config("batch-gateway-$payby"); - next if !$gatewaynum; - # If the same gateway is selected for both paybys, only import it once - $gateways{$gatewaynum} = FS::payment_gateway->by_key($gatewaynum); - if ( !$gateways{$gatewaynum} ) { + my %gateway_done; + # If a gateway is selected for more than one payby+agentnum, still + # only import from it once; we expect it will send back multiple + # result batches. + foreach my $config (batch_gateways(%opt)) { + my ($gateway, $payby, $agentnum) = @$config; + next if $gateway_done{$gateway->gatewaynum}; + next unless $gateway->batch_processor->can('default_transport'); + # already warned about this above + warn "Importing results from '".$gateway->label."'\n" if $DEBUG; + # Note that import_from_gateway is not agent-limited; if a gateway + # returns results for batches not associated with this agent, we will + # still accept them. Well-behaved gateways will not do that. + $error = eval { + FS::pay_batch->import_from_gateway( gateway =>$gateway, debug => $DEBUG ) + } || $@; + if ( $error ) { + # this we can roll back $dbh->rollback; - die "batch-gateway-$payby gateway $gatewaynum not found\n"; + die "error receiving from gateway '".$gateway->label."':\n$error\n"; } - } - - foreach my $gateway (values %gateways) { - if ( $gateway->batch_processor->can('default_transport') ) { - warn "Importing results from '".$gateway->label."'\n" if $DEBUG; - $error = eval { - FS::pay_batch->import_from_gateway( gateway =>$gateway, debug => $DEBUG ) - } || $@; - if ( $error ) { - # this we can roll back - $dbh->rollback; - die "error receiving from gateway '".$gateway->label."':\n$error\n"; - } - } - # else we already warned about it above } #$gateway # resolve batches if we can diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 9a43180..a2d1b3e 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -256,10 +256,17 @@ sub send_email { } push @to, $options{bcc} if defined($options{bcc}); + # fully unpack all addresses found in @to (including Bcc) to make the + # envelope list + my @env_to; + foreach my $dest (@to) { + push @env_to, map { $_->address } Email::Address->parse($dest); + } + local $@; # just in case eval { sendmail($message, { transport => $transport, from => $from, - to => \@to }) }; + to => \@env_to }) }; my $error = ''; if(ref($@) and $@->isa('Email::Sender::Failure')) { @@ -274,7 +281,7 @@ sub send_email { if ( $conf->exists('log_sent_mail') ) { my $cust_msg = FS::cust_msg->new({ 'env_from' => $options{'from'}, - 'env_to' => join(', ', @to), + 'env_to' => join(', ', @env_to), 'header' => $message->header_as_string, 'body' => $message->body_as_string, '_date' => $time, diff --git a/FS/FS/Setup.pm b/FS/FS/Setup.pm index 0c3226a..f005a36 100644 --- a/FS/FS/Setup.pm +++ b/FS/FS/Setup.pm @@ -7,7 +7,6 @@ use vars qw( @EXPORT_OK ); use Tie::IxHash; use Crypt::OpenSSL::RSA; use FS::UID qw( dbh driver_name ); -#use FS::Record; use FS::svc_domain; $FS::svc_domain::whois_hack = 1; @@ -99,6 +98,12 @@ sub enable_encryption { $conf->set('encryptionpublickey', $rsa->get_public_key_string ); $conf->set('encryptionprivatekey', $rsa->get_private_key_string ); + # reload Record globals, false laziness with FS::Record + $FS::Record::conf_encryption = $conf->exists('encryption'); + $FS::Record::conf_encryptionmodule = $conf->config('encryptionmodule'); + $FS::Record::conf_encryptionpublickey = join("\n",$conf->config('encryptionpublickey')); + $FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey')); + } sub enable_banned_pay_pad { diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index caa31f7..d7add71 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -1952,7 +1952,8 @@ sub balance_due_msg { # (yes, or if invoice_sections is enabled; this is just for compatibility) if ( $self->due_date ) { $msg .= ' - ' . $self->mt('Please pay by'). ' '. - $self->due_date2str('short'); + $self->due_date2str('short') + unless $self->conf->config_bool('invoice_omit_due_date'); } elsif ( $self->terms ) { $msg .= ' - '. $self->mt($self->terms); } diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index 04aeda1..6cc04b9 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -300,7 +300,7 @@ sub cust_header { my %header2method = ( 'Customer' => 'name', 'Cust. Status' => 'cust_status_label', - 'Cust#' => 'custnum', + 'Cust#' => 'display_custnum', 'Name' => 'contact', 'Company' => 'company', @@ -347,6 +347,8 @@ sub cust_header { # 'Payment Type' => 'cust_payby', 'Current Balance' => 'current_balance', 'Agent Cust#' => 'agent_custid', + 'Agent' => 'agent_name', + 'Agent Cust# or Cust#' => 'display_custnum', 'Advertising Source' => 'referral', ); $header2method{'Cust#'} = 'display_custnum' @@ -450,6 +452,8 @@ sub cust_sql_fields { } push @fields, 'agent_custid'; + push @fields, 'agentnum' if grep { $_ eq 'agent_name' } @cust_fields; + my @extra_fields = (); if (grep { $_ eq 'current_balance' } @cust_fields) { push @extra_fields, FS::cust_main->balance_sql . " AS current_balance"; diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm index e82e332..85bee7d 100644 --- a/FS/FS/UI/Web/small_custview.pm +++ b/FS/FS/UI/Web/small_custview.pm @@ -142,16 +142,25 @@ sub small_custview { } $html .= '</TD></TR><TR><TD></TD><TD BGCOLOR="#ffffff">'; - if ( $cust_main->daytime && $cust_main->night ) { - $html .= ( FS::Msgcat::_gettext('daytime') || 'Day' ). - ' '. $cust_main->daytime. - '<BR>'. ( FS::Msgcat::_gettext('night') || 'Night' ). - ' '. $cust_main->night; - } elsif ( $cust_main->daytime || $cust_main->night ) { - $html .= $cust_main->daytime || $cust_main->night; + + my $num_numbers = 0; + $num_numbers++ foreach grep $cust_main->$_(), qw( daytime night mobile ); + if ( $num_numbers > 1 ) { + $html .= ucfirst( FS::Msgcat::_gettext('daytime') ). + ' '. $cust_main->daytime. '<BR>' + if $cust_main->daytime; + $html .= ucfirst( FS::Msgcat::_gettext('night') ). + ' '. $cust_main->night. '<BR>' + if $cust_main->night; + $html .= ucfirst( FS::Msgcat::_gettext('mobile') ). + ' '. $cust_main->mobile. '<BR>' + if $cust_main->night; + } elsif ( $num_numbers ) { # == 1 ) { + $html .= ( $cust_main->daytime || $cust_main->night || $cust_main->mobile ). + '<BR>'; } if ( $cust_main->fax ) { - $html .= '<BR>Fax '. $cust_main->fax; + $html .= 'Fax '. $cust_main->fax; } $html .= '</TD></TR></TABLE></TD>'; diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index a8aa43b..e49f6df 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -873,7 +873,10 @@ sub send_reset_email { 'svcnum' => $opt{'svcnum'}, }; - my $timeout = '24 hours'; #? + + my $conf = new FS::Conf; + my $timeout = + ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours'; my $reset_session_id; do { @@ -885,8 +888,6 @@ sub send_reset_email { #email it - my $conf = new FS::Conf; - my $cust_main = ''; my @cust_contact = grep $_->selfservice_access, $self->cust_contact; $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 71552b0..4bd3f26 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -5784,13 +5784,94 @@ sub _upgrade_data { #class method $class->_upgrade_otaker(%opts); + # turn on encryption as part of regular upgrade, so all new records are immediately encrypted + # existing records will be encrypted in queueable_upgrade (below) + unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) { + eval "use FS::Setup"; + die $@ if $@; + FS::Setup::enable_encryption(); + } + } sub queueable_upgrade { my $class = shift; + + ### encryption gets turned on in _upgrade_data, above + + eval "use FS::upgrade_journal"; + die $@ if $@; + + # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted, + # clear that out before encrypting/tokenizing anything else + if (!FS::upgrade_journal->is_done('clear_payinfo_history')) { + foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') { + my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL'; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute or die $sth->errstr; + } + FS::upgrade_journal->set_done('clear_payinfo_history'); + } + + # encrypt old records + if ($conf->exists('encryption') && !FS::upgrade_journal->is_done('encryption_check')) { + + # allow replacement of closed cust_pay/cust_refund records + local $FS::payinfo_Mixin::allow_closed_replace = 1; + + # because it looks like nothing's changing + local $FS::Record::no_update_diff = 1; + + # commit everything immediately + local $FS::UID::AutoCommit = 1; + + # encrypt what's there + foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') { + my $tclass = 'FS::'.$table; + my $lastrecnum = 0; + my @recnums = (); + while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) { + my $record = $tclass->by_key($recnum); + next unless $record; # small chance it's been deleted, that's ok + next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby; + # window for possible conflict is practically nonexistant, + # but just in case... + $record = $record->select_for_update; + my $error = $record->replace; + die $error if $error; + } + } + + FS::upgrade_journal->set_done('encryption_check'); + } + + # now that everything's encrypted, tokenize... FS::cust_main::Billing_Realtime::token_check(@_); } +# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum +# cust_payby might get deleted while this runs +# not a method! +sub _upgrade_next_recnum { + my ($dbh,$table,$lastrecnum,$recnums) = @_; + my $recnum = shift @$recnums; + return $recnum if $recnum; + my $tclass = 'FS::'.$table; + my $sql = 'SELECT '.$tclass->primary_key. + ' FROM '.$table. + ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum. + ' ORDER BY '.$tclass->primary_key.' LIMIT 500';; + my $sth = $dbh->prepare($sql) or die $dbh->errstr; + $sth->execute() or die $sth->errstr; + my @recnums; + while (my $rec = $sth->fetchrow_hashref) { + push @$recnums, $rec->{$tclass->primary_key}; + } + $sth->finish(); + $$lastrecnum = $$recnums[-1]; + return shift @$recnums; +} + =back =head1 BUGS diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 4821ce5..6932647 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -544,14 +544,19 @@ sub bill { foreach my $part_pkg ( @part_pkg ) { - $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill ); + my $this_cust_pkg = $cust_pkg; + # for add-on packages, copy the object to avoid leaking changes back to + # the caller if pkg_list is in use; see RT#73607 + if ( $part_pkg->get('pkgpart') != $real_pkgpart ) { + $this_cust_pkg = FS::cust_pkg->new({ %hash }); + } my $pass = ''; - if ( $cust_pkg->separate_bill ) { + if ( $this_cust_pkg->separate_bill ) { # if no_auto is also set, that's fine. we just need to not have # invoices that are both auto and no_auto, and since the package # gets an invoice all to itself, it will only be one or the other. - $pass = $cust_pkg->pkgnum; + $pass = $this_cust_pkg->pkgnum; if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet push @passes, $pass; $total_setup{$pass} = do { my $z = 0; \$z }; @@ -565,17 +570,17 @@ sub bill { ); $cust_bill_pkg{$pass} = []; } - } elsif ( ($cust_pkg->no_auto || $part_pkg->no_auto) ) { + } elsif ( ($this_cust_pkg->no_auto || $part_pkg->no_auto) ) { $pass = 'no_auto'; } - my $next_bill = $cust_pkg->getfield('bill') || 0; + my $next_bill = $this_cust_pkg->getfield('bill') || 0; my $error; # let this run once if this is the last bill upon cancellation while ( $next_bill <= $cmp_time or $options{cancel} ) { $error = $self->_make_lines( 'part_pkg' => $part_pkg, - 'cust_pkg' => $cust_pkg, + 'cust_pkg' => $this_cust_pkg, 'precommit_hooks' => \@precommit_hooks, 'line_items' => $cust_bill_pkg{$pass}, 'setup' => $total_setup{$pass}, @@ -590,12 +595,12 @@ sub bill { last if $error; # or if we're not incrementing the bill date. - last if ($cust_pkg->getfield('bill') || 0) == $next_bill; + last if ($this_cust_pkg->getfield('bill') || 0) == $next_bill; # or if we're letting it run only once last if $options{cancel}; - $next_bill = $cust_pkg->getfield('bill') || 0; + $next_bill = $this_cust_pkg->getfield('bill') || 0; #stop if -o was passed to freeside-daily last if $options{'one_recur'}; diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index b15920b..64ce7ec 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -1216,6 +1216,30 @@ sub _upgrade_data { #class method ### $class->upgrade_set_cardtype; + # for batch payments, make sure paymask is set + do { + local $FS::payinfo_Mixin::allow_closed_replace = 1; + local $FS::payinfo_Mixin::ignore_masked_payinfo = 1; + + my $cursor = FS::Cursor->new({ + table => 'cust_pay', + extra_sql => ' WHERE paymask IS NULL AND payinfo IS NOT NULL + AND payby IN(\'CARD\', \'CHEK\') + AND batchnum IS NOT NULL', + }); + + # records from cursors for some reason don't decrypt payinfo, so + # call replace_old to fetch the record "normally" + while (my $cust_pay = $cursor->fetch) { + $cust_pay = $cust_pay->replace_old; + $cust_pay->set('paymask', $cust_pay->mask_payinfo); + my $error = $cust_pay->replace; + if ($error) { + die "$error (setting masked payinfo on payment#". $cust_pay->paynum. + ")\n" + } + } + }; } sub process_upgrade_paybatch { diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 8127c6a..d29c6d0 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -297,6 +297,7 @@ sub approve { 'custnum' => $new->custnum, 'payby' => $new->payby, 'payinfo' => $new->payinfo || $old->payinfo, + 'paymask' => $new->mask_payinfo, 'paid' => $new->paid, '_date' => $new->_date, 'usernum' => $new->usernum, diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index bcb5176..b491f91 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -38,6 +38,8 @@ use FS::sales; # for modify_charge use FS::cust_credit; +use Data::Dumper; + # temporary fix; remove this once (un)suspend admin notices are cleaned up use FS::Misc qw(send_email); @@ -3017,7 +3019,7 @@ sub modify_charge { $pkg_opt_modified = 1; } } - $pkg_opt_modified = 1 if (scalar(@old_additional) - 1) != $i; + $pkg_opt_modified = 1 if scalar(@old_additional) != $i; $pkg_opt{'additional_count'} = $i if $i > 0; my $old_classnum; @@ -3171,9 +3173,6 @@ sub modify_charge { ''; } - - -use Data::Dumper; sub process_bulk_cust_pkg { my $job = shift; my $param = shift; @@ -5582,6 +5581,32 @@ sub _upgrade_data { # class method my $error = $part_pkg_link->remove_linked; die $error if $error; } + + # RT#73607: canceling a package with billing addons sometimes changes its + # pkgpart. + # Find records where the last replace_new record for the package before it + # was canceled has a different pkgpart from the package itself. + my @cust_pkg = qsearch({ + 'table' => 'cust_pkg', + 'select' => 'cust_pkg.*, h_cust_pkg.pkgpart AS h_pkgpart', + 'addl_from' => ' JOIN ( + SELECT pkgnum, MAX(historynum) AS historynum FROM h_cust_pkg + WHERE cancel IS NULL + AND history_action = \'replace_new\' + GROUP BY pkgnum + ) AS last_history USING (pkgnum) + JOIN h_cust_pkg USING (historynum)', + 'extra_sql' => ' WHERE cust_pkg.cancel is not null + AND cust_pkg.pkgpart != h_cust_pkg.pkgpart' + }); + foreach my $cust_pkg ( @cust_pkg ) { + my $pkgnum = $cust_pkg->pkgnum; + warn "fixing pkgpart on canceled pkg#$pkgnum\n"; + $cust_pkg->set('pkgpart', $cust_pkg->h_pkgpart); + my $error = $cust_pkg->replace; + die $error if $error; + } + } =back diff --git a/FS/FS/cust_pkg/Search.pm b/FS/FS/cust_pkg/Search.pm index 89809de..311dbdb 100644 --- a/FS/FS/cust_pkg/Search.pm +++ b/FS/FS/cust_pkg/Search.pm @@ -281,7 +281,7 @@ sub search { } ### - # parse refnum (advertising source) + # parse (customer) refnum (advertising source) ### if ( exists($params->{'refnum'}) ) { @@ -292,7 +292,7 @@ sub search { @refnum = ( $params->{'refnum'} ); } my $in = join(',', grep /^\d+$/, @refnum); - push @where, "refnum IN($in)" if length $in; + push @where, "cust_main.refnum IN($in)" if length $in; } ### @@ -450,7 +450,8 @@ sub search { '' => {}, ); - if( exists($params->{'active'} ) ) { + if ( exists($params->{'active'} ) ) { + # This overrides all the other date-related fields, and includes packages # that were active at some time during the interval. It excludes: # - packages that were set up after the end of the interval @@ -464,40 +465,51 @@ sub search { "(cust_pkg.cancel IS NULL OR cust_pkg.cancel >= $beginning )", "(cust_pkg.susp IS NULL OR cust_pkg.susp >= $beginning )", "NOT (".FS::cust_pkg->onetime_sql . ")"; - } - else { + + } else { + my $exclude_change_from = 0; my $exclude_change_to = 0; foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) { - next unless exists($params->{$field}); + if ( $params->{$field.'_null'} ) { + + push @where, "cust_pkg.$field IS NULL"; + # this should surely be obsoleted by now: OR cust_pkg.$field == 0 ) - my($beginning, $ending) = @{$params->{$field}}; + } else { - next if $beginning == 0 && $ending == 4294967295; + next unless exists($params->{$field}); + + my($beginning, $ending) = @{$params->{$field}}; + + next if $beginning == 0 && $ending == 4294967295; + + push @where, + "cust_pkg.$field IS NOT NULL", + "cust_pkg.$field >= $beginning", + "cust_pkg.$field <= $ending"; + + $orderby ||= "ORDER BY cust_pkg.$field"; + + if ( $field eq 'setup' ) { + $exclude_change_from = 1; + } elsif ( $field eq 'cancel' ) { + $exclude_change_to = 1; + } elsif ( $field eq 'change_date' ) { + # if we are given setup and change_date ranges, and the setup date + # falls in _both_ ranges, then include the package whether it was + # a change or not + $exclude_change_from = 0; + } - push @where, - "cust_pkg.$field IS NOT NULL", - "cust_pkg.$field >= $beginning", - "cust_pkg.$field <= $ending"; - - $orderby ||= "ORDER BY cust_pkg.$field"; - - if ( $field eq 'setup' ) { - $exclude_change_from = 1; - } elsif ( $field eq 'cancel' ) { - $exclude_change_to = 1; - } elsif ( $field eq 'change_date' ) { - # if we are given setup and change_date ranges, and the setup date - # falls in _both_ ranges, then include the package whether it was - # a change or not - $exclude_change_from = 0; } + } if ($exclude_change_from) { - push @where, "change_pkgnum IS NULL"; + push @where, "cust_pkg.change_pkgnum IS NULL"; } if ($exclude_change_to) { # a join might be more efficient here @@ -506,6 +518,7 @@ sub search { WHERE cust_pkg.pkgnum = changed_to_pkg.change_pkgnum )"; } + } $orderby ||= 'ORDER BY bill'; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 5abbaca..63c860f 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -290,7 +290,7 @@ sub prepare { my @to; if ( exists($opt{'to'}) ) { - @to = split(/\s*,\s*/, $opt{'to'}); + @to = map { $_->format } Email::Address->parse($opt{'to'}); } elsif ( $cust_main ) { @@ -393,14 +393,17 @@ sub prepare { # effective To: address (not in headers) push @to, $self->bcc_addr if $self->bcc_addr; - my $env_to = join(', ', @to); + my @env_to; + foreach my $dest (@to) { + push @env_to, map { $_->address } Email::Address->parse($dest); + } my $cust_msg = FS::cust_msg->new({ 'custnum' => $cust_main ? $cust_main->custnum : '', 'msgnum' => $self->msgnum, '_date' => $time, 'env_from' => $env_from, - 'env_to' => $env_to, + 'env_to' => join(',', @env_to), 'header' => $message->header_as_string, 'body' => $message->body_as_string, 'error' => '', @@ -507,7 +510,9 @@ sub send_prepared { $domain = $1; } - my @to = split(/\s*,\s*/, $cust_msg->env_to); + # in principle should already be a list of bare addresses, but run it + # through Email::Address to make sure + my @env_to = map { $_->address } Email::Address->parse($cust_msg->env_to); my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), 'helo' => $domain ); @@ -533,7 +538,7 @@ sub send_prepared { eval { sendmail( $message, { transport => $transport, from => $cust_msg->env_from, - to => \@to }) + to => \@env_to }) }; my $error = ''; if(ref($@) and $@->isa('Email::Sender::Failure')) { diff --git a/FS/FS/part_export/broadband_sqlradius.pm b/FS/FS/part_export/broadband_sqlradius.pm index e58c641..2d6681e 100644 --- a/FS/FS/part_export/broadband_sqlradius.pm +++ b/FS/FS/part_export/broadband_sqlradius.pm @@ -133,6 +133,8 @@ sub radius_check_suspended { sub _export_suspend { my( $self, $svc_broadband ) = (shift, shift); + return '' if $self->option('skip_provisioning'); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index f0ef3fc..9e65e51 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -26,6 +26,10 @@ tie %options, 'Tie::IxHash', type => 'select', options => [qw( usergroup radusergroup ) ], }, + 'skip_provisioning' => { + type => 'checkbox', + label => 'Skip provisioning records to this database' + }, 'ignore_accounting' => { type => 'checkbox', label => 'Ignore accounting records from this database' @@ -154,6 +158,8 @@ sub radius_check { #override for other svcdb sub _export_insert { my($self, $svc_x) = (shift, shift); + return '' if $self->option('skip_provisioning'); + foreach my $table (qw(reply check)) { my $method = "radius_$table"; my %attrib = $self->$method($svc_x); @@ -179,6 +185,8 @@ sub _export_insert { sub _export_replace { my( $self, $new, $old ) = (shift, shift, shift); + return '' if $self->option('skip_provisioning'); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -289,6 +297,8 @@ sub _export_replace { sub _export_suspend { my( $self, $svc_acct ) = (shift, shift); + return '' if $self->option('skip_provisioning'); + my $new = $svc_acct->clone_suspended; local $SIG{HUP} = 'IGNORE'; @@ -360,6 +370,8 @@ sub _export_suspend { sub _export_unsuspend { my( $self, $svc_x ) = (shift, shift); + return '' if $self->option('skip_provisioning'); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -399,6 +411,8 @@ sub _export_unsuspend { sub _export_delete { my( $self, $svc_x ) = (shift, shift); + return '' if $self->option('skip_provisioning'); + my $jobnum = ''; my $usergroup = $self->option('usergroup') || 'usergroup'; diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 35f178e..bb8c6bc 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -401,19 +401,20 @@ I<bulk_skip>, I<provision_hold> and I<options> If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as values, the appropriate FS::pkg_svc records will be replaced. I<hidden_svc> -can be set to a hashref of svcparts and flag values ('Y' or '') to set the -'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be set -to a hashref of svcparts and flag values ('Y' or '') to set the respective field -in those records. +can be set to a hashref of svcparts and flag values ('Y' or '') to set the +'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be +set to a hashref of svcparts and flag values ('Y' or '') to set the +respective field in those records. -If I<primary_svc> is set to the svcpart of the primary service, the appropriate -FS::pkg_svc record will be updated. +If I<primary_svc> is set to the svcpart of the primary service, the +appropriate FS::pkg_svc record will be updated. -If I<options> is set to a hashref, the appropriate FS::part_pkg_option records -will be replaced. +If I<options> is set to a hashref, the appropriate FS::part_pkg_option +records will be replaced. If I<part_pkg_currency> is set to a hashref of options (with the keys as -option_CURRENCY), appropriate FS::part_pkg::currency records will be replaced. +option_CURRENCY), appropriate FS::part_pkg::currency records will be +replaced. =cut @@ -2345,6 +2346,56 @@ sub queueable_upgrade { die $error if $error; } } + + # remove custom flag from one-time charge packages that were accidentally + # flagged as custom + $search = FS::Cursor->new({ + 'table' => 'part_pkg', + 'hashref' => { 'freq' => '0', + 'custom' => 'Y', + 'family_pkgpart' => { op => '!=', value => '' }, + }, + 'addl_from' => ' JOIN + (select pkgpart from cust_pkg group by pkgpart having count(*) = 1) + AS singular_pkg USING (pkgpart)', + }); + my @fields = grep { $_ ne 'pkgpart' + and $_ ne 'custom' + and $_ ne 'disabled' } FS::part_pkg->fields; + PKGPART: while (my $part_pkg = $search->fetch) { + # can't merge the package back into its parent (too late for that) + # but we can remove the custom flag if it's not actually customized, + # i.e. nothing has been changed. + + my $family_pkgpart = $part_pkg->family_pkgpart; + next PKGPART if $family_pkgpart == $part_pkg->pkgpart; + my $parent_pkg = FS::part_pkg->by_key($family_pkgpart); + foreach my $field (@fields) { + if ($part_pkg->get($field) ne $parent_pkg->get($field)) { + next PKGPART; + } + } + # options have to be identical too + # but links, FCC options, discount plans, and usage packages can't be + # changed through the "modify charge" UI, so skip them + my %newopt = $part_pkg->options; + my %oldopt = $parent_pkg->options; + OPTION: foreach my $option (keys %newopt) { + if (delete $newopt{$option} ne delete $oldopt{$option}) { + next PKGPART; + } + } + if (keys(%newopt) or keys(%oldopt)) { + next PKGPART; + } + # okay, now replace it + warn "Removing custom flag from part_pkg#".$part_pkg->pkgpart."\n"; + $part_pkg->set('custom', ''); + my $error = $part_pkg->replace; + die $error if $error; + } # $search->fetch + + return; } =item curuser_pkgs_sql diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index df97286..d96c472 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -191,6 +191,9 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash', 'skip_dcontext' => { 'name' => 'Do not charge for CDRs where dcontext is set to any of these (comma-separated) values: ', }, + 'skip_dcontext_prefix' => { 'name' => 'Do not charge for CDRs where dcontext starts with: ', + }, + 'skip_dcontext_suffix' => { 'name' => 'Do not charge for CDRs where dcontext ends with: ', }, @@ -336,7 +339,8 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash', use_cdrtypenum ignore_cdrtypenum use_calltypenum ignore_calltypenum ignore_disposition disposition_in disposition_prefix - skip_dcontext skip_dcontext_suffix skip_dst_prefix + skip_dcontext skip_dcontext_prefix skip_dcontext_suffix + skip_dst_prefix skip_dstchannel_prefix skip_src_length_more noskip_src_length_accountcode_tollfree accountcode_tollfree_ratenum accountcode_tollfree_field @@ -608,6 +612,12 @@ sub check_chargable { if $self->option_cacheable('skip_dcontext') =~ /\S/ && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext')); + my $len_dcontext_prefix = + length($self->option_cacheable('skip_dcontext_prefix')); + return "dcontext starts with ". $self->option_cacheable('skip_dcontext_prefix') + if $len_dcontext_prefix + && substr($cdr->dcontext,0,$len_dcontext_prefix) eq $self->option_cacheable('skip_dcontext_prefix'); + my $len_suffix = length($self->option_cacheable('skip_dcontext_suffix')); return "dcontext ends with ". $self->option_cacheable('skip_dcontext_suffix') if $len_suffix diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index 1049751..4aeb331 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -14,6 +14,7 @@ use FS::Record qw( dbh qsearch qsearchs ); use FS::Conf; use FS::cust_pay; use FS::Log; +use Try::Tiny; =head1 NAME @@ -1086,16 +1087,21 @@ sub export_to_gateway { return ''; } - my $batch = Business::BatchPayment->create(Batch => - batch_id => $self->batchnum, - items => \@items - ); - $processor->submit($batch); + try { + my $batch = Business::BatchPayment->create(Batch => + batch_id => $self->batchnum, + items => \@items + ); + $processor->submit($batch); - if ($batch->processor_id) { - $self->set('processor_id',$batch->processor_id); - $self->replace; - } + if ($batch->processor_id) { + $self->set('processor_id',$batch->processor_id); + $self->replace; + } + } catch { + $dbh->rollback if $oldAutoCommit; + die $_; + }; $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; diff --git a/FS/FS/pay_batch/paymentech.pm b/FS/FS/pay_batch/paymentech.pm index 1282507..3cf3134 100644 --- a/FS/FS/pay_batch/paymentech.pm +++ b/FS/FS/pay_batch/paymentech.pm @@ -175,7 +175,14 @@ sub _upgrade_gateway { my $conf = FS::Conf->new; my @batchconfig = $conf->config('batchconfig-paymentech'); my %options; - @options{ qw(bin terminalID merchantID login password ) } = @batchconfig; + @options{ qw( + bin + terminalID + merchantID + login + password + with_recurringInd + ) } = @batchconfig; $options{'industryType'} = 'EC'; ( 'Paymentech', %options ); } diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index 3a51022..1c45720 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -307,13 +307,12 @@ sub payinfo_used { my $payinfo = shift || $self->payinfo; my %hash = ( 'custnum' => $self->custnum, - 'payby' => 'CARD', + 'payby' => $self->payby, ); return 1 if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } ) - || qsearch('cust_pay', - { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } ) + || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo } ) ; return 0; diff --git a/FS/FS/webservice_log.pm b/FS/FS/webservice_log.pm index 7e320c2..1dfabe6 100644 --- a/FS/FS/webservice_log.pm +++ b/FS/FS/webservice_log.pm @@ -125,6 +125,40 @@ sub check { =back +=head1 CLASS METHODS + +=over 4 + +=item price_print + +Calculates cost of printing unbilled print jobs for this customer. + +=cut + +sub price_print { + my( $class, %opt ) = @_; + +# $opt{'beginning'} ||= 0; +# $opt{'ending'} ||= 4294967295; + + #false laziness w/ClientAPI/Freeside.pm + my $color = 1.10; + my $page = 0.10; + + $class->scalar_sql(" + SELECT SUM( $color + quantity * $page ) + FROM webservice_log + WHERE custnum = $opt{custnum} + AND method = 'print' + AND status IS NULL + "); +# AND _date >= $opt{beginning} +# AND _date < $opt{ending} + +} + +=back + =head1 BUGS =head1 SEE ALSO |