1 package FS::SelfService;
4 use vars qw($VERSION @ISA @EXPORT_OK $socket %autoload $tag);
10 use Storable 2.09 qw(nstore_fd fd_retrieve);
14 @ISA = qw( Exporter );
16 $socket = "/usr/local/freeside/selfservice_socket";
17 $socket .= '.'.$tag if defined $tag && length($tag);
19 #maybe should ask ClientAPI for this list
21 'passwd' => 'passwd/passwd',
22 'chfn' => 'passwd/passwd',
23 'chsh' => 'passwd/passwd',
24 'login' => 'MyAccount/login',
25 'logout' => 'MyAccount/logout',
26 'customer_info' => 'MyAccount/customer_info',
27 'edit_info' => 'MyAccount/edit_info', #add to ss cgi!
28 'invoice' => 'MyAccount/invoice',
29 'list_invoices' => 'MyAccount/list_invoices', #?
30 'cancel' => 'MyAccount/cancel', #add to ss cgi!
31 'payment_info' => 'MyAccount/payment_info',
32 'process_payment' => 'MyAccount/process_payment',
33 'list_pkgs' => 'MyAccount/list_pkgs', #add to ss cgi!
34 'order_pkg' => 'MyAccount/order_pkg', #add to ss cgi!
35 'cancel_pkg' => 'MyAccount/cancel_pkg', #add to ss cgi!
36 'charge' => 'MyAccount/charge', #?
37 'part_svc_info' => 'MyAccount/part_svc_info',
38 'provision_acct' => 'MyAccount/provision_acct',
39 'provision_external' => 'MyAccount/provision_external',
40 'unprovision_svc' => 'MyAccount/unprovision_svc',
41 'signup_info' => 'Signup/signup_info',
42 'new_customer' => 'Signup/new_customer',
43 'agent_login' => 'Agent/agent_login',
44 'agent_logout' => 'Agent/agent_logout',
45 'agent_info' => 'Agent/agent_info',
46 'agent_list_customers' => 'Agent/agent_list_customers',
48 @EXPORT_OK = ( keys(%autoload), qw( regionselector expselect popselector ) );
50 $ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
51 $ENV{'SHELL'} = '/bin/sh';
52 $ENV{'IFS'} = " \t\n";
55 $ENV{'BASH_ENV'} = '';
57 my $freeside_uid = scalar(getpwnam('freeside'));
58 die "not running as the freeside user\n" if $> != $freeside_uid;
60 foreach my $autoload ( keys %autoload ) {
71 $param->{_packet} = \''. $autoload{$autoload}. '\';
73 simple_packet($param);
83 socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
84 connect(SOCK, sockaddr_un($socket)) or die "connect: $!";
85 nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
88 #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
90 #block until there is a message on socket
91 # my $w = new IO::Select;
93 # my @wait = $w->can_read;
94 my $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
95 die $return->{'_error'} if defined $return->{_error} && $return->{_error};
102 FS::SelfService - Freeside self-service API
106 # password and shell account changes
107 use FS::SelfService qw(passwd chfn chsh);
109 # "my account" functionality
110 use FS::SelfService qw( login customer_info invoice cancel payment_info process_payment );
112 my $rv = login( { 'username' => $username,
114 'password' => $password,
118 if ( $rv->{'error'} ) {
119 #handle login error...
122 my $session_id = $rv->{'session_id'};
125 my $customer_info = customer_info( { 'session_id' => $session_id } );
127 #payment_info and process_payment are available in 1.5+ only
128 my $payment_info = payment_info( { 'session_id' => $session_id } );
130 #!!! process_payment example
132 #!!! list_pkgs example
134 #!!! order_pkg example
136 #!!! cancel_pkg example
138 # signup functionality
139 use FS::SelfService qw( signup_info new_customer );
141 my $signup_info = signup_info;
143 $rv = new_customer( {
146 'company' => $company,
147 'address1' => $address1,
148 'address2' => $address2,
152 'country' => $country,
153 'daytime' => $daytime,
157 'payinfo' => $payinfo,
159 'paydate' => $paydate,
160 'payname' => $payname,
161 'invoicing_list' => $invoicing_list,
162 'referral_custnum' => $referral_custnum,
163 'pkgpart' => $pkgpart,
164 'username' => $username,
165 '_password' => $password,
167 'agentnum' => $agentnum,
171 my $error = $rv->{'error'};
172 if ( $error eq '_decline' ) {
182 Use this API to implement your own client "self-service" module.
184 If you just want to customize the look of the existing "self-service" module,
187 =head1 PASSWORD, GECOS, SHELL CHANGING FUNCTIONS
199 =head1 "MY ACCOUNT" FUNCTIONS
205 Creates a user session. Takes a hash reference as parameter with the
218 Returns a hash reference with the following keys:
224 Empty on success, or an error message on errors.
228 Session identifier for successful logins
232 =item customer_info HASHREF
234 Returns general customer information.
236 Takes a hash reference as parameter with a single key: B<session_id>
238 Returns a hash reference with the following keys:
252 Array reference of hash references of open inoices. Each hash reference has
253 the following keys: invnum, date, owed
257 An HTML fragment containing shipping and billing addresses.
259 =item The following fields are also returned: first last company address1 address2 city county state zip country daytime night fax ship_first ship_last ship_company ship_address1 ship_address2 ship_city ship_state ship_zip ship_country ship_daytime ship_night ship_fax payby payinfo payname month year invoicing_list postal_invoicing
263 =item edit_info HASHREF
265 Takes a hash reference as parameter with any of the following keys:
267 first last company address1 address2 city county state zip country daytime night fax ship_first ship_last ship_company ship_address1 ship_address2 ship_city ship_state ship_zip ship_country ship_daytime ship_night ship_fax payby payinfo paycvv payname month year invoicing_list postal_invoicing
269 If a field exists, the customer record is updated with the new value of that
270 field. If a field does not exist, that field is not changed on the customer
273 Returns a hash reference with a single key, B<error>, empty on success, or an
274 error message on errors
276 =item invoice HASHREF
278 Returns an invoice. Takes a hash reference as parameter with two keys:
279 session_id and invnum
281 Returns a hash reference with the following keys:
287 Empty on success, or an error message on errors
299 =item list_invoices HASHREF
301 Returns a list of all customer invoices. Takes a hash references with a single
304 Returns a hash reference with the following keys:
310 Empty on success, or an error message on errors
314 Reference to array of hash references with the following keys:
324 Invoice date, in UNIX epoch time
332 Cancels this customer.
334 Takes a hash reference as parameter with a single key: B<session_id>
336 Returns a hash reference with a single key, B<error>, which is empty on
337 success or an error message on errors.
339 =item payment_info HASHREF
341 Returns information that may be useful in displaying a payment page.
343 Takes a hash reference as parameter with a single key: B<session_id>.
345 Returns a hash reference with the following keys:
351 Empty on success, or an error message on errors
359 Exact name on credit card (CARD/DCRD)
373 Customer's current default payment type.
377 For CARD/DCRD payment types, the card type (Visa card, MasterCard, Discover card, American Express card, etc.)
381 For CARD/DCRD payment types, the card number
385 For CARD/DCRD payment types, expiration month
389 For CARD/DCRD payment types, expiration year
391 =item cust_main_county
393 County/state/country data - array reference of hash references, each of which has the fields of a cust_main_county record (see L<FS::cust_main_county>). Note these are not FS::cust_main_county objects, but hash references of columns and values.
397 Array reference of all states in the current default country.
401 Hash reference of card types; keys are card types, values are the exact strings
402 passed to the process_payment function
406 Unique transaction identifier (prevents multiple charges), passed to the
407 process_payment function
411 =item process_payment HASHREF
413 Processes a payment and possible change of address or payment type. Takes a
414 hash reference as parameter with the following keys:
422 If true, address and card information entered will be saved for subsequent
427 If true, future credit card payments will be done automatically (sets payby to
428 CARD). If false, future credit card payments will be done on-demand (sets
429 payby to DCRD). This option only has meaning if B<save> is set true.
449 Card expiration month
457 Unique transaction identifier, returned from the payment_info function.
458 Prevents multiple charges.
462 Returns a hash reference with a single key, B<error>, empty on success, or an
463 error message on errors
467 Returns package information for this customer.
469 Takes a hash reference as parameter with a single key: B<session_id>
471 Returns a hash reference containing customer package information. The hash reference contains the following keys:
476 =item cust_pkg HASHREF
478 Array reference of hash references, each of which has the fields of a cust_pkg
479 record (see L<FS::cust_pkg>) as well as the fields below. Note these are not
480 the internal FS:: objects, but hash references of columns and values.
482 =item all fields of part_pkg (XXXpare this down to a secure subset)
484 =item part_svc - An array of hash references, each of which has the following keys:
488 =item all fields of part_svc (XXXpare this down to a secure subset)
496 Empty on success, or an error message on errors.
502 Orders a package for this customer.
504 Takes a hash reference as parameter with the following keys:
514 optional svcpart, required only if the package definition does not contain
515 one svc_acct service definition with quantity 1 (it may contain others with
528 Returns a hash reference with a single key, B<error>, empty on success, or an
529 error message on errors. The special error '_decline' is returned for
530 declined transactions.
534 Cancels a package for this customer.
536 Takes a hash reference as parameter with the following keys:
546 Returns a hash reference with a single key, B<error>, empty on success, or an
547 error message on errors.
551 =head1 SIGNUP FUNCTIONS
555 =item signup_info HASHREF
557 Takes a hash reference as parameter with the following keys:
561 =item session_id - Optional agent/reseller interface session
565 Returns a hash reference containing information that may be useful in
566 displaying a signup page. The hash reference contains the following keys:
570 =item cust_main_county
572 County/state/country data - array reference of hash references, each of which has the fields of a cust_main_county record (see L<FS::cust_main_county>). Note these are not FS::cust_main_county objects, but hash references of columns and values.
576 Available packages - array reference of hash references, each of which has the fields of a part_pkg record (see L<FS::part_pkg>). Each hash reference also has an additional 'payby' field containing an array reference of acceptable payment types specific to this package (see below and L<FS::part_pkg/payby>). Note these are not FS::part_pkg objects, but hash references of columns and values. Requires the 'signup_server-default_agentnum' configuration value to be set, or
577 an agentnum specified explicitly via reseller interface session_id in the
582 Array reference of hash references, each of which has the fields of an agent record (see L<FS::agent>). Note these are not FS::agent objects, but hash references of columns and values.
584 =item agentnum2part_pkg
586 Hash reference; keys are agentnums, values are array references of available packages for that agent, in the same format as the part_pkg arrayref above.
590 Access numbers - array reference of hash references, each of which has the fields of an svc_acct_pop record (see L<FS::svc_acct_pop>). Note these are not FS::svc_acct_pop objects, but hash references of columns and values.
592 =item security_phrase
594 True if the "security_phrase" feature is enabled
598 Array reference of acceptable payment types for signup
602 =item CARD (credit card - automatic)
604 =item DCRD (credit card - on-demand - version 1.5+ only)
606 =item CHEK (electronic check - automatic)
608 =item DCHK (electronic check - on-demand - version 1.5+ only)
610 =item LECB (Phone bill billing)
612 =item BILL (billing, not recommended for signups)
614 =item COMP (free, definately not recommended for signups)
616 =item PREPAY (special billing type: applies a credit (see FS::prepay_credit) and sets billing type to BILL)
622 True if CVV features are available (1.5+ or 1.4.2 with CVV schema patch)
626 Hash reference of message catalog values, to support error message customization. Currently available keys are: passwords_dont_match, invalid_card, unknown_card_type, and not_a (as in "Not a Discover card"). Values are configured in the web interface under "View/Edit message catalog".
638 =item new_customer HASHREF
640 Creates a new customer. Takes a hash reference as parameter with the
645 =item first - first name (required)
647 =item last - last name (required)
649 =item ss (not typically collected; mostly used for ACH transactions)
653 =item address1 (required)
657 =item city (required)
661 =item state (required)
665 =item daytime - phone
671 =item payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY (see L</signup_info> (required)
673 =item payinfo - Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
675 =item paycvv - Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
677 =item paydate - Expiration date for CARD/DCRD
679 =item payname - Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
681 =item invoicing_list - comma-separated list of email addresses for email invoices. The special value 'POST' is used to designate postal invoicing (it may be specified alone or in addition to email addresses),
683 =item referral_custnum - referring customer number
685 =item pkgpart - pkgpart of initial package
691 =item sec_phrase - security phrase
693 =item popnum - access number (index, not the literal number)
695 =item agentnum - agent number
699 Returns a hash reference with the following keys:
703 =item error Empty on success, or an error message on errors. The special error '_decline' is returned for declined transactions; other error messages should be suitable for display to the user (and are customizable in under Sysadmin | View/Edit message catalog)
707 =item regionselector HASHREF | LIST
709 Takes as input a hashref or list of key/value pairs with the following keys:
713 =item selected_county
717 =item selected_country
719 =item prefix - Specify a unique prefix string if you intend to use the HTML output multiple time son one page.
721 =item onchange - Specify a javascript subroutine to call on changes
725 =item default_country
727 =item locales - An arrayref of hash references specifying regions. Normally you can just pass the value of the I<cust_main_county> field returned by B<signup_info>.
731 Returns a list consisting of three HTML fragments for county selection,
732 state selection and country selection, respectively.
736 #false laziness w/FS::cust_main_county (this is currently the "newest" version)
744 $param->{'selected_country'} ||= $param->{'default_country'};
745 $param->{'selected_state'} ||= $param->{'default_state'};
747 my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
751 my %cust_main_county;
753 # unless ( @cust_main_county ) { #cache
754 #@cust_main_county = qsearch('cust_main_county', {} );
755 #foreach my $c ( @cust_main_county ) {
756 foreach my $c ( @{ $param->{'locales'} } ) {
757 #$countyflag=1 if $c->county;
758 $countyflag=1 if $c->{county};
759 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
760 #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
761 $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
764 $countyflag=1 if $param->{selected_county};
766 my $script_html = <<END;
768 function opt(what,value,text) {
769 var optionName = new Option(text, value, false, false);
770 var length = what.length;
771 what.options[length] = optionName;
773 function ${prefix}country_changed(what) {
774 country = what.options[what.selectedIndex].text;
775 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
776 what.form.${prefix}state.options[i] = null;
778 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
780 foreach my $country ( sort keys %cust_main_county ) {
781 $script_html .= "\nif ( country == \"$country\" ) {\n";
782 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
783 my $text = $state || '(n/a)';
784 $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
786 $script_html .= "}\n";
789 $script_html .= <<END;
791 function ${prefix}state_changed(what) {
795 $script_html .= <<END;
796 state = what.options[what.selectedIndex].text;
797 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
798 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
799 what.form.${prefix}county.options[i] = null;
802 foreach my $country ( sort keys %cust_main_county ) {
803 $script_html .= "\nif ( country == \"$country\" ) {\n";
804 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
805 $script_html .= "\nif ( state == \"$state\" ) {\n";
806 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
807 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
808 my $text = $county || '(n/a)';
810 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
812 $script_html .= "}\n";
814 $script_html .= "}\n";
818 $script_html .= <<END;
823 my $county_html = $script_html;
825 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
826 $county_html .= '</SELECT>';
829 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$param->{'selected_county'}">!;
832 my $state_html = qq!<SELECT NAME="${prefix}state" !.
833 qq!onChange="${prefix}state_changed(this); $param->{'onchange'}">!;
834 foreach my $state ( sort keys %{ $cust_main_county{$param->{'selected_country'}} } ) {
835 my $text = $state || '(n/a)';
836 my $selected = $state eq $param->{'selected_state'} ? 'SELECTED' : '';
837 $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
839 $state_html .= '</SELECT>';
841 $state_html .= '</SELECT>';
843 my $country_html = qq!<SELECT NAME="${prefix}country" !.
844 qq!onChange="${prefix}country_changed(this); $param->{'onchange'}">!;
845 my $countrydefault = $param->{default_country} || 'US';
846 foreach my $country (
847 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
848 keys %cust_main_county
850 my $selected = $country eq $param->{'selected_country'} ? ' SELECTED' : '';
851 $country_html .= "\n<OPTION$selected>$country</OPTION>"
853 $country_html .= '</SELECT>';
855 ($county_html, $state_html, $country_html);
859 #=item expselect HASHREF | LIST
861 #Takes as input a hashref or list of key/value pairs with the following keys:
865 #=item prefix - Specify a unique prefix string if you intend to use the HTML output multiple time son one page.
867 #=item date - current date, in yyyy-mm-dd or m-d-yyyy format
871 =item expselect PREFIX [ DATE ]
873 Takes as input a unique prefix string and the current expiration date, in
874 yyyy-mm-dd or m-d-yyyy format
876 Returns an HTML fragments for expiration date selection.
886 #my $prefix = $param->{'prefix'};
887 #my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
888 #my $date = exists($param->{'date'}) ? $param->{'date'} : '';
890 my $date = scalar(@_) ? shift : '';
892 my( $m, $y ) = ( 0, 0 );
893 if ( $date =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #PostgreSQL date format
894 ( $m, $y ) = ( $2, $1 );
895 } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
896 ( $m, $y ) = ( $1, $3 );
898 my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
900 $return .= "<OPTION";
901 $return .= " SELECTED" if $_ == $m;
904 $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
906 my $thisYear = $t[5] + 1900;
907 for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. 2037 ) {
908 $return .= "<OPTION";
909 $return .= " SELECTED" if $_ == $y;
912 $return .= "</SELECT>";
917 =item popselector HASHREF | LIST
919 Takes as input a hashref or list of key/value pairs with the following keys:
925 =item pops - An arrayref of hash references specifying access numbers. Normally you can just pass the value of the I<svc_acct_pop> field returned by B<signup_info>.
929 Returns an HTML fragment for access number selection.
933 #horrible false laziness with FS/FS/svc_acct_pop.pm::popselector
941 my $popnum = $param->{'popnum'};
942 my $pops = $param->{'pops'};
944 return '<INPUT TYPE="hidden" NAME="popnum" VALUE="">' unless @$pops;
945 return $pops->[0]{city}. ', '. $pops->[0]{state}.
946 ' ('. $pops->[0]{ac}. ')/'. $pops->[0]{exch}. '-'. $pops->[0]{loc}.
947 '<INPUT TYPE="hidden" NAME="popnum" VALUE="'. $pops->[0]{popnum}. '">'
948 if scalar(@$pops) == 1;
953 push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
954 $popnum2pop{$_->{popnum}} = $_;
959 function opt(what,href,text) {
960 var optionName = new Option(text, href, false, false)
961 var length = what.length;
962 what.options[length] = optionName;
966 my $init_popstate = $param->{'init_popstate'};
967 if ( $init_popstate ) {
968 $text .= '<INPUT TYPE="hidden" NAME="init_popstate" VALUE="'.
969 $init_popstate. '">';
972 function acstate_changed(what) {
973 state = what.options[what.selectedIndex].text;
974 what.form.popac.options.length = 0
975 what.form.popac.options[0] = new Option("Area code", "-1", false, true);
979 my @states = $init_popstate ? ( $init_popstate ) : keys %pop;
980 foreach my $state ( sort { $a cmp $b } @states ) {
981 $text .= "\nif ( state == \"$state\" ) {\n" unless $init_popstate;
983 foreach my $ac ( sort { $a cmp $b } keys %{ $pop{$state} }) {
984 $text .= "opt(what.form.popac, \"$ac\", \"$ac\");\n";
985 if ($ac eq $param->{'popac'}) {
986 $text .= "what.form.popac.options[what.form.popac.length-1].selected = true;\n";
989 $text .= "}\n" unless $init_popstate;
991 $text .= "popac_changed(what.form.popac)}\n";
994 function popac_changed(what) {
995 ac = what.options[what.selectedIndex].text;
996 what.form.popnum.options.length = 0;
997 what.form.popnum.options[0] = new Option("City", "-1", false, true);
1001 foreach my $state ( @states ) {
1002 foreach my $popac ( keys %{ $pop{$state} } ) {
1003 $text .= "\nif ( ac == \"$popac\" ) {\n";
1005 foreach my $pop ( @{$pop{$state}->{$popac}}) {
1006 my $o_popnum = $pop->{popnum};
1007 my $poptext = $pop->{city}. ', '. $pop->{state}.
1008 ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1010 $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n";
1011 if ($popnum == $o_popnum) {
1012 $text .= "what.form.popnum.options[what.form.popnum.length-1].selected = true;\n";
1020 $text .= "}\n</SCRIPT>\n";
1023 qq!<TABLE CELLPADDING="0"><TR><TD><SELECT NAME="acstate"! .
1024 qq!SIZE=1 onChange="acstate_changed(this)"><OPTION VALUE=-1>State!;
1025 $text .= "<OPTION" . ($_ eq $param->{'acstate'} ? " SELECTED" : "") .
1026 ">$_" foreach sort { $a cmp $b } @states;
1027 $text .= '</SELECT>'; #callback? return 3 html pieces? #'</TD>';
1030 qq!<SELECT NAME="popac" SIZE=1 onChange="popac_changed(this)">!.
1031 qq!<OPTION>Area code</SELECT></TR><TR VALIGN="top">!;
1033 $text .= qq!<TR><TD><SELECT NAME="popnum" SIZE=1 STYLE="width: 20em"><OPTION>City!;
1036 #comment this block to disable initial list polulation
1037 my @initial_select = ();
1038 if ( scalar( @$pops ) > 100 ) {
1039 push @initial_select, $popnum2pop{$popnum} if $popnum2pop{$popnum};
1041 @initial_select = @$pops;
1043 foreach my $pop ( sort { $a->{state} cmp $b->{state} } @initial_select ) {
1044 $text .= qq!<OPTION VALUE="!. $pop->{popnum}. '"'.
1045 ( ( $popnum && $pop->{popnum} == $popnum ) ? ' SELECTED' : '' ). ">".
1046 $pop->{city}. ', '. $pop->{state}.
1047 ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1050 $text .= qq!</SELECT></TD></TR></TABLE>!;
1058 =head1 RESELLER FUNCTIONS
1060 Note: Resellers can also use the B<signup_info> and B<new_customer> functions
1061 with their active session, and the B<customer_info> and B<order_pkg> functions
1062 with their active session and an additonal I<custnum> parameter.
1070 =item agent_list_customers
1078 L<freeside-selfservice-clientd>, L<freeside-selfservice-server>