FS RT #30362 - added configuration option to disable package modification in selfservice
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index 88266da..0131d89 100644 (file)
@@ -7,6 +7,7 @@ use subs qw( _cache _provision );
 use IO::Scalar;
 use Data::Dumper;
 use Digest::MD5 qw(md5_hex);
+use Digest::SHA qw(sha512_hex);
 use Date::Format;
 use Time::Duration;
 use Time::Local qw(timelocal_nocheck);
@@ -44,6 +45,7 @@ use FS::cust_pkg;
 use FS::payby;
 use FS::acct_rt_transaction;
 use FS::msg_template;
+use FS::contact;
 
 $DEBUG = 1;
 $me = '[FS::ClientAPI::MyAccount]';
@@ -94,6 +96,7 @@ sub skin_info {
   } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) {
     $agentnum = $1;
   }
+  $p->{'agentnum'} = $agentnum;
 
   my $conf = new FS::Conf;
 
@@ -209,6 +212,7 @@ sub login {
   my $conf = new FS::Conf;
 
   my $svc_x = '';
+  my $session = {};
   if ( $p->{'domain'} eq 'svc_phone'
        && $conf->exists('selfservice_server-phone_login') ) { 
 
@@ -226,8 +230,19 @@ sub login {
 
     $svc_x = $svc_phone;
 
+  } elsif ( $p->{email}
+              && (my $contact = FS::contact->by_selfservice_email($p->{email}))
+          )
+  {
+    return { error => 'Incorrect contact password.' }
+      unless $contact->authenticate_password($p->{'password'});
+
+    $session->{'custnum'} = $contact->custnum;
+
   } else {
 
+    ( $p->{username}, $p->{domain} ) = split('@', $p->{email}) if $p->{email};
+
     my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
       or return { error => 'Domain '. $p->{'domain'}. ' not found' };
 
@@ -250,35 +265,37 @@ sub login {
 
   }
 
-  my $session = {
-    'svcnum' => $svc_x->svcnum,
-  };
+  if ( $svc_x ) {
+
+    $session->{'svcnum'} = $svc_x->svcnum;
 
-  my $cust_svc = $svc_x->cust_svc;
-  my $cust_pkg = $cust_svc->cust_pkg;
-  if ( $cust_pkg ) {
-    my $cust_main = $cust_pkg->cust_main;
-    $session->{'custnum'} = $cust_main->custnum;
-    if ( $conf->exists('pkg-balances') ) {
-      my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
-                          $cust_main->ncancelled_pkgs;
-      $session->{'pkgnum'} = $cust_pkg->pkgnum
-        if scalar(@cust_pkg) > 1;
+    my $cust_svc = $svc_x->cust_svc;
+    my $cust_pkg = $cust_svc->cust_pkg;
+    if ( $cust_pkg ) {
+      my $cust_main = $cust_pkg->cust_main;
+      $session->{'custnum'} = $cust_main->custnum;
+      if ( $conf->exists('pkg-balances') ) {
+        my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
+                            $cust_main->ncancelled_pkgs;
+        $session->{'pkgnum'} = $cust_pkg->pkgnum
+          if scalar(@cust_pkg) > 1;
+      }
     }
-  }
 
-  #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
-  #return { error => 'Only primary user may log in.' } 
-  #  if $conf->exists('selfservice_server-primary_only')
-  #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
-  my $part_pkg = $cust_pkg->part_pkg;
-  return { error => 'Only primary user may log in.' }
-    if $conf->exists('selfservice_server-primary_only')
-       && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+    #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
+    #return { error => 'Only primary user may log in.' } 
+    #  if $conf->exists('selfservice_server-primary_only')
+    #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
+    my $part_pkg = $cust_pkg->part_pkg;
+    return { error => 'Only primary user may log in.' }
+      if $conf->exists('selfservice_server-primary_only')
+         && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+
+  }
 
   my $session_id;
   do {
-    $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
+    $session_id = sha512_hex(time(). {}. rand(). $$)
   } until ( ! defined _cache->get($session_id) ); #just in case
 
   my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
@@ -379,6 +396,8 @@ sub access_info {
       $conf->exists('ticket_system-selfservice_edit_subject') && 
       $cust_main->edit_subject;
 
+  $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
+
   return { %$info,
            'custnum'       => $custnum,
            'access_pkgnum' => $session->{'pkgnum'},
@@ -536,7 +555,9 @@ sub customer_info_short {
                       1, ##nobalance
                     );
 
-    $return{name} = $cust_main->first. ' '. $cust_main->get('last');
+    $return{first}  = $cust_main->first;
+    $return{'last'} = $cust_main->get('last');
+    $return{name}   = $cust_main->first. ' '. $cust_main->get('last');
 
     $return{payby} = $cust_main->payby;
 
@@ -546,8 +567,10 @@ sub customer_info_short {
     }
     #maybe a little more expensive, but it should be cached by now
     for (@location_editable_fields) {
-      $return{$_} = $cust_main->bill_location->get($_);
-      $return{'ship_'.$_} = $cust_main->ship_location->get($_);
+      $return{$_} = $cust_main->bill_location->get($_)
+        if $cust_main->bill_locationnum;
+      $return{'ship_'.$_} = $cust_main->ship_location->get($_)
+        if $cust_main->ship_locationnum;
     }
  
     if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
@@ -728,6 +751,8 @@ sub edit_info {
     $payby = $1;
   }
 
+  my $conf = new FS::Conf;
+
   if ( $payby =~ /^(CARD|DCRD)$/ ) {
 
     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
@@ -740,6 +765,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;
@@ -816,6 +845,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') ],
@@ -830,7 +863,7 @@ sub payment_info {
 
       'save_unchecked' => $conf->exists('selfservice-save_unchecked'),
 
-      'credit_card_surcharge_percentage' => $conf->config('credit-card-surcharge-percentage'),
+      'credit_card_surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')),
     };
 
   }
@@ -888,7 +921,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,
@@ -938,10 +972,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'} };
@@ -967,10 +1007,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})$/
@@ -992,6 +1036,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 {
@@ -1009,7 +1057,7 @@ sub validate_payment {
 
   { 
     'cust_main'      => $cust_main, #XXX or just custnum??
-    'amount'         => $amount,
+    'amount'         => sprintf('%.2f', $amount),
     'payby'          => $payby,
     'payinfo'        => $payinfo,
     'paymask'        => $cust_main->mask_payinfo( $payby, $payinfo ),
@@ -1019,7 +1067,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,
@@ -1193,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,
     };
 
@@ -1252,6 +1299,50 @@ sub realtime_collect {
   return { 'error' => '', amount => $amount, %$error };
 }
 
+sub start_thirdparty {
+  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 $cust_main = FS::cust_main->by_key($custnum);
+  
+  my $amount = $p->{'amount'}
+    or return { error => 'no amount' };
+
+  my $result = $cust_main->create_payment(
+    'method'      => $p->{'method'},
+    'amount'      => $p->{'amount'},
+    'pkgnum'      => $session->{'pkgnum'},
+    'session_id'  => $p->{'session_id'},
+  );
+  
+  if ( ref($result) ) { # hashref or error
+    return $result;
+  } else {
+    return { error => $result };
+  }
+}
+
+sub finish_thirdparty {
+  my $p = shift;
+  my $session_id = delete $p->{'session_id'};
+  my $session = _cache->get($session_id)
+    or return { 'error' => "Can't resume session" };
+  my $custnum = $session->{'custnum'};
+  my $cust_main = FS::cust_main->by_key($custnum);
+
+  if ( $p->{_cancel} ) {
+    # customer backed out of making a payment
+    return $cust_main->cancel_payment( $session_id );
+  }
+  my $result = $cust_main->execute_payment( $session_id, %$p );
+  if ( ref($result) ) {
+    return $result;
+  } else {
+    return { error => $result };
+  }
+}
+
 sub process_payment_order_pkg {
   my $p = shift;
 
@@ -1526,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
@@ -1538,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 ],
@@ -1570,6 +1663,7 @@ sub list_pkgs {
                           my $primary_cust_svc = $_->primary_cust_svc;
                           +{ $_->hash,
                             $_->part_pkg->hash,
+                           immutable   => $immutable,
                             pkg_label   => $_->pkg_locale,
                             status      => $_->status,
                             statuscolor => $_->statuscolor,
@@ -1648,7 +1742,7 @@ sub list_svcs {
     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->[1] += sprintf('%.1f', $_->minutes); # minutes remaining
     $row->[2] += $part->minutes; # minutes total
   }
 
@@ -1744,6 +1838,7 @@ sub list_svcs {
                     'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
                     'begin'   => ($cust_pkg->last_bill || 0),
                     'nonzero' => 1,
+                    'disable_charged_party' => 1,
                   );
                   $hash{$_} = $sum_cdr->hashref;
                 }
@@ -2027,7 +2122,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 {
@@ -2039,6 +2139,7 @@ sub list_cdr_usage {
 
 sub _usage_details {
   my($callback, $p, %opt) = @_;
+  my $conf = FS::Conf->new;
 
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
@@ -2057,7 +2158,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') 
@@ -2091,6 +2191,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
   #
@@ -2248,6 +2356,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 )
@@ -2257,6 +2369,11 @@ sub change_pkg {
   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
     or return { 'error' => "unknown package $p->{pkgnum}" };
 
+  #if someone does need self-service package change of suspended packages,
+  # figure out how to be more discriminating
+  return { error=>"Can't change a suspended package", pkgnum=>$cust_pkg->pkgnum}
+    if $cust_pkg->status eq 'suspended';
+
   my @newpkg;
   my $error = FS::cust_pkg::order( $custnum,
                                    [$p->{pkgpart}],
@@ -2264,10 +2381,9 @@ sub change_pkg {
                                    \@newpkg,
                                  );
 
-  my $conf = new FS::Conf;
   if ( $conf->exists('signup_server-realtime') ) {
 
-    my $bill_error = _do_bop_realtime( $cust_main, $status );
+    my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_credit'=>1 );
 
     if ($bill_error) {
       $newpkg[0]->suspend;
@@ -2339,25 +2455,32 @@ sub order_recharge {
 }
 
 sub _do_bop_realtime {
-  my ($cust_main, $status) = (shift, shift);
+  my ($cust_main, $status, %opt) = @_;
 
     my $old_balance = $cust_main->balance;
 
     my $bill_error =    $cust_main->bill
-                     || $cust_main->apply_payments_and_credits
-                     || $cust_main->realtime_collect('selfservice' => 1);
+                     || $cust_main->apply_payments_and_credits;
+
+    $bill_error ||= $cust_main->realtime_collect('selfservice' => 1)
+      if $cust_main->payby =~ /^(CARD|CHEK)$/;
 
     if (    $cust_main->balance > $old_balance
          && $cust_main->balance > 0
-         && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ?
-              1 : $status eq 'suspended' ) ) {
-      #this makes sense.  credit is "un-doing" the invoice
-      my $conf = new FS::Conf;
-      $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
-                          'self-service decline',
-                          'reason_type' => $conf->config('signup_credit_type'),
-                        );
-      $cust_main->apply_credits( 'order' => 'newest' );
+         && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/
+                || $status eq 'suspended'
+            )
+       )
+    {
+      unless ( $opt{'no_credit'} ) {
+        #this makes sense.  credit is "un-doing" the invoice
+        my $conf = new FS::Conf;
+        $cust_main->credit( sprintf("%.2f", $cust_main->balance-$old_balance ),
+                            'self-service decline',
+                            reason_type=>$conf->config('signup_credit_type'),
+                          );
+        $cust_main->apply_credits( 'order' => 'newest' );
+      }
 
       return { 'error' => '_decline', 'bill_error' => $bill_error };
     }
@@ -2755,13 +2878,33 @@ sub myaccount_passwd {
   } )
     or return { 'error' => "Service not found" };
 
-  if ( exists($p->{'old_password'}) ) {
-    return { 'error' => "Incorrect password." }
-      unless $svc_acct->check_password($p->{'old_password'});
-  }
+  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.'
+    if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8);
 
   $svc_acct->set_password($p->{'new_password'});
-  my $error = $svc_acct->replace();
+  $error ||= $svc_acct->replace();
+
+  #regular pw change in self-service should change contact pw too, otherwise its
+  #way too confusing.  hell its confusing they're separate at all, but alas.
+  #need to support the "ISP provides email that's used as a contact email" case
+  #as well as we can.
+  my $contact = FS::contact->by_selfservice_email($svc_acct->email);
+  if ( $contact && $contact->custnum == $custnum ) {
+    #svc_acct was successful but this one returns an error?  "shouldn't happen"
+    $error ||= $contact->change_password($p->{'new_password'});
+  }
 
   my($label, $value) = $svc_acct->cust_svc->label;
 
@@ -2772,29 +2915,113 @@ sub myaccount_passwd {
 
 }
 
+#  sub contact_passwd {
+#    my $p = shift;
+#    my($context, $session, $custnum) = _custoragent_session_custnum($p);
+#    return { 'error' => $session } if $context eq 'error';
+#  
+#    return { 'error' => 'Not logged in as a contact.' }
+#      unless $session->{'contactnum'};
+#  
+#    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 $search = { 'custnum' => $custnum };
+#    #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
+#    $custnum =~ /^(\d+)$/ or die "illegal custnum";
+#    my $search = " AND selfservice_access IS NOT NULL ".
+#                 " AND selfservice_access = 'Y' ".
+#                 " AND ( disabled IS NULL OR disabled = '' )".
+#                 " AND custnum IS NOT NULL AND custnum = $1";
+#    $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
+#  
+#    my $contact = qsearchs( {
+#      'table'     => 'contact',
+#      'addl_from' => 'LEFT JOIN cust_main USING ( custnum ) ',
+#      'hashref'   => { 'contactnum' => $session->{'contactnum'}, },
+#      'extra_sql' => $search, #important
+#    } )
+#      or return { 'error' => "Email not found" }; #?  how did we get logged in?
+#                                                  # deleted since then?
+#  
+#    my $error = '';
+#  
+#    # use these svc_acct length restrictions??
+#    my $conf = new FS::Conf;
+#    $error = 'Password too short.'
+#      if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6);
+#    $error = 'Password too long.'
+#      if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8);
+#  
+#    $error ||= $contact->change_password($p->{'new_password'});
+#  
+#    return { 'error' => $error, };
+#  
+#  }
+
 sub reset_passwd {
   my $p = shift;
 
+  my $info = skin_info($p);
+
   my $conf = new FS::Conf;
   my $verification = $conf->config('selfservice-password_reset_verification')
-    or return { 'error' => 'Password resets disabled' };
+    or return { %$info, 'error' => 'Password resets disabled' };
 
-  my $username = $p->{'username'};
+  my $contact = '';
+  my $svc_acct = '';
+  my $cust_main = '';
+  if ( $p->{'email'} ) { #new-style, changes contact and svc_acct
+  
+    $contact = FS::contact->by_selfservice_email($p->{'email'});
+
+    $cust_main = $contact->cust_main if $contact;
+
+    #also look for an svc_acct, otherwise it would be super confusing
+
+    my($username, $domain) = split('@', $p->{'email'});
+    my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } );
+    if ( $svc_domain ) {
+      $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
+                                         'domsvc'   => $svc_domain->svcnum  }
+                          );
+      if ( $svc_acct ) {
+        my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+        $cust_main ||= $cust_pkg->cust_main if $cust_pkg;
+
+        #precaution: don't change svc_acct password not part of the same
+        # customer as contact
+        $svc_acct = '' if ! $cust_pkg
+                       || $cust_pkg->custnum != $cust_main->custnum;
+      }
+      
+    }
+
+    return { %$info, 'error' => 'Email address not found' }
+      unless $contact || $svc_acct;
 
-  my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
-    or return { 'error' => 'Account not found' };
+  } elsif ( $p->{'username'} ) { #old style, looks in svc_acct only
+
+    my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
+      or return { %$info, 'error' => 'Account not found' };
 
-  my $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
-                                        'domsvc'   => $svc_domain->svcnum  }
-                         )
-    or return { 'error' => 'Account not found' };
+    $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
+                                       'domsvc'   => $svc_domain->svcnum  }
+                        )
+      or return { %$info, 'error' => 'Account not found' };
 
-  my $cust_pkg = $svc_acct->cust_svc->cust_pkg
-    or return { 'error' => 'Account not found' };
+    my $cust_pkg = $svc_acct->cust_svc->cust_pkg
+      or return { %$info, 'error' => 'Account not found' };
 
-  my $cust_main = $cust_pkg->cust_main;
+    $cust_main = $cust_pkg->cust_main;
+
+  }
 
   my %verify = (
+    'email'   => sub { 1; },
     'paymask' => sub { 
       my( $p, $cust_main ) = @_;
       $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/
@@ -2821,42 +3048,62 @@ sub reset_passwd {
   foreach my $verify ( split(',', $verification) ) {
 
     &{ $verify{$verify} }( $p, $cust_main )
-      or return { 'error' => 'Account not found' };
+      or return { %$info, 'error' => 'Account not found' };
 
   }
 
-  #okay, we're verified, now create a unique session
+  #okay, we're verified
 
-  my $reset_session = {
-    'svcnum' => $svc_acct->svcnum,
-  };
+  if ( $contact ) {
 
-  my $timeout = '1 hour'; #?
+    my $error = $contact->send_reset_email(
+                            'svcnum' => ($svc_acct ? $svc_acct->svcnum : ''),
+                          );
+
+    if ( $error ) {
+      return { %$info, 'error' => $error }; #????
+    }
+
+  } elsif ( $svc_acct ) {
+
+    #create a unique session
+
+    my $reset_session = {
+      'svcnum'   => $svc_acct->svcnum,
+      'agentnum' =>
+    };
+
+    my $timeout = '1 hour'; #?
+
+    my $reset_session_id;
+    do {
+      $reset_session_id = sha512_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 { %$info, '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 { %$info, 'error' => $error }; #????
+    }
 
-  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' => '' };
+  return { %$info, 'error' => '' };
 }
 
 sub check_reset_passwd {
@@ -2869,14 +3116,46 @@ sub check_reset_passwd {
   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'};
+  if ( $reset_session->{'svcnum'} ) {
 
-  my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
-    or return { 'error' => "Service not found" };
+    my $svcnum = $reset_session->{'svcnum'};
 
-  return { 'error'    => '',
-           'username' => $svc_acct->username,
-         };
+    my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+      or return { 'error' => "Service not found" };
+
+    $p->{'agentnum'} = $svc_acct->cust_svc->cust_pkg->cust_main->agentnum;
+    my $info = skin_info($p);
+
+    return { %$info,
+             'error'      => '',
+             'session_id' => $p->{'session_id'},
+             'username'   => $svc_acct->username,
+           };
+
+  } elsif ( $reset_session->{'contactnum'} ) {
+
+    my $contactnum = $reset_session->{'contactnum'};
+
+    my $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+      or return { 'error' => "Contact not found" };
+
+    my @contact_email = $contact->contact_email;
+    return { 'error' => 'No contact email' } unless @contact_email;
+
+    $p->{'agentnum'} = $contact->cust_main->agentnum;
+    my $info = skin_info($p);
+
+    return { %$info,
+             'error'      => '',
+             'session_id' => $p->{'session_id'},
+             'email'      => $contact_email[0]->email, #the first?
+           };
+
+  } else {
+
+    return { 'error' => 'No svcnum or contactnum in session' }; #??
+
+  }
 
 }
 
@@ -2887,29 +3166,70 @@ sub process_reset_passwd {
   my $verification = $conf->config('selfservice-password_reset_verification')
     or return { 'error' => 'Password resets disabled' };
 
-  return { 'error' => "New passwords don't match." }
+  my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $info = '';
+
+  my $svc_acct = '';
+  if ( $reset_session->{'svcnum'} ) {
+
+    my $svcnum = $reset_session->{'svcnum'};
+
+    $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+      or return { 'error' => "Service not found" };
+
+    $p->{'agentnum'} ||= $svc_acct->cust_svc->cust_pkg->cust_main->agentnum;
+    $info ||= skin_info($p);
+
+  }
+
+  my $contact = '';
+  if ( $reset_session->{'contactnum'} ) {
+
+    my $contactnum = $reset_session->{'contactnum'};
+
+    $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+      or return { 'error' => "Contact not found" };
+
+    $p->{'agentnum'} ||= $contact->cust_main->agentnum;
+    $info ||= skin_info($p);
+
+  }
+
+  return { %$info, 'error' => "New passwords don't match." }
     if $p->{'new_password'} ne $p->{'new_password2'};
 
-  return { 'error' => 'Enter new password' }
+  return { %$info, '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
+  if ( $svc_acct ) {
 
-  my $svcnum = $reset_session->{'svcnum'};
+    $svc_acct->set_password($p->{'new_password'});
+    my $error = $svc_acct->replace();
 
-  my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
-    or return { 'error' => "Service not found" };
+    return { %$info, 'error' => $error } if $error;
 
-  $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,
+    #       };
 
-  my($label, $value) = $svc_acct->cust_svc->label;
+  }
 
-  return { 'error' => $error,
-           #'label' => $label,
-           #'value' => $value,
-         };
+  if ( $contact ) {
+
+    my $error = $contact->change_password($p->{'new_password'});
+
+    return { %$info, 'error' => $error }; # if $error;
+
+  }
+
+  #password changed ,so remove session, don't want it reused
+  _cache->remove($p->{'session_id'});
+
+  return { %$info, 'error' => '' };
 
 }
 
@@ -2964,8 +3284,8 @@ sub create_ticket {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
-#  warn "$me create_ticket: initializing ticket system\n" if $DEBUG;
-#  FS::TicketSystem->init();
+  warn "$me create_ticket: initializing ticket system\n" if $DEBUG;
+  FS::TicketSystem->init();
 
   my $conf = new FS::Conf;
   my $queue = $p->{'queue'}