summaryrefslogtreecommitdiff
path: root/FS/FS
diff options
context:
space:
mode:
Diffstat (limited to 'FS/FS')
-rw-r--r--FS/FS/ClientAPI/Freeside.pm22
-rw-r--r--FS/FS/ClientAPI_XMLRPC.pm1
-rw-r--r--FS/FS/Conf.pm16
-rw-r--r--FS/FS/ConfDefaults.pm3
-rw-r--r--FS/FS/Cron/pay_batch.pm103
-rw-r--r--FS/FS/Misc.pm11
-rw-r--r--FS/FS/Setup.pm7
-rw-r--r--FS/FS/Template_Mixin.pm3
-rw-r--r--FS/FS/UI/Web.pm6
-rw-r--r--FS/FS/UI/Web/small_custview.pm25
-rw-r--r--FS/FS/contact.pm7
-rw-r--r--FS/FS/cust_main.pm81
-rw-r--r--FS/FS/cust_main/Billing.pm21
-rw-r--r--FS/FS/cust_pay.pm24
-rw-r--r--FS/FS/cust_pay_batch.pm1
-rw-r--r--FS/FS/cust_pkg.pm33
-rw-r--r--FS/FS/cust_pkg/Search.pm63
-rw-r--r--FS/FS/msg_template/email.pm15
-rw-r--r--FS/FS/part_export/broadband_sqlradius.pm2
-rw-r--r--FS/FS/part_export/sqlradius.pm14
-rw-r--r--FS/FS/part_pkg.pm69
-rw-r--r--FS/FS/part_pkg/voip_cdr.pm12
-rw-r--r--FS/FS/pay_batch.pm24
-rw-r--r--FS/FS/pay_batch/paymentech.pm9
-rw-r--r--FS/FS/payinfo_Mixin.pm5
-rw-r--r--FS/FS/webservice_log.pm34
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