add billing_history selfservice API call, RT#17617
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index 6e56d78..605d1ad 100644 (file)
@@ -115,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
@@ -151,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;
@@ -183,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' };
 
@@ -370,8 +385,12 @@ sub customer_info {
 
     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;
@@ -405,15 +424,21 @@ sub customer_info {
       $return{open_invoices} = \@open;
     }
 
+    $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($_);
     }
@@ -496,9 +521,11 @@ sub customer_info_short {
     my $cust_main = qsearchs('cust_main', $search )
       or return { 'error' => "unknown custnum $custnum" };
 
+    $return{countrydefault} = scalar($conf->config('countrydefault'));
+
     $return{small_custview} =
       small_custview( $cust_main,
-                      scalar($conf->config('countrydefault')),
+                      $return{countrydefault},
                       1, ##nobalance
                     );
 
@@ -545,6 +572,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'})
@@ -557,6 +681,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;
 
@@ -585,7 +715,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;
@@ -716,7 +846,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') ) {
@@ -790,7 +920,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;
@@ -912,11 +1042,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
@@ -1422,6 +1560,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'}
@@ -1445,7 +1586,8 @@ sub list_svcs {
       map { 
             my $svc_x = $_->svc_x;
             my($label, $value) = $_->label;
-            my $svcdb = $_->part_svc->svcdb;
+            my $part_svc = $_->part_svc;
+            my $svcdb = $part_svc->svcdb;
             my $cust_pkg = $_->cust_pkg;
             my $part_pkg = $cust_pkg->part_pkg;
 
@@ -1455,6 +1597,7 @@ sub list_svcs {
               'label'      => $label,
               'value'      => $value,
               'pkg_status' => $cust_pkg->status,
+              'readonly'   => ( $part_svc->selfservice_access eq 'readonly' ),
             );
 
             if ( $svcdb eq 'svc_acct' ) {
@@ -1538,6 +1681,41 @@ sub svc_status_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;
 
@@ -1560,7 +1738,6 @@ sub acct_forward_info {
 
 sub process_acct_forward {
   my $p = shift;
-
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
@@ -1584,8 +1761,19 @@ sub process_acct_forward {
   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;
   }
 
@@ -1706,9 +1894,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 {
@@ -1733,12 +1922,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;
@@ -1748,7 +1951,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
@@ -2418,7 +2623,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;
@@ -2559,7 +2764,7 @@ sub process_reset_passwd {
   my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
     or return { 'error' => "Service not found" };
 
-  $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;