RT# 83365 - Added city select to work like back end
[freeside.git] / fs_selfservice / FS-SelfService / SelfService.pm
index 06dcc2b..af989ed 100644 (file)
@@ -9,6 +9,7 @@ use FileHandle;
 #use IO::Handle;
 use IO::Select;
 use Storable 2.09 qw(nstore_fd fd_retrieve);
+use Time::HiRes;
 
 $VERSION = '0.03';
 
@@ -33,6 +34,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'switch_cust'               => 'MyAccount/switch_cust',
   'customer_info'             => 'MyAccount/customer_info',
   'customer_info_short'       => 'MyAccount/customer_info_short',
+  'customer_recurring'        => 'MyAccount/customer_recurring',
 
   'contact_passwd'            => 'MyAccount/contact/contact_passwd',
   'list_contacts'             => 'MyAccount/contact/list_contacts',
@@ -48,6 +50,12 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'legacy_invoice_pdf'        => 'MyAccount/legacy_invoice_pdf',
   'invoice_logo'              => 'MyAccount/invoice_logo',
   'list_invoices'             => 'MyAccount/list_invoices', #?
+  'list_payments'             => 'MyAccount/list_payments',
+  'payment_receipt'           => 'MyAccount/payment_receipt',
+  'list_payby'                => 'MyAccount/list_payby',
+  'insert_payby'              => 'MyAccount/insert_payby',
+  'update_payby'              => 'MyAccount/update_payby',
+  'delete_payby'              => 'MyAccount/delete_payby', 
   'cancel'                    => 'MyAccount/cancel',        #add to ss cgi!
   'payment_info'              => 'MyAccount/payment_info',
   'payment_info_renew_info'   => 'MyAccount/payment_info_renew_info',
@@ -60,6 +68,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'process_prepay'            => 'MyAccount/process_prepay',
   'realtime_collect'          => 'MyAccount/realtime_collect',
   'list_pkgs'                 => 'MyAccount/list_pkgs',     #add to ss (added?)
+  'pkg_info'                  => 'MyAccount/pkg_info',
   'list_svcs'                 => 'MyAccount/list_svcs',     #add to ss (added?)
   'list_svc_usage'            => 'MyAccount/list_svc_usage',   
   'svc_status_html'           => 'MyAccount/svc_status_html',
@@ -96,6 +105,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'reset_passwd'              => 'MyAccount/reset_passwd',
   'check_reset_passwd'        => 'MyAccount/check_reset_passwd',
   'process_reset_passwd'      => 'MyAccount/process_reset_passwd',
+  'validate_passwd'           => 'MyAccount/validate_passwd',
   'list_tickets'              => 'MyAccount/list_tickets',
   'create_ticket'             => 'MyAccount/create_ticket',
   'get_ticket'                => 'MyAccount/get_ticket',
@@ -108,6 +118,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'new_customer'              => 'Signup/new_customer',
   'new_customer_minimal'      => 'Signup/new_customer_minimal',
   'capture_payment'           => 'Signup/capture_payment',
+  'new_prospect'              => 'Signup/new_prospect',
   #N/A 'clear_signup_cache'        => 'Signup/clear_cache',
   'new_agent'                 => 'Agent/new_agent',
   'agent_login'               => 'Agent/agent_login',
@@ -134,6 +145,8 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'quotation_remove_pkg'      => 'MyAccount/quotation/quotation_remove_pkg',
   'quotation_order'           => 'MyAccount/quotation/quotation_order',
 
+  'freesideinc_service'       => 'Freeside/freesideinc_service',
+
 );
 @EXPORT_OK = (
   keys(%autoload),
@@ -186,28 +199,57 @@ sub simple_packet {
   my $packet = shift;
   warn "sending ". $packet->{_packet}. " to server"
     if $DEBUG;
-  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
-  connect(SOCK, sockaddr_un($socket)) or die "connect to $socket: $!";
-  nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
-  SOCK->flush;
 
-  #shoudl trap: Magic number checking on storable file failed at blib/lib/Storable.pm (autosplit into blib/lib/auto/Storable/fd_retrieve.al) line 337, at /usr/local/share/perl/5.6.1/FS/SelfService.pm line 71
+  # Retry socket operation 5 times per second
+  # until successful or $max_retry
+  my $max_retry = 25;
+  my $sock_timeout = 5;
+  my $enable_sock_timeout = 0;
 
-  #block until there is a message on socket
-#  my $w = new IO::Select;
-#  $w->add(\*SOCK);
-#  my @wait = $w->can_read;
+  for my $try ( 1..$max_retry ) {
+    local $@;
 
-  warn "reading message from server"
-    if $DEBUG;
+    my $return;
 
-  my $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
-  die $return->{'_error'} if defined $return->{_error} && $return->{_error};
+    eval {
+      local $SIG{ALRM} = sub{die "socket $socket: timeout ${sock_timeout}s"};
+      alarm $sock_timeout
+        if $enable_sock_timeout;
 
-  warn "returning message to client"
-    if $DEBUG;
+      socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+      connect(SOCK, sockaddr_un($socket)) or die "connect to $socket: $!";
+      nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
+      SOCK->flush;
 
-  $return;
+      # shoudl trap: Magic number checking on storable file failed at blib/lib/Storable.pm (autosplit into blib/lib/auto/Storable/fd_retrieve.al) line 337, at /usr/local/share/perl/5.6.1/FS/SelfService.pm line 71
+      # block until there is a message on socket
+      #  my $w = new IO::Select;
+      #  $w->add(\*SOCK);
+      #  my @wait = $w->can_read;
+
+      warn "reading message from server"
+        if $DEBUG;
+
+      $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
+      die $return->{'_error'} if defined $return->{_error} && $return->{_error};
+
+      warn "returning message to client"
+        if $DEBUG;
+
+      return $return;
+    };
+
+    return $return
+      unless $@;
+
+    die "(Attempt $try) $@"
+      if $try == $max_retry;
+
+    warn "(Attempt $try) $@"
+      if $DEBUG;
+
+    Time::HiRes::sleep(0.2);
+  }
 }
 
 =head1 NAME
@@ -263,7 +305,6 @@ FS::SelfService - Freeside self-service API
 
   my $customer_info = customer_info( { 'session_id' => $session_id } );
 
-  #payment_info and process_payment are available in 1.5+ only
   my $payment_info = payment_info( { 'session_id' => $session_id } );
 
   #!!! process_payment example
@@ -500,6 +541,31 @@ first last company address1 address2 city county state zip country daytime night
 
 =back
 
+=item customer_recurring HASHREF
+
+Takes a hash reference as parameter with a single key B<session_id>
+or keys B<agent_session_id> and B<custnum>.
+
+Returns a hash reference with the keys error, custnum and display_recurring.
+
+display_recurring is an arrayref of hashrefs with the following keys:
+
+=over 4
+
+=item freq
+
+frequency of charge, in months unless units are specified
+
+=item freq_pretty
+
+frequency of charge, suitable for display
+
+=item amount
+
+amount charged at this frequency
+
+=back
+
 =item edit_info HASHREF
 
 Takes a hash reference as parameter with any of the following keys:
@@ -567,6 +633,197 @@ Invoice date, in UNIX epoch time
 
 =back
 
+=item list_payments HASHREF
+
+Returns a list of all customer payments.  Takes a hash reference with a single
+key, session_id.
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item error
+
+Empty on success, or an error message on errors
+
+=item payments
+
+Reference to array of hash references with the following keys:
+
+=over 4
+
+=item paynum
+
+Payment #
+
+=item _date
+
+Payument date, in UNIX epoch time
+
+=item date
+
+Payment date, in a human-readable format
+
+=item date_short
+
+Payment date, in a shorter human-readable format
+
+=item paid
+
+Amount paid
+
+=item payby
+
+Payment method: CARD, CHEK (electronic check), or BILL (physical check).
+
+=item paycardtype
+
+Payment card type
+
+=item paymask
+
+Payment card mask
+
+=item processor
+
+Processor for cards and electronic checks
+
+=item auth
+
+Authorization number
+
+=item order_number
+
+Order number
+
+=back
+
+=back
+
+=item list_payby HASHREF
+
+Returns a list of all stored customer payment information (credit cards and
+electronic check accounts).  Takes a hash reference with a single key,
+session_id.
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item error
+
+Empty on success, or an error message on errors
+
+=item payby
+
+Reference to array of hash references with the following keys:
+
+=over 4
+
+=item custpaybynum
+
+=item weight
+
+Numeric weighting.  Stored payment information with a lower weight is attempted
+first.
+
+=item payby
+
+CARD (Automatic credit card), CHEK (Automatic electronic check), DCRD
+(on-demand credit card) or DCHK (on-demand electronic check).
+
+=item paymask
+
+Masked credit card number (or, masked account and routing numbers)
+
+=item paydate
+
+Credit card expiration date
+
+=item payname
+
+Exact name on card (or bank name, for electronic checks)
+
+=item paystate
+
+For electronic checks, bank state
+
+=item paytype
+
+For electronic checks, account type (Personal/Business, Checking/Savings)
+
+=back
+
+=back
+
+=item insert_payby HASHREF
+
+Adds new stored payment information for this customer.  Takes a hash reference
+with the following keys:
+
+=over 4
+
+=item session_id
+
+=item weight
+
+Numeric weighting.  Stored payment information with a lower weight is attempted
+first.
+
+=item payby
+
+CARD (Automatic credit card), CHEK (Automatic electronic check), DCRD
+(on-demand credit card) or DCHK (on-demand electronic check).
+
+=item payinfo
+
+Credit card number (or electronic check "account@routing")
+
+=item paycvv
+
+CVV2 number / security code
+
+=item paydate
+
+Credit card expiration date
+
+=item payname
+
+Exact name on card (or bank name, for electronic checks)
+
+=item paystate
+
+For electronic checks, bank state
+
+=item paytype
+
+For electronic checks, account type (i.e. "Personal Savings", "Personal Checking", "Business Checking")A
+
+=item payip
+
+Optional IP address from which payment was submitted
+
+=back
+
+If there is an error, returns a hash reference with a single key, B<error>,
+otherwise returns a hash reference with a single key, B<custpaybynum>.
+
+=item update_payby HASHREF
+
+Updates stored payment information.  Takes a hash reference with the same
+keys as insert_payby, as well as B<custpaybynum> to specify which record
+to update.  All keys except B<session_id> and B<custpaybynum> are optional;
+if omitted, the previous values in the record will be preserved.
+
+If there is an error, returns a hash reference with a single key, B<error>,
+otherwise returns a hash reference with a single key, B<custpaybynum>.
+
+=item delete_payby HASHREF
+
+Removes stored payment information.  Takes a hash reference with two keys,
+B<session_id> and B<custpaybynum>.  Returns a hash reference with a single key,
+B<error>, which is an error message or empty for successful removal.
+
 =item cancel HASHREF
 
 Cancels this customer.
@@ -580,7 +837,21 @@ success or an error message on errors.
 
 Returns information that may be useful in displaying a payment page.
 
-Takes a hash reference as parameter with a single key: B<session_id>.
+Takes a hash reference as parameter with the following keys:
+
+=over 4
+
+=item session_id
+
+Required session ID
+
+=item payment_payby
+
+=item omit_cust_main_county
+
+Optional, pass a true value to omit cust_main_county data for performance.
+
+=back
 
 Returns a hash reference with the following keys:
 
@@ -859,6 +1130,46 @@ Blank if the service is not over limit, or the date the service exceeded its usa
 
 =back
 
+=item pkg_info
+
+Returns package information for package.
+
+Takes a hash reference as parameter with the following keys:
+
+=over 4
+
+=item session_id
+
+Session identifier
+
+=item pkgnum
+
+Package Number
+
+=back
+
+Returns a hash reference containing customer package information.  The hash reference contains the following keys:
+
+=over 4
+
+=item pkg_label
+
+Name of this package
+
+=item pkgpart
+
+Part package primary key
+
+=item classnum
+
+Package class number
+
+=item error
+
+error message if errror.
+
+=back
+
 =item list_svcs
 
 Returns service information for this customer.
@@ -951,6 +1262,10 @@ Number of total bytes gained by recharge
 
 Orders a package for this customer.
 
+If signup_server-realtime is set, bills the new package, attemps to collect
+payment and (for auto-payment customers) cancels the package if the payment is
+declined.
+
 Takes a hash reference as parameter with the following keys:
 
 =over 4
@@ -967,6 +1282,11 @@ Package to order (see L<FS::part_pkg>).
 
 Quantity for this package order (default 1).
 
+=item run_bill_events
+
+If true, runs billing events for the customer after ordering and billing the
+package (signup_server-realtime must be set).
+
 =item locationnum
 
 Optional locationnum for this package order, for existing locations.
@@ -974,13 +1294,22 @@ Optional locationnum for this package order, for existing locations.
 Or, for new locations, pass the following fields: address1*, address2, city*,
 county, state*, zip*, country.  (* = required in this case)
 
+(None of this is required at all if you are just ordering a package
+at the customer's existing default service location.)
+
 =item address1
 
-=item address 2
+=item address2
 
 =item city
 
-=item 
+=item county
+
+=item state
+
+=item zip
+
+=item country
 
 =item svcpart
 
@@ -1140,7 +1469,7 @@ following keys:
 =item bill_date
 
 (Future) Bill date.  Indicates a future date for which billing could be run.
-Specified as a integer UNIX timestamp.  Pass this value to the B<order_renew>
+Specified as an integer UNIX timestamp.  Pass this value to the B<order_renew>
 function.
 
 =item bill_date_pretty
@@ -1156,7 +1485,7 @@ Base amount which will be charged if renewed early as of this date.
 
 Renewal date; i.e. even-futher future date at which the customer will be paid
 through if the early renewal is completed with the given B<bill-date>.
-Specified as a integer UNIX timestamp.
+Specified as an integer UNIX timestamp.
 
 =item renew_date_pretty
 
@@ -1217,6 +1546,11 @@ Session identifier
 
 pkgpart of package to cancel
 
+=item date
+
+Optional date, for future cancellation (expiration) instead of immediate
+cancellation.  Specified as an integer UNIX timestamp ("epoch time").
+
 =back
 
 Returns a hash reference with a single key, B<error>, empty on success, or an
@@ -1375,6 +1709,46 @@ error message, or empty on success.
 
 =item list_contacts
 
+Takes a hash reference as parameter with a single key, B<session_id>.
+
+Returns a hash reference with two parameters: B<error>, which contains an error
+message, or empty on success, and B<contacts>, a list of contacts.
+
+B<contacts> is an array reference of hash references (i.e. an array of structs,
+ in XML-RPC).  Each hash reference (struct) has the following keys:
+
+=over 4
+
+=item contactnum
+
+=item class
+
+Contact class name (contact type).
+
+=item first
+
+First name
+
+=item last
+
+Last name
+
+=item title
+
+Position ("Director of Silly Walks"), NOT honorific ("Mr." or "Mrs.")
+
+=item emailaddress
+
+Comma-separated list of email addresses
+
+=item comment
+
+=item selfservice_access
+
+Y when enabled
+
+=back
+
 =item edit_contact
 
 Updates information for the currently-logged in contact, or (optionally) the
@@ -1452,6 +1826,8 @@ Takes a hash reference as parameter with the following keys:
 Returns a hash reference with a single parameter, B<error>, which contains an
 error message, or empty on success.
 
+=back
+
 =head2 "MY ACCOUNT" QUOTATION FUNCTIONS
 
 All of these functions require the user to be logged in, and the 'session_id'
@@ -1595,8 +1971,6 @@ Removes a package from a quotation. Takes the following arguments:
 
 Returns 'error' => a string, which will be empty on success.
 
-=back
-
 =item quotation_order HASHREF
 
 Converts the packages in a quotation into real packages. Takes the following
@@ -1905,6 +2279,21 @@ State
 
 Zip or postal code
 
+=item ship_address1
+
+=item ship_address2
+
+=item ship_city
+
+=item ship_county
+
+=item ship_state
+
+=item ship_zip
+
+Optional shipping address fields.  If sending an optional shipping address,
+ship_address1, ship_city, ship_state and ship_zip are required.
+
 =item daytime
 
 Daytime phone number
@@ -2051,7 +2440,10 @@ sub regionselector {
 
   my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
 
-  my $countyflag = 0;
+  my $disabled = $param->{'disabled'};
+
+  my $countyflag = $param->{selected_county} ? 1 : 0;
+  my $cityflag = $param->{selected_city} ? 1 : 0;
 
   my %cust_main_county;
 
@@ -2061,17 +2453,17 @@ sub regionselector {
     foreach my $c ( @{ $param->{'locales'} } ) {
       #$countyflag=1 if $c->county;
       $countyflag=1 if $c->{county};
+      $cityflag=1 if ($c->{city} && $cityflag);
       #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
       #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
-      $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
+      $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}}{$c->{city}} = 1;
     }
 #  }
-  $countyflag=1 if $param->{selected_county};
 
   my $script_html = <<END;
     <SCRIPT>
-    function opt(what,value,text) {
-      var optionName = new Option(text, value, false, false);
+    function opt(what,value,text,selected) {
+      var optionName = new Option(text, value, false, selected);
       var length = what.length;
       what.options[length] = optionName;
     }
@@ -2111,8 +2503,37 @@ END
           #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
           foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
             my $text = $county || '(n/a)';
-            $script_html .=
-              qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
+            if (!$county) {
+              if ( $cityflag) {
+                $script_html .= qq!what.form.${prefix}city.style.display='';\n
+                                what.form.${prefix}city_select.style.display='none';\n!
+              }
+              $script_html .= qq!opt(what.form.${prefix}county, "$county", "$text");\n!
+              #$script_html .= qq!what.form.${prefix}county.style.display='none';\n!
+            }
+            else {
+              $script_html .= qq!var countySelected = false; if ("$param->{selected_county}" == "$text") { countySelected = true; }\n
+                              opt(what.form.${prefix}county, "$county", "$text", countySelected);\n
+                              what.form.${prefix}county.style.display='';\n
+                              county = what.form.${prefix}county.options[what.form.${prefix}county.selectedIndex].text;\n!;
+              if ( $cityflag) {
+                $script_html .= qq!\nif ( county == \"$county\" ) {\n!;
+                foreach my $city ( sort keys %{$cust_main_county{$country}{$state}{$county}} ) {
+                  my $text = $city || '(n/a)';
+                  if (!$city) {
+                    $script_html .= qq!what.form.${prefix}city.style.display='';\n
+                                    what.form.${prefix}city_select.style.display='none';\n!
+                  }
+                  else {
+                    $script_html .= qq!var citySelected = false; if ("$param->{selected_city}" == "$text") { citySelected = true; }\n
+                                    opt(what.form.${prefix}city_select, "$city", "$text", citySelected);\n
+                                    what.form.${prefix}city.style.display='none';\n
+                                    what.form.${prefix}city_select.style.display='';\n!
+                  }
+                }
+                $script_html .= "}\n";
+              }
+            }
           }
         $script_html .= "}\n";
       }
@@ -2122,12 +2543,89 @@ END
 
   $script_html .= <<END;
     }
+    function ${prefix}county_changed(what) {
+END
+
+  if ( $cityflag) {
+    $script_html .= <<END;
+      saved_city = "$param->{selected_city}";
+      county = what.options[what.selectedIndex].text;
+      state = what.form.${prefix}state.options[what.form.${prefix}state.selectedIndex].text;
+      country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
+      for ( var i = what.form.${prefix}city_select.length; i >= 0; i-- )
+          what.form.${prefix}city_select.options[i] = null;
+END
+
+    foreach my $country ( sort keys %cust_main_county ) {
+      $script_html .= "\nif ( country == \"$country\" ) {\n";
+      foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
+        $script_html .= "\nif ( state == \"$state\" ) {\n";
+        #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
+        foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
+          $script_html .= "\nif ( county == \"$county\" ) {\n";
+            foreach my $city ( sort keys %{$cust_main_county{$country}{$state}{$county}} ) {
+              my $text = $city || '(n/a)';
+              if (!$city) {
+                $script_html .= qq!what.form.${prefix}city.style.display='';\n
+                                what.form.${prefix}city_select.style.display='none';\n!
+              }
+              else {
+                $script_html .= qq!var citySelected = false; if (saved_city == "$text") { citySelected = true; }\n
+                                opt(what.form.${prefix}city_select, "$city", "$text", citySelected);\n
+                                what.form.${prefix}city.style.display='none';\n
+                                what.form.${prefix}city_select.style.display='';\n!
+              }
+            }
+          $script_html .= "}\n";
+        }
+        $script_html .= "}\n";
+      }
+      $script_html .= "}\n";
+    }
+  }
+
+  $script_html .= <<END;
+    }
+    function ${prefix}city_select_changed(what) {
+END
+
+  if ( $cityflag ) {
+    $script_html .= <<END;
+      what.form.${prefix}city.value = what.options[what.selectedIndex].value;
+END
+  }
+
+  $script_html .= <<END;
+    }
     </SCRIPT>
 END
 
+  my $city_html = '';
+  if ( $cityflag ) {
+    if ( scalar (keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}}{$param->{'selected_county'}} }) > 1 ) {
+      $city_html .= qq!<SELECT NAME="${prefix}city_select" onChange="${prefix}city_select_changed(this); $param->{'onchange'}">!;
+      foreach my $city (
+        sort keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}}{$param->{'selected_county'}} }
+      ) {
+        my $text = $city || '(n/a)';
+        $city_html .= qq!<OPTION VALUE="$city"!.
+                      ($city eq $param->{'selected_city'} ?
+                        ' SELECTED>' :
+                        '>'
+                      ).
+                      $text;
+      }
+      $city_html .= qq!</OPTION><INPUT TYPE="text" ID="${prefix}city" NAME="${prefix}city" VALUE="$param->{'selected_city'}" style="display:none">!;
+    } else {
+      $city_html .= qq!<SELECT NAME="${prefix}city_select" onChange="${prefix}city_select_changed(this); $param->{'onchange'}" style="display:none"></SELECT>
+                    <INPUT TYPE="text" ID="${prefix}city" NAME="${prefix}city" VALUE="$param->{'selected_city'}" style="display:''">!;
+    }
+  }
+
   my $county_html = $script_html;
   if ( $countyflag ) {
-    $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
+    $county_html .= qq!<SELECT NAME="${prefix}county" !.
+                    qq!onChange="${prefix}county_changed(this); $param->{'onchange'}">!;
     foreach my $county ( 
       sort keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}} }
     ) {
@@ -2181,7 +2679,7 @@ END
 
   }
 
-  ($county_html, $state_html, $country_html);
+  ($county_html, $state_html, $country_html, $city_html);
 
 }