RT#28842: NG portal - Changes for prepaid needs
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
index b2f38ea..4096344 100644 (file)
@@ -46,8 +46,11 @@ use FS::payby;
 use FS::acct_rt_transaction;
 use FS::msg_template;
 use FS::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 );
@@ -136,6 +139,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
 
@@ -308,11 +312,12 @@ sub login {
 
 sub logout {
   my $p = shift;
+  my $skin_info = skin_info($p);
   if ( $p->{'session_id'} ) {
     _cache->remove($p->{'session_id'});
-    return { %{ skin_info($p) }, 'error' => '' };
+    return { %$skin_info, 'error' => '' };
   } else {
-    return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message
+    return { %$skin_info, 'error' => "Can't resume session" }; #better error message
   }
 }
 
@@ -398,6 +403,8 @@ sub access_info {
 
   $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
 
+  $info->{'hide_usage'} = $conf->exists('selfservice_hide-usage');
+
   return { %$info,
            'custnum'       => $custnum,
            'access_pkgnum' => $session->{'pkgnum'},
@@ -747,6 +754,8 @@ sub edit_info {
     $payby = $1;
   }
 
+  my $conf = new FS::Conf;
+
   if ( $payby =~ /^(CARD|DCRD)$/ ) {
 
     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
@@ -759,6 +768,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;
@@ -835,8 +848,9 @@ sub payment_info {
 
       'card_types' => card_types(),
 
-      'withcvv'     => $conf->exists('selfservice-require_cvv'), #or enable optional cvv?
-      'require_cvv' => $conf->exists('selfservice-require_cvv'),
+      '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 ],
 
@@ -1025,6 +1039,8 @@ 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' };
     }
@@ -1119,6 +1135,7 @@ sub do_process_payment {
 
   my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
     'quiet'       => 1,
+    'manual'      => 1,
     'selfservice' => 1,
     'paynum_ref'  => \$paynum,
     %$validate,
@@ -1604,6 +1621,7 @@ sub list_pkgs {
     or return { 'error' => "unknown custnum $custnum" };
 
   my $conf = new FS::Conf;
+  my $immutable = $conf->exists('selfservice_immutable-package');
   
 # the duplication below is necessary:
 # 1. to maintain the current buggy behaviour wrt the cust_pkg and part_pkg
@@ -1616,6 +1634,7 @@ sub list_pkgs {
            'custnum'  => $custnum,
            'cust_pkg' => [ map {
                           { $_->hash,
+                           immutable => $immutable,
                             part_pkg => [ map $_->hashref, $_->part_pkg ],
                             part_svc =>
                               [ map $_->hashref, $_->available_part_svc ],
@@ -1648,6 +1667,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,
@@ -1697,6 +1717,9 @@ sub list_svcs {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
+  my $conf = new FS::Conf;
+
+  my $hide_usage = $conf->exists('selfservice_hide-usage') ? 1 : 0;
   my $search = { 'custnum' => $custnum };
   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
   my $cust_main = qsearchs('cust_main', $search )
@@ -1710,7 +1733,6 @@ sub list_svcs {
 
   my @cust_svc = ();
   my @cust_pkg_usage = ();
-  #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
   foreach my $cust_pkg ( $p->{'ncancelled'} 
                          ? $cust_main->ncancelled_pkgs
                          : $cust_main->unsuspended_pkgs ) {
@@ -1721,14 +1743,16 @@ sub list_svcs {
 
   @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
   my %usage_pools;
-  foreach (@cust_pkg_usage) {
-    my $part = $_->part_pkg_usage;
-    my $tag = $part->description . ($part->shared ? 1 : 0);
-    my $row = $usage_pools{$tag} 
-          ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
-    $row->[1] += sprintf('%.1f', $_->minutes); # minutes remaining
-    $row->[2] += $part->minutes; # minutes total
-  }
+  if (!$hide_usage) {
+    foreach (@cust_pkg_usage) {
+      my $part = $_->part_pkg_usage;
+      my $tag = $part->description . ($part->shared ? 1 : 0);
+      my $row = $usage_pools{$tag} 
+            ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
+      $row->[1] += sprintf('%.1f', $_->minutes); # minutes remaining
+      $row->[2] += $part->minutes; # minutes total
+    }
+  } # otherwise just leave them empty
 
   if ( $p->{'svcdb'} ) {
     my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
@@ -1742,108 +1766,112 @@ sub list_svcs {
   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
   #              @svc_x;
 
-    my $conf = new FS::Conf;
+  my @svcs; # stuff to return to the client
+  foreach my $cust_svc (@cust_svc) {
+    my $svc_x = $cust_svc->svc_x;
+    my($label, $value) = $cust_svc->label;
+    my $part_svc = $cust_svc->part_svc;
+    my $svcdb = $part_svc->svcdb;
+    my $cust_pkg = $cust_svc->cust_pkg;
+    my $part_pkg = $cust_pkg->part_pkg;
 
-  { 
+    my %hash = (
+      'svcnum'         => $cust_svc->svcnum,
+      'display_svcnum' => $cust_svc->display_svcnum,
+      'svcdb'          => $svcdb,
+      'label'          => $label,
+      'value'          => $value,
+      'pkg_label'      => $cust_pkg->pkg_locale,
+      'pkg_status'     => $cust_pkg->status,
+      'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
+    );
+
+    # would it make sense to put this in a svc_* method?
+
+    if ( $svcdb eq 'svc_acct' ) {
+      foreach (qw(username email finger seconds)) {
+        $hash{$_} = $svc_x->$_;
+      }
+
+      if (!$hide_usage) {
+        %hash = (
+          %hash,
+          'upbytes'    => display_bytecount($svc_x->upbytes),
+          'downbytes'  => display_bytecount($svc_x->downbytes),
+          'totalbytes' => display_bytecount($svc_x->totalbytes),
+
+          'recharge_amount'  => $part_pkg->option('recharge_amount',1),
+          'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
+          'recharge_upbytes'    =>
+            display_bytecount($part_pkg->option('recharge_upbytes',1)),
+          'recharge_downbytes'  =>
+            display_bytecount($part_pkg->option('recharge_downbytes',1)),
+          'recharge_totalbytes' =>
+            display_bytecount($part_pkg->option('recharge_totalbytes',1)),
+          # more...
+        );
+      }
+
+    } elsif ( $svcdb eq 'svc_dsl' ) {
+
+      $hash{'phonenum'} = $svc_x->phonenum;
+      if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
+        $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
+        $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
+          if $svc_x->company;
+      } else {
+        $hash{'name'} = $cust_main->name;
+      }
+      # no usage to hide here
+
+    } elsif ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
+      if (!$hide_usage) {
+        # could potentially show lots of things...
+        $hash{'outbound'} = 1;
+        $hash{'inbound'}  = 0;
+        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
+          # not be cdr_terminations at all.  have to go by date.
+          # find all since the last bill date.
+          # XXX cdr types?  we are going to need them.
+          if ( $hash{$_} ) {
+            my $sum_cdr = $svc_x->sum_cdrs(
+              'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
+              'begin'   => ($cust_pkg->last_bill || 0),
+              'nonzero' => 1,
+              'disable_charged_party' => 1,
+            );
+            $hash{$_} = $sum_cdr->hashref;
+          }
+        }
+      } # not hiding usage
+    } # svcdb
+
+    push @svcs, \%hash;
+  } # foreach $cust_svc
+
+  return { 
     'svcnum'   => $session->{'svcnum'},
     'custnum'  => $custnum,
     'date_format' => $conf->config('date_format') || '%m/%d/%Y',
     'view_usage_nodomain' => $conf->exists('selfservice-view_usage_nodomain'),
-    'svcs'     => [
-      map { 
-            my $svc_x = $_->svc_x;
-            my($label, $value) = $_->label;
-            my $part_svc = $_->part_svc;
-            my $svcdb = $part_svc->svcdb;
-            my $cust_pkg = $_->cust_pkg;
-            my $part_pkg = $cust_pkg->part_pkg;
-
-            my %hash = (
-              'svcnum'         => $_->svcnum,
-              'display_svcnum' => $_->display_svcnum,
-              'svcdb'          => $svcdb,
-              'label'          => $label,
-              'value'          => $value,
-              'pkg_label'      => $cust_pkg->pkg_locale,
-              'pkg_status'     => $cust_pkg->status,
-              'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
-            );
-
-            if ( $svcdb eq 'svc_acct' ) {
-              %hash = (
-                %hash,
-                'username'   => $svc_x->username,
-                'email'      => $svc_x->email,
-                'finger'     => $svc_x->finger,
-                'seconds'    => $svc_x->seconds,
-                'upbytes'    => display_bytecount($svc_x->upbytes),
-                'downbytes'  => display_bytecount($svc_x->downbytes),
-                'totalbytes' => display_bytecount($svc_x->totalbytes),
-
-                'recharge_amount'  => $part_pkg->option('recharge_amount',1),
-                'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
-                'recharge_upbytes'    =>
-                  display_bytecount($part_pkg->option('recharge_upbytes',1)),
-                'recharge_downbytes'  =>
-                  display_bytecount($part_pkg->option('recharge_downbytes',1)),
-                'recharge_totalbytes' =>
-                  display_bytecount($part_pkg->option('recharge_totalbytes',1)),
-                # more...
-              );
-
-            } elsif ( $svcdb eq 'svc_dsl' ) {
-              $hash{'phonenum'} = $svc_x->phonenum;
-              if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
-                $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
-                $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
-                  if $svc_x->company;
-              } else {
-                $hash{'name'} = $cust_main->name;
-              }
-            } elsif ( $svcdb eq 'svc_phone' ) {
-              # could potentially show lots of things...
-              $hash{'outbound'} = 1;
-              $hash{'inbound'}  = 0;
-              if ( $part_pkg->plan eq 'voip_inbound' ) {
-                $hash{'outbound'} = 0;
-                $hash{'inbound'}  = 1;
-              } elsif ( $part_pkg->option('selfservice_inbound_format')
-                    or  $conf->config('selfservice-default_inbound_cdr_format')
-              ) {
-                $hash{'inbound'}  = 1;
-              }
-              foreach (qw(inbound outbound)) {
-                # hmm...we can't filter by status here, because there might
-                # not be cdr_terminations at all.  have to go by date.
-                # find all since the last bill date.
-                # XXX cdr types?  we are going to need them.
-                if ( $hash{$_} ) {
-                  my $sum_cdr = $svc_x->sum_cdrs(
-                    'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
-                    'begin'   => ($cust_pkg->last_bill || 0),
-                    'nonzero' => 1,
-                    'disable_charged_party' => 1,
-                  );
-                  $hash{$_} = $sum_cdr->hashref;
-                }
-              }
-            }
-
-            # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) {
-            #  %hash = (
-            #    %hash,
-            #  );
-            #}
-
-            \%hash;
-          }
-          @cust_svc
-    ],
+    'svcs'     => \@svcs,
     'usage_pools' => [
       map { $usage_pools{$_} }
       sort { $a cmp $b }
       keys %usage_pools
     ],
+    'hide_usage' => $hide_usage,
   };
 
 }
@@ -2104,11 +2132,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
     );
@@ -2116,32 +2144,34 @@ 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 {
   my($callback, $p, %opt) = @_;
   my $conf = FS::Conf->new;
 
+  if ( $conf->exists('selfservice_hide-usage') ) {
+    return { 'error' => 'Viewing usage is not allowed.' };
+  }
+
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
   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') 
@@ -2242,11 +2272,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;
@@ -2305,11 +2347,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;
@@ -2340,6 +2383,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 )
@@ -2354,27 +2401,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);
 
-  my $conf = new FS::Conf;
   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 };
@@ -2588,11 +2634,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';
     
@@ -2609,8 +2656,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',
@@ -2620,19 +2667,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;
@@ -2724,9 +2771,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"