add display_svcnum & pkg_label to list_svcs selfservice API call, RT#17617
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index 7608439..88a5675 100644 (file)
@@ -4,12 +4,16 @@ use 5.008; #require 5.8+ for Time::Local 1.05+
 use strict;
 use vars qw( $cache $DEBUG $me );
 use subs qw( _cache _provision );
+use IO::Scalar;
 use Data::Dumper;
 use Digest::MD5 qw(md5_hex);
 use Date::Format;
-use Business::CreditCard;
 use Time::Duration;
 use Time::Local qw(timelocal_nocheck);
+use Business::CreditCard;
+use HTML::Entities;
+use Text::CSV_XS;
+use Spreadsheet::WriteExcel;
 use FS::UI::Web::small_custview qw(small_custview); #less doh
 use FS::UI::Web;
 use FS::UI::bytecount qw( display_bytecount );
@@ -19,9 +23,11 @@ use FS::Record qw(qsearch qsearchs dbh);
 use FS::Msgcat qw(gettext);
 use FS::Misc qw(card_types);
 use FS::Misc::DateTime qw(parse_datetime);
+use FS::TicketSystem;
 use FS::ClientAPI_SessionCache;
 use FS::cust_svc;
 use FS::svc_acct;
+use FS::svc_forward;
 use FS::svc_domain;
 use FS::svc_phone;
 use FS::svc_external;
@@ -30,15 +36,12 @@ use FS::dsl_device;
 use FS::part_svc;
 use FS::cust_main;
 use FS::cust_bill;
+use FS::legacy_cust_bill;
 use FS::cust_main_county;
 use FS::cust_pkg;
 use FS::payby;
 use FS::acct_rt_transaction;
-use HTML::Entities;
-use FS::TicketSystem;
-use Text::CSV_XS;
-use IO::Scalar;
-use Spreadsheet::WriteExcel;
+use FS::msg_template;
 
 $DEBUG = 0;
 $me = '[FS::ClientAPI::MyAccount]';
@@ -112,13 +115,13 @@ sub skin_info {
       ( map { $_ => scalar( $conf->config($_, $agentnum) ) }
         qw( company_name date_format ) ),
       ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) }
-        qw( body_bgcolor box_bgcolor
+        qw( body_bgcolor box_bgcolor stripe1_bgcolor stripe2_bgcolor
             text_color link_color vlink_color hlink_color alink_color
             font title_color title_align title_size menu_bgcolor menu_fontsize
           )
       ),
       ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
-        qw( menu_skipblanks menu_skipheadings menu_nounderline )
+        qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo )
       ),
       ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) }
         qw( title_left_image title_right_image
@@ -148,12 +151,25 @@ sub login_info {
     %{ skin_info($p) },
     'phone_login'  => $conf->exists('selfservice_server-phone_login'),
     'single_domain'=> scalar($conf->config('selfservice_server-single_domain')),
+    'banner_url'       => scalar($conf->config('selfservice-login_banner_url')),
+    'banner_image_md5' => 
+      md5_hex($conf->config_binary('selfservice-login_banner_image')),
   );
 
   return \%info;
 
 }
 
+sub login_banner_image {
+  my $p = shift;
+  my $conf = new FS::Conf;
+  my $image = $conf->config_binary('selfservice-login_banner_image');
+  return { 
+    'md5'   => md5_hex($image),
+    'image' => $image,
+  };
+}
+
 #false laziness w/FS::ClientAPI::passwd::passwd
 sub login {
   my $p = shift;
@@ -180,6 +196,8 @@ sub login {
 
   } else {
 
+warn Dumper($p);
+
     my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
       or return { error => 'Domain '. $p->{'domain'}. ' not found' };
 
@@ -365,10 +383,16 @@ sub customer_info {
     my $cust_main = qsearchs('cust_main', $search )
       or return { 'error' => "unknown custnum $custnum" };
 
+    $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} =
+        time2str('%m/%d/%Y', $return{next_bill_date} );
     }
 
     my @tickets = $cust_main->tickets;
@@ -400,17 +424,30 @@ sub customer_info {
                        };
                      } $cust_main->open_cust_bill;
       $return{open_invoices} = \@open;
+
+      my $sql = 'SELECT MAX(_date) FROM cust_bill WHERE custnum = ?';
+      my $sth = dbh->prepare($sql) or die  dbh->errstr;
+      $sth->execute($custnum)      or die $sth->errstr;
+      $return{'last_invoice_date'} = $sth->fetchrow_arrayref->[0];
+      $return{'last_invoice_date_pretty'} =
+        time2str('%m/%d/%Y', $return{'last_invoice_date'} );
     }
 
+    $return{countrydefault} = scalar($conf->config('countrydefault'));
+
     $return{small_custview} =
       small_custview( $cust_main,
-                      scalar($conf->config('countrydefault')),
+                      $return{countrydefault},
                       ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance
                     );
 
     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
     $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_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($_);
     }
@@ -469,8 +506,8 @@ sub customer_info {
 
   }
 
-  return { 'error'          => '',
-           'custnum'        => $custnum,
+  return { 'error'   => '',
+           'custnum' => $custnum,
            %return,
          };
 
@@ -493,9 +530,13 @@ sub customer_info_short {
     my $cust_main = qsearchs('cust_main', $search )
       or return { 'error' => "unknown custnum $custnum" };
 
+    $return{display_custnum} = $cust_main->display_custnum;
+
+    $return{countrydefault} = scalar($conf->config('countrydefault'));
+
     $return{small_custview} =
       small_custview( $cust_main,
-                      scalar($conf->config('countrydefault')),
+                      $return{countrydefault},
                       1, ##nobalance
                     );
 
@@ -542,6 +583,103 @@ sub customer_info_short {
          };
 }
 
+sub billing_history {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  return { 'error' => 'No customer' } unless $custnum;
+
+  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" };
+
+  my %return = ();
+
+  if ( $session->{'pkgnum'} ) { 
+    #$return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
+    #next_bill_date from cust_pkg?
+    return { 'error' => 'No history for package' };
+  }
+
+  $return{balance} = $cust_main->balance;
+  $return{next_bill_date} = $cust_main->next_bill_date;
+  $return{next_bill_date_pretty} =
+    time2str('%m/%d/%Y', $return{next_bill_date} );
+
+  my @history = ();
+
+  my $conf = new FS::Conf;
+
+  if ( $conf->exists('selfservice-billing_history-line_items') ) {
+
+    foreach my $cust_bill ( $cust_main->cust_bill ) {
+
+      push @history, {
+        'type'        => 'Line item',
+        'description' => $_->desc. ( $_->sdate && $_->edate
+                                       ? ' '. time2str('%d-%b-%Y', $_->sdate).
+                                         ' To '. time2str('%d-%b-%Y', $_->edate)
+                                       : ''
+                                   ),
+        'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
+        'date'        => $cust_bill->_date,
+        'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
+      }
+        foreach $cust_bill->cust_bill_pkg;
+
+    }
+
+  } else {
+
+    push @history, {
+                     'type'        => 'Invoice',
+                     'description' => 'Invoice #'. $_->display_invnum,
+                     'amount'      => sprintf('%.2f', $_->charged ),
+                     'date'        => $_->_date,
+                     'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                   }
+      foreach $cust_main->cust_bill;
+
+  }
+
+  push @history, {
+                   'type'        => 'Payment',
+                   'description' => 'Payment', #XXX type
+                   'amount'      => sprintf('%.2f', 0 - $_->paid ),
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $cust_main->cust_pay;
+
+  push @history, {
+                   'type'        => 'Credit',
+                   'description' => 'Credit', #more info?
+                   'amount'      => sprintf('%.2f', 0 -$_->amount ),
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $cust_main->cust_credit;
+
+  push @history, {
+                   'type'        => 'Refund',
+                   'description' => 'Refund', #more info?  type, like payment?
+                   'amount'      => $_->refund,
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $cust_main->cust_refund;
+
+  @history = sort { $b->{'date'} <=> $a->{'date'} } @history;
+
+  $return{'history'} = \@history;
+
+  return \%return;
+
+}
+
 sub edit_info {
   my $p = shift;
   my $session = _cache->get($p->{'session_id'})
@@ -554,6 +692,12 @@ sub edit_info {
     or return { 'error' => "unknown custnum $custnum" };
 
   my $new = new FS::cust_main { $cust_main->hash };
+  # Avoid accidentally changing the service address.
+  if ( !$new->has_ship_address ) {
+    $new->set( $_ => $new->get($_) )
+      foreach $new->addr_fields;
+  }
+
   $new->set( $_ => $p->{$_} )
     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
 
@@ -582,7 +726,7 @@ sub edit_info {
     $p->{'payinfo1'} =~ /^([\dx]+)$/
       or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
     my $payinfo1 = $1;
-     $p->{'payinfo2'} =~ /^([\dx\.]+)$/ # . turned on by -require-bank-branch?
+     $p->{'payinfo2'} =~ /^([\dx\.]+)$/ # . turned on by echeck-country CA ?
       or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
     my $payinfo2 = $1;
     $payinfo = $payinfo1. '@'. $payinfo2;
@@ -713,7 +857,7 @@ sub payment_info {
     $return{payinfo2} = $payinfo2;
     $return{paytype}  = $cust_main->paytype;
     $return{paystate} = $cust_main->paystate;
-
+    $return{payname}  = $cust_main->payname;   # override 'first/last name' default from above, if any.  Is instution-name here.  (#15819)
   }
 
   if ( $conf->config('prepayment_discounts-credit_type') ) {
@@ -787,7 +931,7 @@ sub validate_payment {
    
     $payinfo = $p->{'payinfo'};
 
-    #more intelligent mathing will be needed here if you change
+    #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;
@@ -909,11 +1053,19 @@ sub do_process_payment {
 
   if ( $validate->{'save'} ) {
     my $new = new FS::cust_main { $cust_main->hash };
-    if ($validate->{'payby'} eq 'CARD' || $validate->{'payby'} eq 'DCRD') {
+    if ($payby eq 'CARD' || $payby eq 'DCRD') {
       $new->set( $_ => $validate->{$_} )
-        foreach qw( payname paystart_month paystart_year payissue payip
-                    address1 address2 city state zip country );
+        foreach qw( payname paystart_month paystart_year payissue payip );
       $new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' );
+
+      # Avoid accidentally changing the service address.
+      if ( !$new->has_ship_address ) {
+        $new->set( "ship_$_" => $new->get($_) ) 
+          foreach $new->addr_fields;
+      }
+      $new->set( $_ => $validate->{$_} )
+        foreach qw(address1 address2 city state country zip);
+
     } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
       $new->set( $_ => $validate->{$_} )
         foreach qw( payname payip paytype paystate
@@ -1138,7 +1290,63 @@ sub invoice_pdf {
 
   return { 'error'       => '',
            'invnum'      => $invnum,
-           'invoice_pdf' => $cust_bill->print_pdf( { unsquelch_cdr => 1 } ),
+           'invoice_pdf' => $cust_bill->print_pdf({
+                              'unsquelch_cdr' => 1,
+                              'locale'        => $p->{'locale'},
+                            }),
+         };
+
+}
+
+sub legacy_invoice {
+  my $p = shift;
+  my $session = _cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $legacyinvnum = $p->{'legacyinvnum'};
+
+  my %hash = (
+    'legacyinvnum' => $legacyinvnum,
+    'custnum'      => $custnum,
+  );
+
+  my $legacy_cust_bill =
+         qsearchs('legacy_cust_bill', { %hash, 'locale' => $p->{'locale'} } )
+      || qsearchs('legacy_cust_bill', \%hash )
+    or return { 'error' => "Can't find legacyinvnum" };
+
+  #my %return;
+
+  return { 'error'        => '',
+           'legacyinvnum' => $legacyinvnum,
+           'legacyid'     => $legacy_cust_bill->legacyid,
+           'invoice_html' => $legacy_cust_bill->content_html,
+         };
+
+}
+
+sub legacy_invoice_pdf {
+  my $p = shift;
+  my $session = _cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $legacyinvnum = $p->{'legacyinvnum'};
+
+  my $legacy_cust_bill = qsearchs('legacy_cust_bill', {
+    'legacyinvnum' => $legacyinvnum,
+    'custnum'      => $custnum,
+  }) or return { 'error' => "Can't find legacyinvnum" };
+
+  #my %return;
+
+  return { 'error'        => '',
+           'legacyinvnum' => $legacyinvnum,
+           'legacyid'     => $legacy_cust_bill->legacyid,
+           'invoice_pdf'  => $legacy_cust_bill->content_pdf,
          };
 
 }
@@ -1186,7 +1394,11 @@ sub list_invoices {
   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
     or return { 'error' => "unknown custnum $custnum" };
 
-  my @cust_bill = $cust_main->cust_bill;
+  my $conf = new FS::Conf;
+
+  my @legacy_cust_bill = $cust_main->legacy_cust_bill;
+
+  my @cust_bill = grep ! $_->hide, $cust_main->cust_bill;
 
   my $balance = 0;
 
@@ -1196,18 +1408,32 @@ sub list_invoices {
               map {
                     my $owed = $_->owed;
                     $balance += $owed;
-                    +{ 'invnum'     => $_->invnum,
-                       '_date'      => $_->_date,
-                       'date'       => time2str("%b %o, %Y", $_->_date),
-                       'date_short' => time2str("%m-%d-%Y",  $_->_date),
-                       'previous'   => sprintf('%.2f', ($_->previous)[0]),
-                       'charged'    => sprintf('%.2f', $_->charged),
-                       'owed'       => sprintf('%.2f', $owed),
-                       'balance'    => sprintf('%.2f', $balance),
+                    +{ 'invnum'       => $_->invnum,
+                       '_date'        => $_->_date,
+                       'date'         => time2str("%b %o, %Y", $_->_date),
+                       'date_short'   => time2str("%m-%d-%Y",  $_->_date),
+                       'previous'     => sprintf('%.2f', ($_->previous)[0]),
+                       'charged'      => sprintf('%.2f', $_->charged),
+                       'owed'         => sprintf('%.2f', $owed),
+                       'balance'      => sprintf('%.2f', $balance),
                      }
                   }
                   @cust_bill
-            ]
+            ],
+            'legacy_invoices' => [
+              map {
+                    +{ 'legacyinvnum' => $_->legacyinvnum,
+                       'legacyid'     => $_->legacyid,
+                       '_date'        => $_->_date,
+                       'date'         => time2str("%b %o, %Y", $_->_date),
+                       'date_short'   => time2str("%m-%d-%Y",  $_->_date),
+                       'charged'      => sprintf('%.2f', $_->charged),
+                       'has_content'  => (    length($_->content_pdf)
+                                           || length($_->content_html) ),
+                     }
+                  }
+                  @legacy_cust_bill
+            ],
           };
 }
 
@@ -1345,6 +1571,9 @@ sub list_svcs {
     next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
   }
+
+  @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
+
   if ( $p->{'svcdb'} ) {
     my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
                   ? $p->{'svcdb'}
@@ -1368,14 +1597,20 @@ sub list_svcs {
       map { 
             my $svc_x = $_->svc_x;
             my($label, $value) = $_->label;
-            my $svcdb = $_->part_svc->svcdb;
-            my $part_pkg = $_->cust_pkg->part_pkg;
+            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,
-              'svcdb'  => $svcdb,
-              'label'  => $label,
-              'value'  => $value,
+              'svcnum'         => $_->svcnum,
+              'display_svcnum' => $_->display_svcnum,
+              'svcdb'          => $svcdb,
+              'label'          => $label,
+              'value'          => $value,
+              'pkg_label'      => $cust_pkg->pkg_label,
+              'pkg_status'     => $cust_pkg->status,
+              'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
             );
 
             if ( $svcdb eq 'svc_acct' ) {
@@ -1425,7 +1660,8 @@ sub list_svcs {
 }
 
 sub _customer_svc_x {
-  my($custnum, $svcnum, $table) = @_;
+  my($custnum, $svcnum, $table) = (shift, shift, shift);
+  my $hashref = ref($svcnum) ? $svcnum : { 'svcnum' => $svcnum };
 
   $custnum =~ /^(\d+)$/ or die "illegal custnum";
   my $search = " AND custnum = $1";
@@ -1436,12 +1672,128 @@ sub _customer_svc_x {
     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) ',#.
                    #'LEFT JOIN cust_main USING ( custnum ) ',
-    'hashref'   => { 'svcnum' => $svcnum, },
+    'hashref'   => $hashref,
     'extra_sql' => $search, #important
   } );
 
 }
 
+sub svc_status_html {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  #XXX only svc_dsl for now
+  my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl')
+    or return { 'error' => "Service not found" };
+
+  my $html = $svc_x->getstatus_html;
+
+  return { 'html' => $html };
+
+}
+
+sub svc_status_hash {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  #XXX only svc_acct for now
+  my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct')
+    or return { 'error' => "Service not found" };
+
+  my ( $html, $hashref ) = $svc_x->export_getstatus;
+  return $hashref;
+
+}
+
+sub set_svc_status_hash {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  #XXX only svc_acct for now
+  my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct')
+    or return { 'error' => "Service not found" };
+
+  warn "set_svc_status_hash ". join(' / ', map "$_=>".$p->{$_}, keys %$p )
+    if $DEBUG;
+  my $error = $svc_x->export_setstatus($p); #$p? returns error?
+  return { 'error' => $error } if $error;
+
+  return {}; #? { 'error' => '' }
+
+}
+
+
+sub acct_forward_info {
+  my $p = shift;
+
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  my $svc_forward = _customer_svc_x( $custnum,
+                                     { 'srcsvc' => $p->{'svcnum'} },
+                                     'svc_forward',
+                                   )
+    or return { 'error' => '',
+                'dst'   => '',
+              };
+
+  return { 'error' => '',
+           'dst'   => $svc_forward->dst || $svc_forward->dstsvc_acct->email,
+         };
+
+}
+
+sub process_acct_forward {
+  my $p = shift;
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  my $old = _customer_svc_x( $custnum,
+                             { 'srcsvc' => $p->{'svcnum'} },
+                             'svc_forward',
+                           );
+
+  if ( $p->{'dst'} eq '' ) {
+    if ( $old ) {
+      my $error = $old->delete;
+      return { 'error' => $error };
+    }
+    return { 'error' => '' };
+  }
+
+  my $new = new FS::svc_forward { 'srcsvc' => $p->{'svcnum'},
+                                  'dst'    => $p->{'dst'},
+                                };
+
+  my $error;
+  if ( $old ) {
+    $new->svcnum($old->svcnum);
+    my $cust_svc = $old->cust_svc;
+    $new->svcpart($old->svcpart);
+    $new->pkgnuym($old->pkgnum);
+    $error = $new->replace($old);
+  } else {
+    my $conf = new FS::Conf;
+    $new->svcpart($conf->config('selfservice-svc_forward_svcpart'));
+
+    my $svc_acct = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct' )
+      or return { 'error' => 'No service' }; #how would we even get here?
+
+    $new->pkgnum( $svc_acct->cust_svc->pkgnum );
+
+    $error = $new->insert;
+  }
+
+  return { 'error' => $error };
+
+}
+
 sub list_dsl_devices {
   my $p = shift;
 
@@ -1555,9 +1907,10 @@ sub list_support_usage {
 }
 
 sub _list_cdr_usage {
-  my($svc_phone, $begin, $end) = @_;
-  map [ $_->downstream_csv('format' => 'default', 'keeparray' => 1) ], #XXX config for format
-                       $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
+  # XXX CDR type support...
+  my($svc_phone, $begin, $end, %opt) = @_;
+  map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
+    $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
 }
 
 sub list_cdr_usage {
@@ -1582,12 +1935,26 @@ sub _usage_details {
   return { 'error' => 'No service selected in list_svc_usage' } 
     unless $svc_x;
 
-  my $header = $svcdb eq 'svc_phone'
-                 ? [ split(',', FS::cdr::invoice_header('default') ) ]  #XXX
-                 : [];
-
   my $cust_pkg = $svc_x->cust_svc->cust_pkg;
   my $freq     = $cust_pkg->part_pkg->freq;
+  my %callback_opt;
+  my $header = [];
+  if ( $svcdb eq 'svc_phone' ) {
+    my $format   = $cust_pkg->part_pkg->option('output_format') || '';
+    $format = '' if $format =~ /^sum_/;
+    # sensible default if there is no format or it's a summary format
+    if ( $cust_pkg->part_pkg->plan eq 'voip_inbound' ) {
+      $format ||= 'source_default';
+      $callback_opt{inbound} = 1;
+    }
+    else {
+      $format ||= 'default';
+    }
+    
+    $callback_opt{format} = $format;
+    $header = [ split(',', FS::cdr::invoice_header($format) ) ];
+  }
+
   my $start    = $cust_pkg->setup;
   #my $end      = $cust_pkg->bill; # or time?
   my $end      = time;
@@ -1597,7 +1964,9 @@ sub _usage_details {
     $p->{ending}    = $end;
   }
 
-  my (@usage) = &$callback($svc_x, $p->{beginning}, $p->{ending});
+  my (@usage) = &$callback($svc_x, $p->{beginning}, $p->{ending}, 
+    %callback_opt
+  );
 
   #kinda false laziness with FS::cust_main::bill, but perhaps
   #we should really change this bit to DateTime and DateTime::Duration
@@ -2267,7 +2636,7 @@ sub myaccount_passwd {
       unless $svc_acct->check_password($p->{'old_password'});
   }
 
-  $svc_acct->_password($p->{'new_password'});
+  $svc_acct->set_password($p->{'new_password'});
   my $error = $svc_acct->replace();
 
   my($label, $value) = $svc_acct->cust_svc->label;
@@ -2332,12 +2701,93 @@ sub reset_passwd {
 
   }
 
-  #we're verified.  now what?
+  #okay, we're verified, now create a unique session
+
+  my $reset_session = {
+    'svcnum' => $svc_acct->svcnum,
+  };
+
+  my $timeout = '1 hour'; #?
+
+  my $reset_session_id;
+  do {
+    $reset_session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
+  } until ( ! defined _cache->get("reset_passwd_$reset_session_id") ); #just in case
+
+  _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
+
+  #email it
+
+  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
+  #die "selfservice-password_reset_msgnum unset" unless $msgnum;
+  return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
+  my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
+  my $error = $msg_template->send( 'cust_main'     => $cust_main,
+                                   'object'        => $svc_acct,
+                                   'substitutions' => {
+                                     'session_id' => $reset_session_id,
+                                   }
+                                 );
+  if ( $error ) {
+    return { 'error' => $error }; #????
+  }
 
   return { 'error' => '' };
 }
 
+sub check_reset_passwd {
+  my $p = shift;
+
+  my $conf = new FS::Conf;
+  my $verification = $conf->config('selfservice-password_reset_verification')
+    or return { 'error' => 'Password resets disabled' };
+
+  my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $svcnum = $reset_session->{'svcnum'};
+
+  my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+    or return { 'error' => "Service not found" };
+
+  return { 'error'    => '',
+           'username' => $svc_acct->username,
+         };
+
+}
+
+sub process_reset_passwd {
+  my $p = shift;
+
+  my $conf = new FS::Conf;
+  my $verification = $conf->config('selfservice-password_reset_verification')
+    or return { 'error' => 'Password resets disabled' };
+
+  return { 'error' => "New passwords don't match." }
+    if $p->{'new_password'} ne $p->{'new_password2'};
+
+  return { 'error' => 'Enter new password' }
+    unless length($p->{'new_password'});
+
+  my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $svcnum = $reset_session->{'svcnum'};
+
+  my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+    or return { 'error' => "Service not found" };
+
+  $svc_acct->set_password($p->{'new_password'});
+  my $error = $svc_acct->replace();
+
+  my($label, $value) = $svc_acct->cust_svc->label;
+
+  return { 'error' => $error,
+           #'label' => $label,
+           #'value' => $value,
+         };
+
+}
 
 sub create_ticket {
   my $p = shift;