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 'unprovision_svc' => 'MyAccount/unprovision_svc',
40 'signup_info' => 'Signup/signup_info',
41 'new_customer' => 'Signup/new_customer',
42 'agent_login' => 'Agent/agent_login',
43 'agent_logout' => 'Agent/agent_logout',
44 'agent_info' => 'Agent/agent_info',
45 'agent_list_customers' => 'Agent/agent_list_customers',
47 @EXPORT_OK = ( keys(%autoload), qw( regionselector expselect popselector ) );
49 $ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
50 $ENV{'SHELL'} = '/bin/sh';
51 $ENV{'IFS'} = " \t\n";
54 $ENV{'BASH_ENV'} = '';
56 my $freeside_uid = scalar(getpwnam('freeside'));
57 die "not running as the freeside user\n" if $> != $freeside_uid;
59 foreach my $autoload ( keys %autoload ) {
70 $param->{_packet} = \''. $autoload{$autoload}. '\';
72 simple_packet($param);
82 socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
83 connect(SOCK, sockaddr_un($socket)) or die "connect: $!";
84 nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
87 #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
89 #block until there is a message on socket
90 # my $w = new IO::Select;
92 # my @wait = $w->can_read;
93 my $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
94 die $return->{'_error'} if defined $return->{_error} && $return->{_error};
101 FS::SelfService - Freeside self-service API
105 # password and shell account changes
106 use FS::SelfService qw(passwd chfn chsh);
108 # "my account" functionality
109 use FS::SelfService qw( login customer_info invoice cancel payment_info process_payment );
111 my $rv = login( { 'username' => $username,
113 'password' => $password,
117 if ( $rv->{'error'} ) {
118 #handle login error...
121 my $session_id = $rv->{'session_id'};
124 my $customer_info = customer_info( { 'session_id' => $session_id } );
126 #payment_info and process_payment are available in 1.5+ only
127 my $payment_info = payment_info( { 'session_id' => $session_id } );
129 #!!! process_payment example
131 #!!! list_pkgs example
133 #!!! order_pkg example
135 #!!! cancel_pkg example
137 # signup functionality
138 use FS::SelfService qw( signup_info new_customer );
140 my $signup_info = signup_info;
142 $rv = new_customer( {
145 'company' => $company,
146 'address1' => $address1,
147 'address2' => $address2,
151 'country' => $country,
152 'daytime' => $daytime,
156 'payinfo' => $payinfo,
158 'paydate' => $paydate,
159 'payname' => $payname,
160 'invoicing_list' => $invoicing_list,
161 'referral_custnum' => $referral_custnum,
162 'pkgpart' => $pkgpart,
163 'username' => $username,
164 '_password' => $password,
166 'agentnum' => $agentnum,
170 my $error = $rv->{'error'};
171 if ( $error eq '_decline' ) {
181 Use this API to implement your own client "self-service" module.
183 If you just want to customize the look of the existing "self-service" module,
186 =head1 PASSWORD, GECOS, SHELL CHANGING FUNCTIONS
198 =head1 "MY ACCOUNT" FUNCTIONS
204 Creates a user session. Takes a hash reference as parameter with the
217 Returns a hash reference with the following keys:
223 Empty on success, or an error message on errors.
227 Session identifier for successful logins
231 =item customer_info HASHREF
233 Returns general customer information.
235 Takes a hash reference as parameter with a single key: B<session_id>
237 Returns a hash reference with the following keys:
251 Array reference of hash references of open inoices. Each hash reference has
252 the following keys: invnum, date, owed
256 An HTML fragment containing shipping and billing addresses.
258 =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
262 =item edit_info HASHREF
264 Takes a hash reference as parameter with any of the following keys:
266 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
268 If a field exists, the customer record is updated with the new value of that
269 field. If a field does not exist, that field is not changed on the customer
272 Returns a hash reference with a single key, B<error>, empty on success, or an
273 error message on errors
275 =item invoice HASHREF
277 Returns an invoice. Takes a hash reference as parameter with two keys:
278 session_id and invnum
280 Returns a hash reference with the following keys:
286 Empty on success, or an error message on errors
298 =item list_invoices HASHREF
300 Returns a list of all customer invoices. Takes a hash references with a single
303 Returns a hash reference with the following keys:
309 Empty on success, or an error message on errors
313 Reference to array of hash references with the following keys:
323 Invoice date, in UNIX epoch time
331 Cancels this customer.
333 Takes a hash reference as parameter with a single key: B<session_id>
335 Returns a hash reference with a single key, B<error>, which is empty on
336 success or an error message on errors.
338 =item payment_info HASHREF
340 Returns information that may be useful in displaying a payment page.
342 Takes a hash reference as parameter with a single key: B<session_id>.
344 Returns a hash reference with the following keys:
350 Empty on success, or an error message on errors
358 Exact name on credit card (CARD/DCRD)
372 Customer's current default payment type.
376 For CARD/DCRD payment types, the card type (Visa card, MasterCard, Discover card, American Express card, etc.)
380 For CARD/DCRD payment types, the card number
384 For CARD/DCRD payment types, expiration month
388 For CARD/DCRD payment types, expiration year
390 =item cust_main_county
392 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.
396 Array reference of all states in the current default country.
400 Hash reference of card types; keys are card types, values are the exact strings
401 passed to the process_payment function
405 Unique transaction identifier (prevents multiple charges), passed to the
406 process_payment function
410 =item process_payment HASHREF
412 Processes a payment and possible change of address or payment type. Takes a
413 hash reference as parameter with the following keys:
421 If true, address and card information entered will be saved for subsequent
426 If true, future credit card payments will be done automatically (sets payby to
427 CARD). If false, future credit card payments will be done on-demand (sets
428 payby to DCRD). This option only has meaning if B<save> is set true.
448 Card expiration month
456 Unique transaction identifier, returned from the payment_info function.
457 Prevents multiple charges.
461 Returns a hash reference with a single key, B<error>, empty on success, or an
462 error message on errors
466 Returns package information for this customer.
468 Takes a hash reference as parameter with a single key: B<session_id>
470 Returns a hash reference containing customer package information. The hash reference contains the following keys:
475 =item cust_pkg HASHREF
477 Array reference of hash references, each of which has the fields of a cust_pkg
478 record (see L<FS::cust_pkg>) as well as the fields below. Note these are not
479 the internal FS:: objects, but hash references of columns and values.
481 =item all fields of part_pkg (XXXpare this down to a secure subset)
483 =item part_svc - An array of hash references, each of which has the following keys:
487 =item all fields of part_svc (XXXpare this down to a secure subset)
495 Empty on success, or an error message on errors.
501 Orders a package for this customer.
503 Takes a hash reference as parameter with the following keys:
513 optional svcpart, required only if the package definition does not contain
514 one svc_acct service definition with quantity 1 (it may contain others with
527 Returns a hash reference with a single key, B<error>, empty on success, or an
528 error message on errors. The special error '_decline' is returned for
529 declined transactions.
533 Cancels a package for this customer.
535 Takes a hash reference as parameter with the following keys:
545 Returns a hash reference with a single key, B<error>, empty on success, or an
546 error message on errors.
550 =head1 SIGNUP FUNCTIONS
554 =item signup_info HASHREF
556 Takes a hash reference as parameter with the following keys:
560 =item session_id - Optional agent/reseller interface session
564 Returns a hash reference containing information that may be useful in
565 displaying a signup page. The hash reference contains the following keys:
569 =item cust_main_county
571 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.
575 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
576 an agentnum specified explicitly via reseller interface session_id in the
581 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.
583 =item agentnum2part_pkg
585 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.
589 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.
591 =item security_phrase
593 True if the "security_phrase" feature is enabled
597 Array reference of acceptable payment types for signup
601 =item CARD (credit card - automatic)
603 =item DCRD (credit card - on-demand - version 1.5+ only)
605 =item CHEK (electronic check - automatic)
607 =item DCHK (electronic check - on-demand - version 1.5+ only)
609 =item LECB (Phone bill billing)
611 =item BILL (billing, not recommended for signups)
613 =item COMP (free, definately not recommended for signups)
615 =item PREPAY (special billing type: applies a credit (see FS::prepay_credit) and sets billing type to BILL)
621 True if CVV features are available (1.5+ or 1.4.2 with CVV schema patch)
625 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".
637 =item new_customer HASHREF
639 Creates a new customer. Takes a hash reference as parameter with the
644 =item first - first name (required)
646 =item last - last name (required)
648 =item ss (not typically collected; mostly used for ACH transactions)
652 =item address1 (required)
656 =item city (required)
660 =item state (required)
664 =item daytime - phone
670 =item payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY (see L</signup_info> (required)
672 =item payinfo - Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
674 =item paycvv - Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
676 =item paydate - Expiration date for CARD/DCRD
678 =item payname - Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
680 =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),
682 =item referral_custnum - referring customer number
684 =item pkgpart - pkgpart of initial package
690 =item sec_phrase - security phrase
692 =item popnum - access number (index, not the literal number)
694 =item agentnum - agent number
698 Returns a hash reference with the following keys:
702 =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)
706 =item regionselector HASHREF | LIST
708 Takes as input a hashref or list of key/value pairs with the following keys:
712 =item selected_county
716 =item selected_country
718 =item prefix - Specify a unique prefix string if you intend to use the HTML output multiple time son one page.
720 =item onchange - Specify a javascript subroutine to call on changes
724 =item default_country
726 =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>.
730 Returns a list consisting of three HTML fragments for county selection,
731 state selection and country selection, respectively.
735 #false laziness w/FS::cust_main_county (this is currently the "newest" version)
743 $param->{'selected_country'} ||= $param->{'default_country'};
744 $param->{'selected_state'} ||= $param->{'default_state'};
746 my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
750 my %cust_main_county;
752 # unless ( @cust_main_county ) { #cache
753 #@cust_main_county = qsearch('cust_main_county', {} );
754 #foreach my $c ( @cust_main_county ) {
755 foreach my $c ( @{ $param->{'locales'} } ) {
756 #$countyflag=1 if $c->county;
757 $countyflag=1 if $c->{county};
758 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
759 #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
760 $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
763 $countyflag=1 if $param->{selected_county};
765 my $script_html = <<END;
767 function opt(what,value,text) {
768 var optionName = new Option(text, value, false, false);
769 var length = what.length;
770 what.options[length] = optionName;
772 function ${prefix}country_changed(what) {
773 country = what.options[what.selectedIndex].text;
774 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
775 what.form.${prefix}state.options[i] = null;
777 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
779 foreach my $country ( sort keys %cust_main_county ) {
780 $script_html .= "\nif ( country == \"$country\" ) {\n";
781 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
782 my $text = $state || '(n/a)';
783 $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
785 $script_html .= "}\n";
788 $script_html .= <<END;
790 function ${prefix}state_changed(what) {
794 $script_html .= <<END;
795 state = what.options[what.selectedIndex].text;
796 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
797 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
798 what.form.${prefix}county.options[i] = null;
801 foreach my $country ( sort keys %cust_main_county ) {
802 $script_html .= "\nif ( country == \"$country\" ) {\n";
803 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
804 $script_html .= "\nif ( state == \"$state\" ) {\n";
805 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
806 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
807 my $text = $county || '(n/a)';
809 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
811 $script_html .= "}\n";
813 $script_html .= "}\n";
817 $script_html .= <<END;
822 my $county_html = $script_html;
824 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
825 $county_html .= '</SELECT>';
828 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$param->{'selected_county'}">!;
831 my $state_html = qq!<SELECT NAME="${prefix}state" !.
832 qq!onChange="${prefix}state_changed(this); $param->{'onchange'}">!;
833 foreach my $state ( sort keys %{ $cust_main_county{$param->{'selected_country'}} } ) {
834 my $text = $state || '(n/a)';
835 my $selected = $state eq $param->{'selected_state'} ? 'SELECTED' : '';
836 $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
838 $state_html .= '</SELECT>';
840 $state_html .= '</SELECT>';
842 my $country_html = qq!<SELECT NAME="${prefix}country" !.
843 qq!onChange="${prefix}country_changed(this); $param->{'onchange'}">!;
844 my $countrydefault = $param->{default_country} || 'US';
845 foreach my $country (
846 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
847 keys %cust_main_county
849 my $selected = $country eq $param->{'selected_country'} ? ' SELECTED' : '';
850 $country_html .= "\n<OPTION$selected>$country</OPTION>"
852 $country_html .= '</SELECT>';
854 ($county_html, $state_html, $country_html);
858 #=item expselect HASHREF | LIST
860 #Takes as input a hashref or list of key/value pairs with the following keys:
864 #=item prefix - Specify a unique prefix string if you intend to use the HTML output multiple time son one page.
866 #=item date - current date, in yyyy-mm-dd or m-d-yyyy format
870 =item expselect PREFIX [ DATE ]
872 Takes as input a unique prefix string and the current expiration date, in
873 yyyy-mm-dd or m-d-yyyy format
875 Returns an HTML fragments for expiration date selection.
885 #my $prefix = $param->{'prefix'};
886 #my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
887 #my $date = exists($param->{'date'}) ? $param->{'date'} : '';
889 my $date = scalar(@_) ? shift : '';
891 my( $m, $y ) = ( 0, 0 );
892 if ( $date =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #PostgreSQL date format
893 ( $m, $y ) = ( $2, $1 );
894 } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
895 ( $m, $y ) = ( $1, $3 );
897 my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
899 $return .= "<OPTION";
900 $return .= " SELECTED" if $_ == $m;
903 $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
905 my $thisYear = $t[5] + 1900;
906 for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. 2037 ) {
907 $return .= "<OPTION";
908 $return .= " SELECTED" if $_ == $y;
911 $return .= "</SELECT>";
916 =item popselector HASHREF | LIST
918 Takes as input a hashref or list of key/value pairs with the following keys:
924 =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>.
928 Returns an HTML fragment for access number selection.
932 #horrible false laziness with FS/FS/svc_acct_pop.pm::popselector
940 my $popnum = $param->{'popnum'};
941 my $pops = $param->{'pops'};
943 return '<INPUT TYPE="hidden" NAME="popnum" VALUE="">' unless @$pops;
944 return $pops->[0]{city}. ', '. $pops->[0]{state}.
945 ' ('. $pops->[0]{ac}. ')/'. $pops->[0]{exch}. '-'. $pops->[0]{loc}.
946 '<INPUT TYPE="hidden" NAME="popnum" VALUE="'. $pops->[0]{popnum}. '">'
947 if scalar(@$pops) == 1;
952 push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
953 $popnum2pop{$_->{popnum}} = $_;
958 function opt(what,href,text) {
959 var optionName = new Option(text, href, false, false)
960 var length = what.length;
961 what.options[length] = optionName;
965 my $init_popstate = $param->{'init_popstate'};
966 if ( $init_popstate ) {
967 $text .= '<INPUT TYPE="hidden" NAME="init_popstate" VALUE="'.
968 $init_popstate. '">';
971 function acstate_changed(what) {
972 state = what.options[what.selectedIndex].text;
973 what.form.popac.options.length = 0
974 what.form.popac.options[0] = new Option("Area code", "-1", false, true);
978 my @states = $init_popstate ? ( $init_popstate ) : keys %pop;
979 foreach my $state ( sort { $a cmp $b } @states ) {
980 $text .= "\nif ( state == \"$state\" ) {\n" unless $init_popstate;
982 foreach my $ac ( sort { $a cmp $b } keys %{ $pop{$state} }) {
983 $text .= "opt(what.form.popac, \"$ac\", \"$ac\");\n";
984 if ($ac eq $param->{'popac'}) {
985 $text .= "what.form.popac.options[what.form.popac.length-1].selected = true;\n";
988 $text .= "}\n" unless $init_popstate;
990 $text .= "popac_changed(what.form.popac)}\n";
993 function popac_changed(what) {
994 ac = what.options[what.selectedIndex].text;
995 what.form.popnum.options.length = 0;
996 what.form.popnum.options[0] = new Option("City", "-1", false, true);
1000 foreach my $state ( @states ) {
1001 foreach my $popac ( keys %{ $pop{$state} } ) {
1002 $text .= "\nif ( ac == \"$popac\" ) {\n";
1004 foreach my $pop ( @{$pop{$state}->{$popac}}) {
1005 my $o_popnum = $pop->{popnum};
1006 my $poptext = $pop->{city}. ', '. $pop->{state}.
1007 ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1009 $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n";
1010 if ($popnum == $o_popnum) {
1011 $text .= "what.form.popnum.options[what.form.popnum.length-1].selected = true;\n";
1019 $text .= "}\n</SCRIPT>\n";
1022 qq!<TABLE CELLPADDING="0"><TR><TD><SELECT NAME="acstate"! .
1023 qq!SIZE=1 onChange="acstate_changed(this)"><OPTION VALUE=-1>State!;
1024 $text .= "<OPTION" . ($_ eq $param->{'acstate'} ? " SELECTED" : "") .
1025 ">$_" foreach sort { $a cmp $b } @states;
1026 $text .= '</SELECT>'; #callback? return 3 html pieces? #'</TD>';
1029 qq!<SELECT NAME="popac" SIZE=1 onChange="popac_changed(this)">!.
1030 qq!<OPTION>Area code</SELECT></TR><TR VALIGN="top">!;
1032 $text .= qq!<TR><TD><SELECT NAME="popnum" SIZE=1 STYLE="width: 20em"><OPTION>City!;
1035 #comment this block to disable initial list polulation
1036 my @initial_select = ();
1037 if ( scalar( @$pops ) > 100 ) {
1038 push @initial_select, $popnum2pop{$popnum} if $popnum2pop{$popnum};
1040 @initial_select = @$pops;
1042 foreach my $pop ( sort { $a->{state} cmp $b->{state} } @initial_select ) {
1043 $text .= qq!<OPTION VALUE="!. $pop->{popnum}. '"'.
1044 ( ( $popnum && $pop->{popnum} == $popnum ) ? ' SELECTED' : '' ). ">".
1045 $pop->{city}. ', '. $pop->{state}.
1046 ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1049 $text .= qq!</SELECT></TD></TR></TABLE>!;
1057 =head1 RESELLER FUNCTIONS
1059 Note: Resellers can also use the B<signup_info> and B<new_customer> functions
1060 with their active session, and the B<customer_info> and B<order_pkg> functions
1061 with their active session and an additonal I<custnum> parameter.
1069 =item agent_list_customers
1077 L<freeside-selfservice-clientd>, L<freeside-selfservice-server>