71513: Card tokenization [project branch merge]
authorJonathan Prykop <jonathan@freeside.biz>
Wed, 28 Dec 2016 20:18:29 +0000 (14:18 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Wed, 28 Dec 2016 20:18:29 +0000 (14:18 -0600)
51 files changed:
FS/FS/ClientAPI/Freeside.pm
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Conf.pm
FS/FS/ConfDefaults.pm
FS/FS/Cron/pay_batch.pm
FS/FS/Misc.pm
FS/FS/Setup.pm
FS/FS/Template_Mixin.pm
FS/FS/UI/Web.pm
FS/FS/UI/Web/small_custview.pm
FS/FS/contact.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_batch.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg/Search.pm
FS/FS/msg_template/email.pm
FS/FS/part_export/broadband_sqlradius.pm
FS/FS/part_export/sqlradius.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/voip_cdr.pm
FS/FS/pay_batch.pm
FS/FS/pay_batch/paymentech.pm
FS/FS/payinfo_Mixin.pm
FS/FS/webservice_log.pm
FS/bin/freeside-upgrade
FS/t/suite/15-activate_encryption.t [new file with mode: 0755]
fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
httemplate/edit/agent_payment_gateway.html
httemplate/edit/cust_pkg_discount.html
httemplate/elements/tr-select-pkg-discount.html
httemplate/misc/bulk_change_pkg.cgi
httemplate/misc/bulk_pkg_increment_bill.cgi
httemplate/misc/process/bulk_change_pkg.cgi
httemplate/misc/process/bulk_pkg_increment_bill.cgi
httemplate/misc/process/cancel_pkg.html
httemplate/search/cust_credit_bill_pkg.html
httemplate/search/cust_msg.html
httemplate/search/cust_pkg.cgi
httemplate/search/elements/grouped-search/core
httemplate/search/elements/grouped-search/html
httemplate/search/report_cust_pkg.html
httemplate/view/cust_main/packages/package.html
httemplate/view/cust_msg.html
httemplate/view/directions.html
ng_selfservice/elements/add_password_validation.php [new file with mode: 0644]
ng_selfservice/images/error.png [new file with mode: 0644]
ng_selfservice/images/tick.png [new file with mode: 0644]
ng_selfservice/password.php
ng_selfservice/xmlrpc_validate_passwd.php [new file with mode: 0644]

index 8aa61e6..42b9c42 100644 (file)
@@ -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;
index 3167aa0..e69a06e 100644 (file)
@@ -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',
index ec317ba..0d561a2 100644 (file)
@@ -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.',
index 2fa8344..2c24b13 100644 (file)
@@ -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' =>
index 9791749..1e110f2 100644 (file)
@@ -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
index 9a43180..a2d1b3e 100644 (file)
@@ -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,
index 0c3226a..f005a36 100644 (file)
@@ -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 {
index caa31f7..d7add71 100644 (file)
@@ -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);
     }
index 04aeda1..6cc04b9 100644 (file)
@@ -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";
index e82e332..85bee7d 100644 (file)
@@ -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>';
index a8aa43b..e49f6df 100644 (file)
@@ -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;
index 71552b0..4bd3f26 100644 (file)
@@ -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
index 4821ce5..6932647 100644 (file)
@@ -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'};
index b15920b..64ce7ec 100644 (file)
@@ -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 {
index 8127c6a..d29c6d0 100644 (file)
@@ -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,
index bcb5176..b491f91 100644 (file)
@@ -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
index 89809de..311dbdb 100644 (file)
@@ -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';
index 5abbaca..63c860f 100644 (file)
@@ -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')) {
index e58c641..2d6681e 100644 (file)
@@ -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';
index f0ef3fc..9e65e51 100644 (file)
@@ -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';
index 35f178e..bb8c6bc 100644 (file)
@@ -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
index df97286..d96c472 100644 (file)
@@ -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
index 1049751..4aeb331 100644 (file)
@@ -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;
   '';
index 1282507..3cf3134 100644 (file)
@@ -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 );
 }
index 3a51022..1c45720 100644 (file)
@@ -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;
index 7e320c2..1dfabe6 100644 (file)
@@ -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
index b8a8fbd..1684408 100755 (executable)
@@ -473,11 +473,9 @@ Also performs other upgrade functions:
   [ -r ]: Skip sqlradius updates.  Useful for occassions where the sqlradius
           databases may be inaccessible.
 
-  [ -j ]: Run certain upgrades asychronously from the job queue.  Currently 
-          used only for the 2.x -> 3.x cust_location, cust_pay and part_pkg
-          upgrades.  This may cause odd behavior before the upgrade is
-          complete, so it's recommended only for very large cust_main, cust_pay
-          and/or part_pkg tables that take too long to upgrade.
+  [ -j ]: Run certain upgrades asychronously from the job queue.  Recommended
+          for very large cust_main or part_pkg tables that take too long to
+          upgrade.
 
   [ -a ]: Run schema changes in parallel (Pg only).  DBIx::DBSchema minimum 
           version 0.41 recommended.  Recommended only for large databases and
diff --git a/FS/t/suite/15-activate_encryption.t b/FS/t/suite/15-activate_encryption.t
new file mode 100755 (executable)
index 0000000..e5732f7
--- /dev/null
@@ -0,0 +1,106 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::Test;
+use Test::More tests => 13;
+use FS::Conf;
+use FS::UID qw( dbh );
+use DateTime;
+use FS::cust_main; # to load all other tables
+
+my $fs = FS::Test->new( user => 'admin' );
+my $conf = FS::Conf->new;
+my $err;
+my @tables = qw(cust_payby cust_pay_pending cust_pay cust_pay_void cust_refund);
+
+### can only run on test database (company name "Freeside Test")
+like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT('');
+
+### we need to unencrypt our test db before we can test turning it on
+
+# temporarily load all payinfo into memory
+my %payinfo = ();
+foreach my $table (@tables) {
+  $payinfo{$table} = {};
+  foreach my $record ($fs->qsearch({ table => $table })) {
+    next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+    $payinfo{$table}{$record->get($record->primary_key)} = $record->get('payinfo');
+  }
+}
+
+# turn off encryption
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+  $conf->delete($config);
+  ok( !$conf->exists($config), "deleted $config" ) or BAIL_OUT('');
+}
+$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'));
+
+# save unencrypted values
+foreach my $table (@tables) {
+  local $FS::payinfo_Mixin::allow_closed_replace = 1;
+  local $FS::Record::no_update_diff = 1;
+  local $FS::UID::AutoCommit = 1;
+  my $tclass = 'FS::'.$table;
+  foreach my $key (keys %{$payinfo{$table}}) {
+    my $record = $tclass->by_key($key);
+    $record->payinfo($payinfo{$table}{$key});
+    $err = $record->replace;
+    last if $err;
+  }
+}
+ok( !$err, "save unencrypted values" ) or BAIL_OUT($err);
+
+# make sure it worked
+CHECKDECRYPT:
+foreach my $table (@tables) {
+  my $tclass = 'FS::'.$table;
+  foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+    my $sql = 'SELECT * FROM '.$table.
+              ' WHERE payinfo LIKE \'M%\''.
+              ' AND char_length(payinfo) > 80'.
+              ' AND '.$tclass->primary_key.' = '.$key;
+    my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+    $sth->execute or BAIL_OUT($sth->errstr);
+    if (my $hashrec = $sth->fetchrow_hashref) {
+      $err = $table.' '.$key.' encrypted';
+      last CHECKDECRYPT;
+    }
+  }
+}
+ok( !$err, "all values unencrypted" ) or BAIL_OUT($err);
+
+### now, run upgrade
+$err = system('freeside-upgrade','admin');
+ok( !$err, 'upgrade ran' ) or BAIL_OUT('Error string: '.$!);
+
+# check that confs got set
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+  ok( $conf->exists($config), "$config was set" ) or BAIL_OUT('');
+}
+
+# check that known records got encrypted
+CHECKENCRYPT:
+foreach my $table (@tables) {
+  my $tclass = 'FS::'.$table;
+  foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+    my $sql = 'SELECT * FROM '.$table.
+              ' WHERE payinfo LIKE \'M%\''.
+              ' AND char_length(payinfo) > 80'.
+              ' AND '.$tclass->primary_key.' = '.$key;
+    my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+    $sth->execute or BAIL_OUT($sth->errstr);
+    unless ($sth->fetchrow_hashref) {
+      $err = $table.' '.$key.' not encrypted';
+      last CHECKENCRYPT;
+    }
+  }
+}
+ok( !$err, "all values encrypted" ) or BAIL_OUT($err);
+
+exit;
+
+1;
+
index f396682..40eed80 100644 (file)
 <TABLE WIDTH="100%">
   <TR>
     <TD WIDTH="50%">
-<%= if ($previous < $beginning) {
-    $OUT .= qq!<A HREF="${url}view_cdr_details;svcnum=$svcnum;beginning=!;
-    $OUT .= qq!$previous;ending=$beginning">Previous period</A>!;
-    }else{
+<%=
+    $ahref = qq!<A HREF="${url}view_cdr_details;svcnum=$svcnum;!;
+    $ahref = qq!inbound=1;! if $inbound;
+    if ($previous < $beginning) {
+      $OUT .= $ahref.
+              qq!beginning=$previous;ending=$beginning">Previous period</A>!;
+    } else {
       '';
-    } %>
+    }
+%>
     </TD>
     <TD  WIDTH="50%" ALIGN="right">
-<%= if ($next > $ending) {
-    $OUT .= qq!<A HREF="${url}view_cdr_details;svcnum=$svcnum;beginning=!;
-    $OUT .= qq!$ending;ending=$next">Next period</A>!;
-    }else{
+<%= 
+    if ($next > $ending) {
+      $OUT .= $ahref. qq!beginning=$ending;ending=$next">Next period</A>!;
+    } else {
       '';
-    }%>
+    }
+%>
     </TD>
   </TR>
 </TABLE>
index 753bc76..6d15164 100644 (file)
@@ -12,6 +12,8 @@ Use gateway <SELECT NAME="gatewaynum">
 % foreach my $payment_gateway (
 %      qsearch('payment_gateway', { 'disabled' => '' } )
 %    ) {
+%   # don't let these be selected as agent overrides; there's a different mechanism
+%   next if $payment_gateway->gateway_namespace eq 'Business::BatchPayment';
 %
 
   <OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>)
index e1e3dae..79c3478 100755 (executable)
@@ -1,13 +1,17 @@
 <& /elements/header-popup.html, "Discount Package" &>
 <& /elements/error.html &>
 
-<FORM NAME="DiscountPkgForm" ACTION="<% $p %>edit/process/cust_pkg_discount.html" METHOD=POST>
+<FORM NAME     = "DiscountPkgForm"
+      ACTION   = "<% $p %>edit/process/cust_pkg_discount.html"
+      METHOD   = POST
+      onSubmit = "document.DiscountPkgForm.submit.disabled=true;"
+>
 <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
 
 <% ntable('#cccccc') %>
 
   <TR>
-    <TH ALIGN="right">Current package&nbsp;</TH>
+    <TH ALIGN="right">Package&nbsp;</TH>
     <TD COLSPAN=7>
       <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
     </TD>
@@ -18,6 +22,8 @@
   curr_value_recur  => $recur_discountnum,
   disable_setup     => $disable_setup,
   disable_recur     => $disable_recur,
+  setup_label       => emt('Setup fee discount'),
+  recur_label       => emt('Recurring fee discount'),
 &>
   
 </TABLE>
index dc38cff..0c57fd8 100644 (file)
@@ -34,7 +34,7 @@ description if curr_value_setup is set. Likewise "disable_recur".
 %   if ( $curuser->access_right('Waive setup fee') ) {
 %     push @$pre_options, -2 => 'Waive setup fee';
 %   }
-<& tr-td-label.html, label => emt('Setup fee') &>
+<& tr-td-label.html, label => $opt{setup_label} || emt('Setup fee') &>
   <td>
     <& select-discount.html,
       field       => 'setup_discountnum',
@@ -97,7 +97,7 @@ description if curr_value_setup is set. Likewise "disable_recur".
 % if ( $curuser->access_right('Discount customer package')
 %      and !$opt{disable_recur} ) {
 
-<& tr-td-label.html, label => emt('Recurring fee') &>
+<& tr-td-label.html, label => $opt{recur_label} || emt('Recurring fee') &>
   <td>
     <& select-discount.html,
       field       => 'recur_discountnum',
index 4964e59..6ed272f 100755 (executable)
 %# some false laziness w/search/cust_pkg.cgi
 
 <INPUT TYPE="hidden" NAME="query" VALUE="<% $cgi->keywords |h %>">
-%  for my $param (qw(agentnum custnum magic status classnum custom censustract)) {
-<INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
-%  }
+% for my $param (
+%   qw(
+%     agentnum cust_status cust_main_salesnum salesnum custnum magic status
+%     custom pkgbatch zip
+%     477part 477rownum date
+%     report_option
+%   ),
+%   grep { /^location_\w+$/ || /^report_option_any/ } $cgi->param
+% ) {
+  <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
+%
+% for my $param (qw( censustract censustract2 ) ) {
+%   next unless grep { $_ eq $param } $cgi->param;
+  <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
 %
-% foreach my $pkgpart ($cgi->param('pkgpart')) {
-<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart |h %>">
+% for my $param (qw( pkgpart classnum refnum towernum )) {
+%   foreach my $value ($cgi->param($param)) {
+      <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $value |h %>">
+%   }
 % }
 %
-% foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+% foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
 % 
+  <INPUT TYPE="hidden" NAME="<% $field %>_null" VALUE="<% $cgi->param("${field}_null") |h %>">
   <INPUT TYPE="hidden" NAME="<% $field %>begin" VALUE="<% $cgi->param("${field}.begin") |h %>">
   <INPUT TYPE="hidden" NAME="<% $field %>beginning" VALUE="<% $cgi->param("${field}beginning") |h %>">
   <INPUT TYPE="hidden" NAME="<% $field %>end" VALUE="<% $cgi->param("${field}.end") |h %>">
index d594b55..fc9bbc8 100755 (executable)
 %# some false laziness w/search/cust_pkg.cgi
 
 <INPUT TYPE="hidden" NAME="query" VALUE="<% $cgi->keywords |h %>">
-%  for my $param (qw(agentnum custnum magic status classnum custom censustract)) {
-<INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
-%  }
+% for my $param (
+%   qw(
+%     agentnum cust_status cust_main_salesnum salesnum custnum magic status
+%     custom pkgbatch zip
+%     477part 477rownum date
+%     report_option
+%   ),
+%   grep { /^location_\w+$/ || /^report_option_any/ } $cgi->param
+% ) {
+  <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
+%
+% for my $param (qw( censustract censustract2 ) ) {
+%    next unless grep { $_ eq $param } $cgi->param;
+  <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
 %
-% foreach my $pkgpart ($cgi->param('pkgpart')) {
-<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart |h %>">
+% for my $param (qw( pkgpart classnum refnum towernum )) {
+%   foreach my $value ($cgi->param($param)) {
+      <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $value |h %>">
+%   }
 % }
 %
-% foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+% foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
 % 
+  <INPUT TYPE="hidden" NAME="<% $field %>_null" VALUE="<% $cgi->param("${field}_null") |h %>">
   <INPUT TYPE="hidden" NAME="<% $field %>begin" VALUE="<% $cgi->param("${field}.begin") |h %>">
   <INPUT TYPE="hidden" NAME="<% $field %>beginning" VALUE="<% $cgi->param("${field}beginning") |h %>">
   <INPUT TYPE="hidden" NAME="<% $field %>end" VALUE="<% $cgi->param("${field}.end") |h %>">
index e22dafe..2432f3c 100755 (executable)
@@ -11,16 +11,43 @@ my %search_hash = ();
 
 $search_hash{'query'} = $cgi->param('query');
 
-for my $param (qw(agentnum magic status classnum pkgpart)) {
-  $search_hash{$param} = $cgi->param($param)
-    if $cgi->param($param);
+#scalars
+for (qw( agentnum cust_status cust_main_salesnum salesnum custnum magic status
+         custom cust_fields pkgbatch zip
+         477part 477rownum date 
+    )) 
+{
+  $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
+}
+
+#arrays
+for my $param (qw( pkgpart classnum refnum towernum )) {
+  $search_hash{$param} = [ $cgi->param($param) ]
+    if grep { $_ eq $param } $cgi->param;
+}
+
+#scalars that need to be passed if empty
+for my $param (qw( censustract censustract2 )) {
+  $search_hash{$param} = $cgi->param($param) || ''
+    if grep { $_ eq $param } $cgi->param;
+}
+
+#location flags (checkboxes)
+my @loc = grep /^\w+$/, $cgi->param('loc');
+$search_hash{"location_$_"} = 1 foreach @loc;
+
+my $report_option = $cgi->param('report_option');
+$search_hash{report_option} = $report_option if $report_option;
+
+for my $param (grep /^report_option_any/, $cgi->param) {
+  $search_hash{$param} = $cgi->param($param);
 }
 
 ###
 # parse dates
 ###
 
-#false laziness w/report_cust_pkg.html
+#false laziness w/report_cust_pkg.html and bulk_pkg_increment_bill.cgi
 my %disable = (
   'all'             => {},
   'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
@@ -30,7 +57,9 @@ my %disable = (
   ''                => {},
 );
 
-foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
+
+  $search_hash{$field.'_null'} = scalar( $cgi->param($field.'_null') );
 
   my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
 
index d89f491..48c9de7 100755 (executable)
@@ -25,17 +25,43 @@ my %search_hash = ();
 
 $search_hash{'query'} = $cgi->param('query');
 
-for my $param (qw(agentnum magic status classnum pkgpart)) {
-  $search_hash{$param} = $cgi->param($param)
-    if $cgi->param($param);
+#scalars
+for (qw( agentnum cust_status cust_main_salesnum salesnum custnum magic status
+         custom cust_fields pkgbatch zip
+         477part 477rownum date 
+    )) 
+{
+  $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
+}
+
+#arrays
+for my $param (qw( pkgpart classnum refnum towernum )) {
+  $search_hash{$param} = [ $cgi->param($param) ]
+    if grep { $_ eq $param } $cgi->param;
+}
+
+#scalars that need to be passed if empty
+for my $param (qw( censustract censustract2 )) {
+  $search_hash{$param} = $cgi->param($param) || ''
+    if grep { $_ eq $param } $cgi->param;
+}
+
+#location flags (checkboxes)
+my @loc = grep /^\w+$/, $cgi->param('loc');
+$search_hash{"location_$_"} = 1 foreach @loc;
+
+my $report_option = $cgi->param('report_option');
+$search_hash{report_option} = $report_option if $report_option;
+
+for my $param (grep /^report_option_any/, $cgi->param) {
+  $search_hash{$param} = $cgi->param($param);
 }
 
 ###
 # parse dates
 ###
 
-#false laziness w/report_cust_pkg.html
-# and, now, w/bulk_change_pkg.cgi
+#false laziness w/report_cust_pkg.html and bulk_change_pkg.cgi
 my %disable = (
   'all'             => {},
   'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
@@ -45,7 +71,9 @@ my %disable = (
   ''                => {},
 );
 
-foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
+
+  $search_hash{$field.'_null'} = scalar( $cgi->param($field.'_null') );
 
   my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
 
index 7e33e15..cb20712 100755 (executable)
@@ -1,4 +1,4 @@
-<& /elements/popup-topreload.html, et("Package $past_method") &>
+<& /elements/popup-topreload.html, emt("Package $past_method") &>
 <%once>
 
 my %past = ( 'cancel'   => 'cancelled',
index 0cdd8de..4a14893 100644 (file)
@@ -457,6 +457,8 @@ if ( $cgi->param('nottax') ) {
       push @where, "billpkgtaxratelocationnum IS NULL";
     }
 
+    $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum ) ';
+
     $join_pkg .= ' LEFT JOIN tax_rate_location USING ( taxratelocationnum ) ';
 
   } elsif ( $conf->exists('tax-pkg_address') ) {
index 33e1815..65460f7 100644 (file)
                               ucfirst($_[0]->msgtype) || $_[0]->msgname
                             },
                             sub {
-                              join('<BR>', split(/,\s*/, $_[0]->env_to) )
+                              join('<BR>',
+                                map { encode_entities($_->format) }
+                                Email::Address->parse($_[0]->env_to)
+                              )
                             },
                             'status',
                             sub { encode_entities($_[0]->error) },
index dbd346d..df1d7e5 100755 (executable)
@@ -203,6 +203,8 @@ my %disable = (
 
 foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
 
+  $search_hash{$field.'_null'} = scalar( $cgi->param($field.'_null') );
+
   my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
 
   next if $beginning == 0 && $ending == 4294967295
index 3d38a8c..b15fe86 100644 (file)
@@ -110,7 +110,7 @@ for my $i (0 .. scalar(@groups) - 1) {
   push @group_labels, $label;
 
   my @footer;
-  if ($opt{'subtotal_row'}) {
+  if ($opt{'subtotal_row'} and @groups > 1) {
     for( my $col = 0;
          exists($opt{'subtotal_row'}[$col]) or exists($opt{'header'}[$col]);
          $col++
index 9c2418a..28d0040 100644 (file)
@@ -106,14 +106,18 @@ if ($group_info->{num} > 1) {
 &>
 
 <DIV CLASS="fstabcontainer">
+% if ( $group->num_rows > 0 ) {
+<P><% emt('[quant,_1,_2]', $group->num_rows, $opt{name_singular}) %>
+</P>
 %# download links
-<P><% emt('Download full results') %><BR>
+<P><% emt('Download results:') %>
 % $cgi->param('type', 'xls');
-<A HREF="<% $cgi->self_url %>"><% emt('as Excel spreadsheet') %></A><BR>
+<A HREF="<% $cgi->self_url %>"><% emt('Spreadsheet') %></A>&nbsp;|&nbsp;
 % $cgi->param('type', 'html-print');
-<A HREF="<% $cgi->self_url %>"><% emt('as printable copy') %></A><BR>
+<A HREF="<% $cgi->self_url %>"><% emt('webpage') %></A>
 % $cgi->delete('type');
 </P>
+% }
 
 <% $pager %>
 
index 27aecec..556177c 100755 (executable)
@@ -80,6 +80,7 @@
 
                 what.form.<% $field %>_beginning_text.disabled = true;
                 what.form.<% $field %>_ending_text.disabled = true;
+                what.form.<% $field %>_null.disabled = true;
                 what.form.<% $field %>_beginning_text.style.backgroundColor = '#dddddd';
                 what.form.<% $field %>_ending_text.style.backgroundColor = '#dddddd';
 
 
 %             } else {
 
-                what.form.<% $field %>_beginning_text.disabled = false;
-                what.form.<% $field %>_ending_text.disabled = false;
-                what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
-                what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+                what.form.<% $field %>_null.disabled = false;
 
-                what.form.<% $field %>_beginning_button.style.display = '';
-                what.form.<% $field %>_ending_button.style.display = '';
-                what.form.<% $field %>_beginning_disabled.style.display = 'none';
-                what.form.<% $field %>_ending_disabled.style.display = 'none';
+                if ( ! what.form.<% $field %>_null.checked ) {
+
+                  what.form.<% $field %>_beginning_text.disabled = false;
+                  what.form.<% $field %>_ending_text.disabled = false;
+                  what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
+                  what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+
+                  what.form.<% $field %>_beginning_button.style.display = '';
+                  what.form.<% $field %>_ending_button.style.display = '';
+                  what.form.<% $field %>_beginning_disabled.style.display = 'none';
+                  what.form.<% $field %>_ending_disabled.style.display = 'none';
+
+                }
 
 %             }
 %           }
 
       }
 
+%     foreach my $field (@date_fields) {
+
+        function <% $field %>_null_changed(what) {
+
+          if ( what.checked ) {
+            what.form.<% $field %>_beginning_text.disabled = true;
+            what.form.<% $field %>_ending_text.disabled = true;
+            what.form.<% $field %>_beginning_text.style.backgroundColor = '#dddddd';
+            what.form.<% $field %>_ending_text.style.backgroundColor = '#dddddd';
+            what.form.<% $field %>_beginning_button.style.display = 'none';
+            what.form.<% $field %>_ending_button.style.display = 'none';
+            what.form.<% $field %>_beginning_disabled.style.display = '';
+            what.form.<% $field %>_ending_disabled.style.display = '';
+
+          } else {
+            what.form.<% $field %>_beginning_text.disabled = false;
+            what.form.<% $field %>_ending_text.disabled = false;
+            what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
+            what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+
+            what.form.<% $field %>_beginning_button.style.display = '';
+            what.form.<% $field %>_ending_button.style.display = '';
+            what.form.<% $field %>_beginning_disabled.style.display = 'none';
+            what.form.<% $field %>_ending_disabled.style.display = 'none';
+
+          }
+
+        }
+
+%     }
+
     </SCRIPT>
 
     <& /elements/tr-select-pkg_class.html,
             <TD></TD>
             <TD>From date <i>(m/d/y)</i></TD>
             <TD>To date <i>(m/d/y)</i></TD>
+            <TD>Empty date</TD>
           </TR>
 %   my $noinit = 0;
 %   foreach my $field (@date_fields) {
             </TD>
 %     $noinit = 1;
 %     }
+            <TD ALIGN="center">
+              <& /elements/checkbox.html,
+                   'field'    => $field.'_null',
+                   'value'    => 'Y',
+                   'onchange' => $field.'_null_changed',
+              &>
+            </TD>
           </TR>
 %   } #foreach $field
         </TABLE>
index 14f7fb0..dd15c7b 100644 (file)
                                          && ! $cust_pkg->get('cancel')
                                          && $can_discount_pkg
                                       },
-                   popup       => "edit/cust_pkg_discount.html?$plink".
+                   popup       => "edit/cust_pkg_discount.html?$plink",
                    actionlabel => emt('Discount package'),
                    width       => 616,
                  },
index 91a08eb..d2b043c 100755 (executable)
@@ -61,7 +61,9 @@ $custmsgnum =~ /^(\d+)$/ or die "illegal custmsgnum";
 my $cust_msg = qsearchs('cust_msg', { 'custmsgnum' => $custmsgnum });
 my $date = '';
 $date = time2str('%Y-%m-%d %T', $cust_msg->_date) if ( $cust_msg->_date );
-my $env_to = join('</TD></TR><TR><TD></TD><TD>', split(',', $cust_msg->env_to));
+my @to = map { encode_entities($_->format) }
+          Email::Address->parse($cust_msg->env_to);
+my $env_to = join('</TD></TR><TR><TD></TD><TD>', @to);
 
 my %label = (
   'sent'   => 'Sent:',
index 8377d12..1c99cda 100644 (file)
@@ -62,7 +62,8 @@ function show_route() {
     if ( status == google.maps.DirectionsStatus.OK ) {
       directionsDisplay.setDirections(result);
     } else { 
-      document.body.innerHTML = ('<P STYLE="color: red;">Directions lookup failed with the following error: '+status+'</P>');
+      document.body.innerHTML = ('<P STYLE="color: red;">Directions lookup failed with the following error: '+status+'</P>')
+        + <% include('/elements/google_maps_api_key.html' ) |js_string%>;
     }
   });
 }
diff --git a/ng_selfservice/elements/add_password_validation.php b/ng_selfservice/elements/add_password_validation.php
new file mode 100644 (file)
index 0000000..6938437
--- /dev/null
@@ -0,0 +1,51 @@
+<SCRIPT>
+function add_password_validation (fieldid,nologin) {
+  var inputfield = document.getElementById(fieldid);
+  inputfield.onchange = function () {
+    var fieldid = this.id+'_result';
+    var resultfield = document.getElementById(fieldid);
+    var svcnum = '';
+    var svcfield = document.getElementById(this.id+'_svcnum');
+    if (svcfield) {
+      svcnum = svcfield.options[svcfield.selectedIndex].value;
+    }
+    if (this.value) {
+      resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+      var validate_data = {
+        fieldid: fieldid,
+        check_password: this.value,
+      };
+      if (!nologin) {
+        validate_data['svcnum'] = svcnum;
+      }
+      $.ajax({
+        url: 'xmlrpc_validate_passwd.php',
+        data: validate_data,
+        method: 'POST',
+        success: function ( result ) {
+          result = JSON.parse(result);
+          var resultfield = document.getElementById(fieldid);
+          if (resultfield) {
+            var errorimg = '<IMG SRC="images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            var validimg = '<IMG SRC="images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            if (result.password_valid) {
+              resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
+            } else if (result.password_invalid) {
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.password_invalid+'</SPAN>';
+            } else {
+              resultfield.innerHTML = '';
+            }
+          }
+        },
+        error: function (  jqXHR, textStatus, errorThrown ) {
+          var resultfield = document.getElementById(fieldid);
+          console.log('ajax error: '+textStatus+'+'+errorThrown);
+          if (resultfield) {
+            resultfield.innerHTML = '';
+          }
+        },
+      });
+    }
+  };
+}
+</SCRIPT>
diff --git a/ng_selfservice/images/error.png b/ng_selfservice/images/error.png
new file mode 100644 (file)
index 0000000..628cf2d
Binary files /dev/null and b/ng_selfservice/images/error.png differ
diff --git a/ng_selfservice/images/tick.png b/ng_selfservice/images/tick.png
new file mode 100644 (file)
index 0000000..a9925a0
Binary files /dev/null and b/ng_selfservice/images/tick.png differ
index 41296ed..a6e6795 100644 (file)
@@ -1,5 +1,92 @@
 <? $title ='Change Password'; include('elements/header.php'); ?>
 <? $current_menu = 'password.php'; include('elements/menu.php'); ?>
-Chagne password
+<?
+$error = '';
+$pwd_change_success = false;
+if ( isset($_POST['svcnum']) ) {
+
+  $pwd_change_result = $freeside->myaccount_passwd(array(
+    'session_id'    => $_COOKIE['session_id'],
+    'svcnum'        => $_POST['svcnum'],
+    'new_password'  => $_POST['new_password'],
+    'new_password2' => $_POST['new_password2'],
+  ));
+
+  if ($pwd_change_result['error']) {
+    $error = $pwd_change_result['error'];
+  } else {
+    $pwd_change_success = true;
+  }
+}
+
+if ($pwd_change_success) {
+?>
+
+<P>Password changed for <? echo $pwd_change_result['value'],' ',$pwd_change_result['label'] ?>.</P>
+
+<?
+} else {
+  $pwd_change_svcs = $freeside->list_svcs(array(
+    'session_id' => $_COOKIE['session_id'],
+    'svcdb'      => 'svc_acct',
+  ));
+  if (isset($pwd_change_svcs['error'])) {
+    $error = $error || $pwd_change_svcs['error'];
+  }
+  if (!isset($pwd_change_svcs['svcs'])) {
+    $pwd_change_svcs['svcs'] = $pwd_change_svcs['svcs'];
+    $error = $error || 'Unknown error loading services';
+  }
+  if ($error) {
+    include('elements/error.php');
+  }
+?>
+
+<FORM METHOD="POST">
+<TABLE BGCOLOR="#cccccc">
+  <TR>
+    <TH ALIGN="right">Change password for account: </TH>
+    <TD>
+      <SELECT ID="new_password_svcnum" NAME="svcnum">
+<?
+  $selected_svcnum = isset($_POST['svcnum']) ? $_POST['svcnum'] : $pwd_change_svcs['svcnum'];
+  foreach ($pwd_change_svcs['svcs'] as $svc) {
+?>
+        <OPTION VALUE="<? echo $svc['svcnum'] ?>"<? echo $selected_svcnum == $svc['svcnum'] ? ' SELECTED' : '' ?>>
+          <? echo $svc['label'],': ',$svc['value'] ?>
+        </OPTION>
+<?
+  }
+?>
+      </SELECT>
+    </TD>
+  </TR>
+
+  <TR>
+    <TH ALIGN="right">New password: </TH>
+    <TD>
+      <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
+      <DIV ID="new_password_result"></DIV>
+<? include('elements/add_password_validation.php'); ?>
+      <SCRIPT>add_password_validation('new_password');</SCRIPT>
+    </TD>
+  </TR>
+
+  <TR>
+    <TH ALIGN="right">Re-enter new password: </TH>
+    <TD><INPUT TYPE="password" NAME="new_password2" SIZE="18"></TD>
+  </TR>
+
+</TABLE>
+<BR>
+
+<INPUT TYPE="submit" VALUE="Change password">
+
+</FORM>
+
+<?
+} // end if $pwd_change_show_form
+?>
+
 <? include('elements/menu_footer.php'); ?>
 <? include('elements/footer.php'); ?>
diff --git a/ng_selfservice/xmlrpc_validate_passwd.php b/ng_selfservice/xmlrpc_validate_passwd.php
new file mode 100644 (file)
index 0000000..5632dc3
--- /dev/null
@@ -0,0 +1,15 @@
+<?
+
+require_once('elements/session.php');
+
+$xmlrpc_args = array(
+  fieldid        => $_POST['fieldid'],
+  check_password => $_POST['check_password'],
+  svcnum         => $_POST['svcnum'],
+  session_id     => $_COOKIE['session_id']
+);
+
+$result = $freeside->validate_passwd($xmlrpc_args);
+echo json_encode($result);
+
+?>