self-service payments send receipt like back-office ones, RT#32191
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index 54d5306..279acb2 100644 (file)
@@ -136,6 +136,36 @@ sub skin_info {
       'logo' => scalar($conf->config_binary('logo.png', $agentnum )),
       ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
         qw( head body_header body_footer company_address ) ),
+      'menu' => join("\n", $conf->config("ng_selfservice-menu", $agentnum ) ) ||
+                'main.php Home
+
+                 services.php Services
+                 services.php My Services
+                 services_new.php Order a new service
+
+                 personal.php Profile
+                 personal.php Personal Information
+                 password.php Change Password
+
+                 payment.php Payments
+                 payment_cc.php Credit Card Payment
+                 payment_ach.php Electronic Check Payment
+                 payment_paypal.php PayPal Payment
+                 payment_webpay.php Webpay Payments
+
+                 usage.php Usage
+                 usage_data.php Data usage
+                 usage_cdr.php Call usage
+
+                 tickets.php Help Desk
+                 tickets.php Open Tickets
+                 tickets_resolved.php Resolved Tickets
+                 ticket_create.php Create a new ticket
+
+                 docs.php FAQs
+
+                 logout.php Logout
+                ',
     };
 
     _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
@@ -278,11 +308,12 @@ sub login {
 
 sub logout {
   my $p = shift;
+  my $skin_info = skin_info($p);
   if ( $p->{'session_id'} ) {
     _cache->remove($p->{'session_id'});
-    return { %{ skin_info($p) }, 'error' => '' };
+    return { %$skin_info, 'error' => '' };
   } else {
-    return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message
+    return { %$skin_info, 'error' => "Can't resume session" }; #better error message
   }
 }
 
@@ -368,6 +399,8 @@ sub access_info {
 
   $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
 
+  $info->{'hide_usage'} = $conf->exists('selfservice_hide-usage');
+
   return { %$info,
            'custnum'       => $custnum,
            'access_pkgnum' => $session->{'pkgnum'},
@@ -384,58 +417,29 @@ sub customer_info {
   my %return;
 
   my $conf = new FS::Conf;
-  if ($conf->exists('cust_main-require_address2')) {
-    $return{'require_address2'} = '1';
-  }else{
-    $return{'require_address2'} = '';
-  }
+  $return{'require_address2'} = $conf->exists('cust_main-require_address2');
 
-  if ( $FS::TicketSystem::system ) {
-    warn "$me customer_info: initializing ticket system\n" if $DEBUG;
-    FS::TicketSystem->init();
-  }
+#  if ( $FS::TicketSystem::system ) {
+#    warn "$me customer_info: initializing ticket system\n" if $DEBUG;
+#    FS::TicketSystem->init();
+#  }
  
   if ( $custnum ) { #customer record
 
+    %return = ( %return, %{ customer_info_short($p) } );
+
+    #redundant with customer_info_short, but we need it for several things below
     my $search = { 'custnum' => $custnum };
     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
     my $cust_main = qsearchs('cust_main', $search )
       or return { 'error' => "unknown custnum $custnum" };
 
-    $return{display_custnum} = $cust_main->display_custnum;
+    my $list_tickets = list_tickets($p);
+    $return{'tickets'} = $list_tickets->{'tickets'};
 
-    if ( $session->{'pkgnum'} ) { 
-      $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
-      #next_bill_date from cust_pkg?
+    if ( $session->{'pkgnum'} ) {
+      #XXX open invoices in the pkg-balances case
     } else {
-      $return{balance} = $cust_main->balance;
-      $return{next_bill_date} = $cust_main->next_bill_date;
-      $return{next_bill_date_pretty} =
-        $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
-                                : '(none)';
-    }
-
-    my @tickets = $cust_main->tickets;
-    # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html
-    if ( $FS::TicketSystem::system && FS::TicketSystem->selfservice_priority ) {
-      my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
-      $return{tickets} = [ 
-        sort { 
-          (
-            ($a->{'_selfservice_priority'} eq '') <=>
-            ($b->{'_selfservice_priority'} eq '')
-          ) ||
-          ( $dir * 
-            ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'}) 
-          )
-        } @tickets
-      ];
-    }
-    else {
-      $return{tickets} = \@tickets;
-    }
-
-    unless ( $session->{'pkgnum'} ) {
       my @open = map {
                        {
                          invnum => $_->invnum,
@@ -453,29 +457,17 @@ sub customer_info {
         time2str('%m/%d/%Y', $return{'last_invoice_date'} );
     }
 
-    $return{countrydefault} = scalar($conf->config('countrydefault'));
-
+    #customer_info_short always has nobalance on..
     $return{small_custview} =
       small_custview( $cust_main,
                       $return{countrydefault},
                       ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance
                     );
 
-    $return{name} = $cust_main->first. ' '. $cust_main->get('last');
-
     $return{has_ship_address} = $cust_main->has_ship_address;
     $return{status} = $cust_main->status;
     $return{statuscolor} = $cust_main->statuscolor;
 
-    for (@cust_main_editable_fields) {
-      $return{$_} = $cust_main->get($_);
-    }
-
-    for (@location_editable_fields) {
-      $return{$_} = $cust_main->bill_location->get($_);
-      $return{'ship_'.$_} = $cust_main->ship_location->get($_);
-    }
-    $return{has_ship_address} = $cust_main->has_ship_address;
     # compatibility: some places in selfservice use this to determine
     # if there's a ship address
     if ( $return{has_ship_address} ) {
@@ -483,16 +475,6 @@ sub customer_info {
       $return{ship_first} = $cust_main->first;
     }
 
-    if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
-      $return{payinfo} = $cust_main->paymask;
-      @return{'month', 'year'} = $cust_main->paydate_monthyear;
-    }
-
-    $return{'invoicing_list'} =
-      join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
-    $return{'postal_invoicing'} =
-      0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
-
     if (scalar($conf->config('support_packages'))) {
       my @support_services = ();
       foreach ($cust_main->support_services) {
@@ -519,12 +501,6 @@ sub customer_info {
       $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
     }
 
-    if ( $session->{'svcnum'} ) {
-      my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} });
-      $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc;
-      $return{'svcnum'} = $session->{'svcnum'};
-    }
-
   } elsif ( $session->{'svcnum'} ) { #no customer record
 
     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
@@ -563,6 +539,17 @@ sub customer_info_short {
 
     $return{display_custnum} = $cust_main->display_custnum;
 
+    if ( $session->{'pkgnum'} ) { 
+      $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
+      #next_bill_date from cust_pkg?
+    } else {
+      $return{balance} = $cust_main->balance;
+      $return{next_bill_date} = $cust_main->next_bill_date;
+      $return{next_bill_date_pretty} =
+        $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
+                                : '(none)';
+    }
+
     $return{countrydefault} = scalar($conf->config('countrydefault'));
 
     $return{small_custview} =
@@ -590,10 +577,11 @@ sub customer_info_short {
       @return{'month', 'year'} = $cust_main->paydate_monthyear;
     }
     
+    my @invoicing_list = $cust_main->invoicing_list;
     $return{'invoicing_list'} =
-      join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
-    #$return{'postal_invoicing'} =
-    #  0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
+      join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
+    $return{'postal_invoicing'} =
+      0 < ( grep { $_ eq 'POST' } @invoicing_list );
 
     if ( $session->{'svcnum'} ) {
       my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} });
@@ -762,6 +750,8 @@ sub edit_info {
     $payby = $1;
   }
 
+  my $conf = new FS::Conf;
+
   if ( $payby =~ /^(CARD|DCRD)$/ ) {
 
     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
@@ -774,6 +764,10 @@ sub edit_info {
 
     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
 
+    if ( $conf->exists('selfservice-onfile_require_cvv') ){
+      return { 'error' => 'CVV2 is required' } unless $p->{'paycvv'};
+    }
+
   } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) {
 
     my $payinfo;
@@ -850,6 +844,10 @@ sub payment_info {
 
       'card_types' => card_types(),
 
+      'withcvv'            => $conf->exists('selfservice-require_cvv'), #or enable optional cvv?
+      'require_cvv'        => $conf->exists('selfservice-require_cvv'),
+      'onfile_require_cvv' => $conf->exists('selfservice-onfile_require_cvv'),
+
       'paytypes' => [ @FS::cust_main::paytypes ],
 
       'paybys' => [ $conf->config('signup_server-payby') ],
@@ -922,7 +920,8 @@ sub payment_info {
 
   #doubleclick protection
   my $_date = time;
-  $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
+  $return{payunique} = "webui-MyAccount-$_date-$$-". rand() * 2**32; #new
+  $return{paybatch} = $return{payunique};  #back compat
 
   return { 'error' => '',
            %return,
@@ -972,10 +971,16 @@ sub validate_payment {
     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
   my $payname = $1;
 
+  $p->{'payunique'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+    or return { 'error' => gettext('illegal_text'). " payunique: ". $p->{'payunique'} };
+  my $payunique = $1;
+
   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
   my $paybatch = $1;
 
+  $payunique = $paybatch if ! length($payunique) && length($paybatch);
+
   $p->{'payby'} ||= 'CARD';
   $p->{'payby'} =~ /^([A-Z]{4})$/
     or return { 'error' => "illegal_payby " . $p->{'payby'} };
@@ -1001,10 +1006,14 @@ sub validate_payment {
    
     $payinfo = $p->{'payinfo'};
 
+    my $onfile = 0;
+
     #more intelligent matching will be needed here if you change
     #card_masking_method and don't remove existing paymasks
-    $payinfo = $cust_main->payinfo
-      if $cust_main->paymask eq $payinfo;
+    if ( $cust_main->paymask eq $payinfo ) {
+      $payinfo = $cust_main->payinfo;
+      $onfile = 1;
+    }
 
     $payinfo =~ s/\D//g;
     $payinfo =~ /^(\d{13,16}|\d{8,9})$/
@@ -1026,6 +1035,10 @@ sub validate_payment {
           or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
         $paycvv = $1;
       }
+    } elsif ( $conf->exists('selfservice-onfile_require_cvv') ) {
+      return { 'error' => 'CVV2 is required' };
+    } elsif ( !$onfile && $conf->exists('selfservice-require_cvv') ) {
+      return { 'error' => 'CVV2 is required' };
     }
   
   } else {
@@ -1053,7 +1066,8 @@ sub validate_payment {
     'month'          => $p->{'month'},
     'year'           => $p->{'year'},
     'payname'        => $payname,
-    'paybatch'       => $paybatch, #this doesn't actually do anything
+    'payunique'      => $payunique,
+    'paybatch'       => $paybatch,
     'paycvv'         => $paycvv,
     'payname'        => $payname,
     'discount_term'  => $discount_term,
@@ -1117,6 +1131,7 @@ sub do_process_payment {
 
   my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
     'quiet'       => 1,
+    'manual'      => 1,
     'selfservice' => 1,
     'paynum_ref'  => \$paynum,
     %$validate,
@@ -1227,16 +1242,14 @@ sub do_process_payment {
 
   if ( $cust_pay ) {
 
-    my($gw, $auth, $order) = split(':', $cust_pay->paybatch);
-
     return {
       'error'        => '',
       'amount'       => sprintf('%.2f', $cust_pay->paid),
       'date'         => $cust_pay->_date,
       'date_pretty'  => time2str('%Y-%m-%d', $cust_pay->_date),
       'time_pretty'  => time2str('%T', $cust_pay->_date),
-      'auth_num'     => $auth,
-      'order_num'    => $order,
+      'auth_num'     => $cust_pay->auth,
+      'order_num'    => $cust_pay->order_number,
       'receipt_html' => $receipt_html,
     };
 
@@ -1604,6 +1617,7 @@ sub list_pkgs {
     or return { 'error' => "unknown custnum $custnum" };
 
   my $conf = new FS::Conf;
+  my $immutable = $conf->exists('selfservice_immutable-package');
   
 # the duplication below is necessary:
 # 1. to maintain the current buggy behaviour wrt the cust_pkg and part_pkg
@@ -1616,6 +1630,7 @@ sub list_pkgs {
            'custnum'  => $custnum,
            'cust_pkg' => [ map {
                           { $_->hash,
+                           immutable => $immutable,
                             part_pkg => [ map $_->hashref, $_->part_pkg ],
                             part_svc =>
                               [ map $_->hashref, $_->available_part_svc ],
@@ -1648,8 +1663,10 @@ sub list_pkgs {
                           my $primary_cust_svc = $_->primary_cust_svc;
                           +{ $_->hash,
                             $_->part_pkg->hash,
-                            pkg_label => $_->pkg_locale,
-                            status => $_->status,
+                           immutable   => $immutable,
+                            pkg_label   => $_->pkg_locale,
+                            status      => $_->status,
+                            statuscolor => $_->statuscolor,
                             part_svc =>
                               [ map { $_->hashref }
                                   grep { $_->selfservice_access ne 'hidden' }
@@ -1685,6 +1702,7 @@ sub list_pkgs {
                   ],
     'small_custview' =>
       small_custview( $cust_main, $conf->config('countrydefault') ),
+    'date_format' => $conf->config('date_format') || '%m/%d/%Y',
   };
 
 }
@@ -1695,6 +1713,9 @@ sub list_svcs {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
+  my $conf = new FS::Conf;
+
+  my $hide_usage = $conf->exists('selfservice_hide-usage') ? 1 : 0;
   my $search = { 'custnum' => $custnum };
   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
   my $cust_main = qsearchs('cust_main', $search )
@@ -1708,7 +1729,6 @@ sub list_svcs {
 
   my @cust_svc = ();
   my @cust_pkg_usage = ();
-  #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
   foreach my $cust_pkg ( $p->{'ncancelled'} 
                          ? $cust_main->ncancelled_pkgs
                          : $cust_main->unsuspended_pkgs ) {
@@ -1719,14 +1739,16 @@ sub list_svcs {
 
   @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
   my %usage_pools;
-  foreach (@cust_pkg_usage) {
-    my $part = $_->part_pkg_usage;
-    my $tag = $part->description . ($part->shared ? 1 : 0);
-    my $row = $usage_pools{$tag} 
-          ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
-    $row->[1] += $_->minutes; # minutes remaining
-    $row->[2] += $part->minutes; # minutes total
-  }
+  if (!$hide_usage) {
+    foreach (@cust_pkg_usage) {
+      my $part = $_->part_pkg_usage;
+      my $tag = $part->description . ($part->shared ? 1 : 0);
+      my $row = $usage_pools{$tag} 
+            ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
+      $row->[1] += sprintf('%.1f', $_->minutes); # minutes remaining
+      $row->[2] += $part->minutes; # minutes total
+    }
+  } # otherwise just leave them empty
 
   if ( $p->{'svcdb'} ) {
     my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
@@ -1740,107 +1762,110 @@ sub list_svcs {
   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
   #              @svc_x;
 
-    my $conf = new FS::Conf;
+  my @svcs; # stuff to return to the client
+  foreach my $cust_svc (@cust_svc) {
+    my $svc_x = $cust_svc->svc_x;
+    my($label, $value) = $cust_svc->label;
+    my $part_svc = $cust_svc->part_svc;
+    my $svcdb = $part_svc->svcdb;
+    my $cust_pkg = $cust_svc->cust_pkg;
+    my $part_pkg = $cust_pkg->part_pkg;
 
-  { 
+    my %hash = (
+      'svcnum'         => $cust_svc->svcnum,
+      'display_svcnum' => $cust_svc->display_svcnum,
+      'svcdb'          => $svcdb,
+      'label'          => $label,
+      'value'          => $value,
+      'pkg_label'      => $cust_pkg->pkg_locale,
+      'pkg_status'     => $cust_pkg->status,
+      'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
+    );
+
+    # would it make sense to put this in a svc_* method?
+
+    if ( $svcdb eq 'svc_acct' ) {
+      foreach (qw(username email finger seconds)) {
+        $hash{$_} = $svc_x->$_;
+      }
+
+      if (!$hide_usage) {
+        %hash = (
+          %hash,
+          'upbytes'    => display_bytecount($svc_x->upbytes),
+          'downbytes'  => display_bytecount($svc_x->downbytes),
+          'totalbytes' => display_bytecount($svc_x->totalbytes),
+
+          'recharge_amount'  => $part_pkg->option('recharge_amount',1),
+          'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
+          'recharge_upbytes'    =>
+            display_bytecount($part_pkg->option('recharge_upbytes',1)),
+          'recharge_downbytes'  =>
+            display_bytecount($part_pkg->option('recharge_downbytes',1)),
+          'recharge_totalbytes' =>
+            display_bytecount($part_pkg->option('recharge_totalbytes',1)),
+          # more...
+        );
+      }
+
+    } elsif ( $svcdb eq 'svc_dsl' ) {
+
+      $hash{'phonenum'} = $svc_x->phonenum;
+      if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
+        $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
+        $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
+          if $svc_x->company;
+      } else {
+        $hash{'name'} = $cust_main->name;
+      }
+      # no usage to hide here
+
+    } elsif ( $svcdb eq 'svc_phone' ) {
+      if (!$hide_usage) {
+        # could potentially show lots of things...
+        $hash{'outbound'} = 1;
+        $hash{'inbound'}  = 0;
+        if ( $part_pkg->plan eq 'voip_inbound' ) {
+          $hash{'outbound'} = 0;
+          $hash{'inbound'}  = 1;
+        } elsif ( $part_pkg->option('selfservice_inbound_format')
+              or  $conf->config('selfservice-default_inbound_cdr_format')
+        ) {
+          $hash{'inbound'}  = 1;
+        }
+        foreach (qw(inbound outbound)) {
+          # hmm...we can't filter by status here, because there might
+          # not be cdr_terminations at all.  have to go by date.
+          # find all since the last bill date.
+          # XXX cdr types?  we are going to need them.
+          if ( $hash{$_} ) {
+            my $sum_cdr = $svc_x->sum_cdrs(
+              'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
+              'begin'   => ($cust_pkg->last_bill || 0),
+              'nonzero' => 1,
+              'disable_charged_party' => 1,
+            );
+            $hash{$_} = $sum_cdr->hashref;
+          }
+        }
+      } # not hiding usage
+    } # svcdb
+
+    push @svcs, \%hash;
+  } # foreach $cust_svc
+
+  return { 
     'svcnum'   => $session->{'svcnum'},
     'custnum'  => $custnum,
     'date_format' => $conf->config('date_format') || '%m/%d/%Y',
     'view_usage_nodomain' => $conf->exists('selfservice-view_usage_nodomain'),
-    'svcs'     => [
-      map { 
-            my $svc_x = $_->svc_x;
-            my($label, $value) = $_->label;
-            my $part_svc = $_->part_svc;
-            my $svcdb = $part_svc->svcdb;
-            my $cust_pkg = $_->cust_pkg;
-            my $part_pkg = $cust_pkg->part_pkg;
-
-            my %hash = (
-              'svcnum'         => $_->svcnum,
-              'display_svcnum' => $_->display_svcnum,
-              'svcdb'          => $svcdb,
-              'label'          => $label,
-              'value'          => $value,
-              'pkg_label'      => $cust_pkg->pkg_locale,
-              'pkg_status'     => $cust_pkg->status,
-              'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
-            );
-
-            if ( $svcdb eq 'svc_acct' ) {
-              %hash = (
-                %hash,
-                'username'   => $svc_x->username,
-                'email'      => $svc_x->email,
-                'finger'     => $svc_x->finger,
-                'seconds'    => $svc_x->seconds,
-                'upbytes'    => display_bytecount($svc_x->upbytes),
-                'downbytes'  => display_bytecount($svc_x->downbytes),
-                'totalbytes' => display_bytecount($svc_x->totalbytes),
-
-                'recharge_amount'  => $part_pkg->option('recharge_amount',1),
-                'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
-                'recharge_upbytes'    =>
-                  display_bytecount($part_pkg->option('recharge_upbytes',1)),
-                'recharge_downbytes'  =>
-                  display_bytecount($part_pkg->option('recharge_downbytes',1)),
-                'recharge_totalbytes' =>
-                  display_bytecount($part_pkg->option('recharge_totalbytes',1)),
-                # more...
-              );
-
-            } elsif ( $svcdb eq 'svc_dsl' ) {
-              $hash{'phonenum'} = $svc_x->phonenum;
-              if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
-                $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
-                $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
-                  if $svc_x->company;
-              } else {
-                $hash{'name'} = $cust_main->name;
-              }
-            } elsif ( $svcdb eq 'svc_phone' ) {
-              # could potentially show lots of things...
-              $hash{'outbound'} = 1;
-              $hash{'inbound'}  = 0;
-              if ( $part_pkg->plan eq 'voip_inbound' ) {
-                $hash{'outbound'} = 0;
-                $hash{'inbound'}  = 1;
-              } elsif ( $part_pkg->option('selfservice_inbound_format')
-                    or  $conf->config('selfservice-default_inbound_cdr_format')
-              ) {
-                $hash{'inbound'}  = 1;
-              }
-              foreach (qw(inbound outbound)) {
-                # hmm...we can't filter by status here, because there might
-                # not be cdr_terminations at all.  have to go by date.
-                # find all since the last bill date.
-                # XXX cdr types?  we are going to need them.
-                if ( $hash{$_} ) {
-                  my $sum_cdr = $svc_x->sum_cdrs(
-                    'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
-                    'begin'   => ($cust_pkg->last_bill || 0),
-                    'nonzero' => 1,
-                  );
-                  $hash{$_} = $sum_cdr->hashref;
-                }
-              }
-            }
-
-            # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) {
-            #  %hash = (
-            #    %hash,
-            #  );
-            #}
-
-            \%hash;
-          }
-          @cust_svc
-    ],
+    'svcs'     => \@svcs,
     'usage_pools' => [
       map { $usage_pools{$_} }
       sort { $a cmp $b }
       keys %usage_pools
     ],
+    'hide_usage' => $hide_usage,
   };
 
 }
@@ -2103,7 +2128,12 @@ sub _list_cdr_usage {
   # we have to return the results all at once...
   my($svc_phone, $begin, $end, %opt) = @_;
   map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
-    $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, %opt );
+    $svc_phone->get_cdrs(
+      'begin'=>$begin,
+      'end'=>$end,
+      'disable_charged_party' => 1,
+      %opt
+    );
 }
 
 sub list_cdr_usage {
@@ -2115,6 +2145,11 @@ sub list_cdr_usage {
 
 sub _usage_details {
   my($callback, $p, %opt) = @_;
+  my $conf = FS::Conf->new;
+
+  if ( $conf->exists('selfservice_hide-usage') ) {
+    return { 'error' => 'Viewing usage is not allowed.' };
+  }
 
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
@@ -2133,7 +2168,6 @@ sub _usage_details {
   my %callback_opt;
   my $header = [];
   if ( $svcdb eq 'svc_phone' ) {
-    my $conf = FS::Conf->new;
     my $format = '';
     if ( $p->{inbound} ) {
       $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') 
@@ -2167,6 +2201,14 @@ sub _usage_details {
     %callback_opt
   );
 
+  if ( $conf->exists('selfservice-hide_cdr_price') ) {
+    # ugly kludge, I know
+    my ($delete_col) = grep { $header->[$_] eq 'Price' } (0..scalar(@$header));
+    if (defined $delete_col) {
+      delete($_->[$delete_col]) foreach ($header, @usage);
+    }
+  }
+
   #kinda false laziness with FS::cust_main::bill, but perhaps
   #we should really change this bit to DateTime and DateTime::Duration
   #
@@ -2324,6 +2366,10 @@ sub change_pkg {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
+  my $conf = new FS::Conf;
+  my $immutable = $conf->exists('selfservice_immutable-package');
+  return { 'error' => "Package modification disabled" } if $immutable;
+
   my $search = { 'custnum' => $custnum };
   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
   my $cust_main = qsearchs('cust_main', $search )
@@ -2345,7 +2391,6 @@ sub change_pkg {
                                    \@newpkg,
                                  );
 
-  my $conf = new FS::Conf;
   if ( $conf->exists('signup_server-realtime') ) {
 
     my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_credit'=>1 );
@@ -2846,6 +2891,13 @@ sub myaccount_passwd {
   my $error = '';
 
   my $conf = new FS::Conf;
+
+  return { 'error' => 'Incorrect current password.' }
+    if  ( exists($p->{'old_password'})
+          || $conf->exists('selfservice-password_change_oldpass')
+        )
+    && ! $svc_acct->check_password($p->{'old_password'});
+
   $error = 'Password too short.'
     if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6);
   $error = 'Password too long.'
@@ -3191,6 +3243,52 @@ sub process_reset_passwd {
 
 }
 
+sub list_tickets {
+  my $p = shift;
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  my @tickets = ();
+  if ( $session->{'pkgnum'} ) { 
+
+    #tickets for specific service with pkg-balances on
+    my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+                                          'pkgnum'  => $session->{'pkgnum'} })
+                     or return { 'error' => 'unknown package' };
+    foreach my $cust_svc ( $cust_pkg->cust_svc ) {
+      push @tickets, $cust_svc->tickets( $p->{status} );
+    }
+
+  } else {
+
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+      or return { 'error' => "unknown custnum $custnum" };
+
+    @tickets = $cust_main->tickets( $p->{status} );
+  }
+
+  # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html
+  if ( $FS::TicketSystem::system && FS::TicketSystem->selfservice_priority ) {
+    my $conf = new FS::Conf;
+    my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
+    +{ tickets => [ 
+         sort { 
+           (
+             ($a->{'_selfservice_priority'} eq '') <=>
+             ($b->{'_selfservice_priority'} eq '')
+           ) ||
+           ( $dir * 
+             ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'})
+           )
+         } @tickets
+       ]
+    };
+  } else {
+    +{ tickets => \@tickets };
+  }
+
+}
+
 sub create_ticket {
   my $p = shift;
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
@@ -3312,10 +3410,10 @@ sub get_ticket {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
-  warn "$me get_ticket: initializing ticket system\n" if $DEBUG;
-  FS::TicketSystem->init();
-  return { 'error' => 'get_ticket configuration error' }
-    if $FS::TicketSystem::system ne 'RT_Internal';
+#  warn "$me get_ticket: initializing ticket system\n" if $DEBUG;
+#  FS::TicketSystem->init();
+#  return { 'error' => 'get_ticket configuration error' }
+#    if $FS::TicketSystem::system ne 'RT_Internal';
 
   # check existence and ownership as part of this
   warn "$me get_ticket: fetching ticket\n" if $DEBUG;
@@ -3387,8 +3485,8 @@ sub adjust_ticket_priority {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
-  warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG;
-  FS::TicketSystem->init;
+#  warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG;
+#  FS::TicketSystem->init;
   my $ss_priority = FS::TicketSystem->selfservice_priority;
 
   return { 'error' => 'adjust_ticket_priority configuration error' }