RT#29169: Show payment information in selfservice portal
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index 8276d7e..cb6ac02 100644 (file)
@@ -46,8 +46,12 @@ use FS::payby;
 use FS::acct_rt_transaction;
 use FS::msg_template;
 use FS::contact;
+use FS::cust_contact;
+use FS::cust_location;
 
-$DEBUG = 1;
+use FS::ClientAPI::MyAccount::quotation; # just for code organization
+
+$DEBUG = 0;
 $me = '[FS::ClientAPI::MyAccount]';
 
 use vars qw( @cust_main_editable_fields @location_editable_fields );
@@ -82,7 +86,7 @@ sub skin_info {
   #return { 'error' => $session } if $context eq 'error';
 
   my $agentnum = '';
-  if ( $context eq 'customer' ) {
+  if ( $context eq 'customer' && $custnum ) {
 
     my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
       or die dbh->errstr;
@@ -136,6 +140,7 @@ 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 ) ),
+      'money_char' => $conf->config("money_char") || '$',
       'menu' => join("\n", $conf->config("ng_selfservice-menu", $agentnum ) ) ||
                 'main.php Home
 
@@ -237,7 +242,16 @@ sub login {
     return { error => 'Incorrect contact password.' }
       unless $contact->authenticate_password($p->{'password'});
 
-    $session->{'custnum'} = $contact->custnum;
+    my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+    if ( scalar(@cust_contact) == 1 ) {
+      $session->{'custnum'} = $cust_contact[0]->custnum;
+    } elsif ( scalar(@cust_contact) ) {
+      $session->{'customers'} = { map { $_->custnum => $_->cust_main->name }
+                                    @cust_contact
+                                };
+    } else {
+      return { error => 'No customer self-service access for contact' }; #??
+    }
 
   } else {
 
@@ -303,6 +317,7 @@ sub login {
 
   return { 'error'      => '',
            'session_id' => $session_id,
+           %$session,
          };
 }
 
@@ -336,6 +351,23 @@ sub switch_acct {
 
 }
 
+sub switch_cust {
+  my $p = shift;
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  $session->{'custnum'} = $p->{'custnum'}
+    if exists $session->{'customers'}{ $p->{'custnum'} };
+
+  my $conf = new FS::Conf;
+  my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
+  _cache->set( $p->{'session_id'}, $session, $timeout );
+
+  return { 'error'      => '',
+           %{ customer_info( { session_id=>$p->{'session_id'} } ) },
+         };
+}
+
 sub payment_gateway {
   # internal use only
   # takes a cust_main and a cust_payby entry, returns the payment_gateway
@@ -380,22 +412,23 @@ sub access_info {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
-  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
-    or return { 'error' => "unknown custnum $custnum" };
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
 
   $info->{'hide_payment_fields'} = [ 
     map { 
-      my $pg = payment_gateway($cust_main, $_);
+      my $pg = $cust_main && payment_gateway($cust_main, $_);
       $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
     } @{ $info->{cust_paybys} }
   ];
 
   $info->{'self_suspend_reason'} = 
-      $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum);
+      $conf->config('selfservice-self_suspend_reason',
+                      $cust_main ? $cust_main->agentnum : ''
+                   );
 
   $info->{'edit_ticket_subject'} =
       $conf->exists('ticket_system-selfservice_edit_subject') && 
-      $cust_main->edit_subject;
+      $cust_main && $cust_main->edit_subject;
 
   $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
 
@@ -432,7 +465,7 @@ sub customer_info {
     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" };
+      or return { 'error' => "customer_info: unknown custnum $custnum" };
 
     my $list_tickets = list_tickets($p);
     $return{'tickets'} = $list_tickets->{'tickets'};
@@ -440,11 +473,13 @@ sub customer_info {
     if ( $session->{'pkgnum'} ) {
       #XXX open invoices in the pkg-balances case
     } else {
+      $return{'money_char'} = $conf->config("money_char") || '$';
       my @open = map {
                        {
-                         invnum => $_->invnum,
-                         date   => time2str("%b %o, %Y", $_->_date),
-                         owed   => $_->owed,
+                         invnum     => $_->invnum,
+                         date       => time2str("%b %o, %Y", $_->_date),
+                         owed       => $_->owed,
+                         charged    => $_->charged,
                        };
                      } $cust_main->open_cust_bill;
       $return{open_invoices} = \@open;
@@ -536,7 +571,7 @@ sub customer_info_short {
     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" };
+      or return { 'error' => "customer_info_short: unknown custnum $custnum" };
 
     $return{display_custnum} = $cust_main->display_custnum;
 
@@ -705,6 +740,7 @@ sub billing_history {
   @history = sort { $b->{'date'} <=> $a->{'date'} } @history;
 
   $return{'history'} = \@history;
+  $return{'money_char'} = $conf->config("money_char") || '$',
 
   return \%return;
 
@@ -1556,25 +1592,31 @@ sub list_invoices {
   my @cust_bill = grep ! $_->hide, $cust_main->cust_bill;
 
   my $balance = 0;
+  my $invoices = [
+    map {
+      #not super efficient, we also run cust_bill_pay/cust_credited inside owed
+      my @payments_and_credits = sort {$b->_date <=> $a->_date} ($_->cust_bill_pay,$_->cust_credited);
+      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),
+         'lastpay'      => @payments_and_credits 
+                           ? time2str("%b %o, %Y", $payments_and_credits[0]->_date)
+                           : '',
+      }
+    } @cust_bill
+  ];
 
   return  { 'error'       => '',
             'balance'     => $cust_main->balance,
-            '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),
-                     }
-                  }
-                  @cust_bill
-            ],
+            'money_char'  => $conf->config("money_char") || '$',
+            'invoices'    => $invoices,
             'legacy_invoices' => [
               map {
                     +{ 'legacyinvnum' => $_->legacyinvnum,
@@ -1825,18 +1867,20 @@ sub list_svcs {
       }
       # no usage to hide here
 
-    } elsif ( $svcdb eq 'svc_phone' ) {
+    } elsif ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
       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;
+        if ( $svcdb eq 'svc_phone' ) {
+          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
@@ -2131,11 +2175,11 @@ sub _list_cdr_usage {
   # XXX CDR type support...
   # XXX any way to do a paged search on this?
   # we have to return the results all at once...
-  my($svc_phone, $begin, $end, %opt) = @_;
+  my($svc_x, $begin, $end, %opt) = @_;
   map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
-    $svc_phone->get_cdrs(
-      'begin'=>$begin,
-      'end'=>$end,
+    $svc_x->get_cdrs(
+      'begin' => $begin,
+      'end'   => $end,
       'disable_charged_party' => 1,
       %opt
     );
@@ -2143,9 +2187,7 @@ sub _list_cdr_usage {
 
 sub list_cdr_usage {
   my $p = shift;
-  _usage_details( \&_list_cdr_usage, $p,
-                  'svcdb' => 'svc_phone',
-                );
+  _usage_details( \&_list_cdr_usage, $p );
 }
 
 sub _usage_details {
@@ -2162,17 +2204,17 @@ sub _usage_details {
   my $search = { 'svcnum' => $p->{'svcnum'} };
   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
 
-  my $svcdb = $opt{'svcdb'} || 'svc_acct';
-
-  my $svc_x = qsearchs( $svcdb, $search );
+  my $cust_svc = qsearchs( 'cust_svc', $search );
   return { 'error' => 'No service selected in list_svc_usage' } 
-    unless $svc_x;
+    unless $cust_svc;
 
-  my $cust_pkg = $svc_x->cust_svc->cust_pkg;
+  my $svc_x = $cust_svc->svc_x;
+  my $svcdb = $svc_x->table;
+  my $cust_pkg = $cust_svc->cust_pkg;
   my $freq     = $cust_pkg->part_pkg->freq;
   my %callback_opt;
   my $header = [];
-  if ( $svcdb eq 'svc_phone' ) {
+  if ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
     my $format = '';
     if ( $p->{inbound} ) {
       $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') 
@@ -2273,11 +2315,23 @@ sub order_pkg {
     or return { 'error' => "unknown custnum $custnum" };
 
   my $status = $cust_main->status;
+
+  my %order_pkg_options = ();
+  if ( $p->{locationnum} > 0 ) {
+    $order_pkg_options{locationnum} = delete($p->{locationnum});
+  } elsif ( $p->{address1} ) {
+    $order_pkg_options{'cust_location'} = new FS::cust_location {
+      map { $_ => $p->{$_} }
+        qw( address1 address2 city county state zip country )
+    };
+  }
+
   #false laziness w/ClientAPI/Signup.pm
 
   my $cust_pkg = new FS::cust_pkg ( {
-    'custnum' => $custnum,
-    'pkgpart' => $p->{'pkgpart'},
+    'custnum'  => $custnum,
+    'pkgpart'  => $p->{'pkgpart'},
+    'quantity' => $p->{'quantity'} || 1,
   } );
   my $error = $cust_pkg->check;
   return { 'error' => $error } if $error;
@@ -2336,11 +2390,12 @@ sub order_pkg {
 
   }
 
-  use Tie::RefHash;
-  tie my %hash, 'Tie::RefHash';
-  %hash = ( $cust_pkg => \@svc );
-  #msgcat
-  $error = $cust_main->order_pkgs( \%hash, 'noexport' => 1 );
+  $error = $cust_main->order_pkg(
+    'cust_pkg' => $cust_pkg,
+    'svcs'     => \@svc,
+    'noexport' => 1,
+    %order_pkg_options,
+  );
   return { 'error' => $error } if $error;
 
   my $conf = new FS::Conf;
@@ -2389,26 +2444,26 @@ sub change_pkg {
   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}],
-                                   [$p->{pkgnum}],
-                                   \@newpkg,
-                                 );
+  my $err_or_cust_pkg = $cust_pkg->change( 'pkgpart'  => $p->{'pkgpart'},
+                                           'quantity' => $p->{'quantity'} || 1,
+                                         );
+
+  return { error=>$err_or_cust_pkg, pkgnum=>$cust_pkg->pkgnum }
+    unless ref($err_or_cust_pkg);
 
   if ( $conf->exists('signup_server-realtime') ) {
 
     my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_credit'=>1 );
 
     if ($bill_error) {
-      $newpkg[0]->suspend;
+      $err_or_cust_pkg->suspend;
       return $bill_error;
     } else {
-      $newpkg[0]->reexport;
+      $err_or_cust_pkg->reexport;
     }
 
   } else {  
-    $newpkg[0]->reexport;
+    $err_or_cust_pkg->reexport;
   }
 
   return { error => '', pkgnum => $cust_pkg->pkgnum };
@@ -2622,11 +2677,12 @@ sub cancel_pkg {
 }
 
 sub provision_phone {
- my $p = shift;
- my @bulkdid;
- @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
 my $p = shift;
 my @bulkdid;
 @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
 
- if($p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/){
+  #editing an existing phone number
+  if ( $p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/ ) {
       my($context, $session, $custnum) = _custoragent_session_custnum($p);
       return { 'error' => $session } if $context eq 'error';
     
@@ -2643,8 +2699,8 @@ sub provision_phone {
       return { 'error' => $svc_phone->replace };
  }
 
-# single DID LNP
unless($p->{'lnp'}) {
+  # single DID LNP
 unless ( $p->{'lnp'} ) {
     $p->{'lnp_desired_due_date'} = parse_datetime($p->{'lnp_desired_due_date'});
     $p->{'lnp_status'} = "portingin";
     return _provision( 'FS::svc_phone',
@@ -2654,19 +2710,19 @@ sub provision_phone {
                  $p,
                  @_
                );
- }
 }
 
-# single DID order
- unless (scalar(@bulkdid)) {
+  # single DID order (the usual case)
 unless (scalar(@bulkdid)) {
     return _provision( 'FS::svc_phone',
                  [qw(phonenum countrycode)],
                  [qw(phonenum countrycode)],
                  $p,
                  @_
                );
- }
 }
 
-# bulk DID order case
+  # bulk DID order case
   my $error;
   foreach my $did ( @bulkdid ) {
     $did =~ s/[^0-9]//g;
@@ -2686,6 +2742,21 @@ sub provision_phone {
   { 'bulkdid' => [ @bulkdid ], 'svc' => $error->{'svc'} }
 }
 
+sub provision_pbx {
+  my $p = shift;
+  warn "provision_pbx called\n"
+    if $DEBUG;
+
+  warn "provision_pbx calling _provision\n"
+    if $DEBUG;
+  _provision( 'FS::svc_pbx',
+              [qw(id title max_extensions max_simultaneous ip_addr)],
+              [qw(id title max_extensions max_simultaneous ip_addr)],
+              $p,
+              @_
+            );
+}
+
 sub provision_acct {
   my $p = shift;
   warn "provision_acct called\n"
@@ -2724,6 +2795,15 @@ sub provision_external {
             );
 }
 
+sub provision_forward {
+  my $p = shift;
+  _provision( 'FS::svc_forward',
+              ['srcsvc','src','dstsvc','dst'],
+              [],
+              $p,
+            );
+}
+
 sub _provision {
   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
   warn "_provision called for $class\n"
@@ -2751,6 +2831,9 @@ sub _provision {
   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
 
+  return { error=> 'svcpart '. $p->{'svcpart'}. " is not a $class definition" }
+    if $class ne 'FS::'. $part_svc->svcdb;
+
   warn "creating $class record\n"
     if $DEBUG;
   my $svc_x = $class->new( {
@@ -2758,9 +2841,21 @@ sub _provision {
     'svcpart' => $p->{'svcpart'},
     map { $_ => $p->{$_} } @$fields
   } );
+
+  my %insert_args = ();
+  #i shouldn't be a special case here (pass an option or something)
+  if ( $class eq 'FS::svc_phone'
+         && grep length($p->{$_}), @location_editable_fields
+     )
+  {
+    $insert_args{'cust_location'} = new FS::cust_location {
+      map { $_ => $p->{$_} } @location_editable_fields
+    };
+  }
+
   warn "inserting $class record\n"
     if $DEBUG;
-  my $error = $svc_x->insert;
+  my $error = $svc_x->insert(%insert_args);
 
   unless ( $error ) {
     warn "finding inserted record for svcnum ". $svc_x->svcnum. "\n"
@@ -2834,6 +2929,10 @@ sub part_svc_info {
         }
   }
 
+  if ($ret->{'svcdb'} eq 'svc_forward') {
+    $ret->{'forward_emails'} = {$cust_pkg->forward_emails()};
+  }
+
   $ret;
 }
 
@@ -2916,7 +3015,12 @@ sub myaccount_passwd {
   #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 ) {
+  if ( $contact && qsearchs('cust_contact', { contactnum=> $contact->contactnum,
+                                              custnum   => $custnum,
+                                              selfservice_access => 'Y',
+                                            }
+                           )
+  ) {
     #svc_acct was successful but this one returns an error?  "shouldn't happen"
     $error ||= $contact->change_password($p->{'new_password'});
   }
@@ -2993,7 +3097,10 @@ sub reset_passwd {
   
     $contact = FS::contact->by_selfservice_email($p->{'email'});
 
-    $cust_main = $contact->cust_main if $contact;
+    if ( $contact ) {
+      my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+      $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
+    }
 
     #also look for an svc_acct, otherwise it would be super confusing
 
@@ -3035,6 +3142,9 @@ sub reset_passwd {
 
   }
 
+  return { %$info, 'error' => 'Multi-customer contacts incompatible with customer-based verification' }
+    if ! $cust_main && $verification ne 'email';
+
   my %verify = (
     'email'   => sub { 1; },
     'paymask' => sub { 
@@ -3157,7 +3267,9 @@ sub check_reset_passwd {
     my @contact_email = $contact->contact_email;
     return { 'error' => 'No contact email' } unless @contact_email;
 
-    $p->{'agentnum'} = $contact->cust_main->agentnum;
+    my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+    $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+      if scalar(@cust_contact) == 1;
     my $info = skin_info($p);
 
     return { %$info,
@@ -3207,7 +3319,9 @@ sub process_reset_passwd {
     $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
       or return { 'error' => "Contact not found" };
 
-    $p->{'agentnum'} ||= $contact->cust_main->agentnum;
+    my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+    $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+      if scalar(@cust_contact) == 1;
     $info ||= skin_info($p);
 
   }