improve layout of self-service documentation
[freeside.git] / fs_selfservice / FS-SelfService / SelfService.pm
1 package FS::SelfService;
2
3 use strict;
4 use vars qw($VERSION @ISA @EXPORT_OK $DEBUG $dir $socket %autoload $tag);
5 use Exporter;
6 use Socket;
7 use FileHandle;
8 #use IO::Handle;
9 use IO::Select;
10 use Storable 2.09 qw(nstore_fd fd_retrieve);
11
12 $VERSION = '0.03';
13
14 @ISA = qw( Exporter );
15
16 $DEBUG = 0;
17
18 $dir = "/usr/local/freeside";
19 $socket =  "$dir/selfservice_socket";
20 $socket .= '.'.$tag if defined $tag && length($tag);
21
22 #maybe should ask ClientAPI for this list
23 %autoload = (
24   'passwd'               => 'passwd/passwd',
25   'chfn'                 => 'passwd/passwd',
26   'chsh'                 => 'passwd/passwd',
27   'login'                => 'MyAccount/login',
28   'logout'               => 'MyAccount/logout',
29   'customer_info'        => 'MyAccount/customer_info',
30   'edit_info'            => 'MyAccount/edit_info',     #add to ss cgi!
31   'invoice'              => 'MyAccount/invoice',
32   'invoice_logo'         => 'MyAccount/invoice_logo',
33   'list_invoices'        => 'MyAccount/list_invoices', #?
34   'cancel'               => 'MyAccount/cancel',        #add to ss cgi!
35   'payment_info'         => 'MyAccount/payment_info',
36   'process_payment'      => 'MyAccount/process_payment',
37   'process_prepay'       => 'MyAccount/process_prepay',
38   'list_pkgs'            => 'MyAccount/list_pkgs',     #add to ss cgi (added?)
39   'list_svcs'            => 'MyAccount/list_svcs',     #add to ss cgi (added?)
40   'list_svc_usage'       => 'MyAccount/list_svc_usage',   
41   'order_pkg'            => 'MyAccount/order_pkg',     #add to ss cgi!
42   'change_pkg'           => 'MyAccount/change_pkg', 
43   'order_recharge'       => 'MyAccount/order_recharge',
44   'cancel_pkg'           => 'MyAccount/cancel_pkg',    #add to ss cgi!
45   'charge'               => 'MyAccount/charge',        #?
46   'part_svc_info'        => 'MyAccount/part_svc_info',
47   'provision_acct'       => 'MyAccount/provision_acct',
48   'provision_external'   => 'MyAccount/provision_external',
49   'unprovision_svc'      => 'MyAccount/unprovision_svc',
50   'myaccount_passwd'     => 'MyAccount/myaccount_passwd',
51   'signup_info'          => 'Signup/signup_info',
52   'new_customer'         => 'Signup/new_customer',
53   'agent_login'          => 'Agent/agent_login',
54   'agent_logout'         => 'Agent/agent_logout',
55   'agent_info'           => 'Agent/agent_info',
56   'agent_list_customers' => 'Agent/agent_list_customers',
57 );
58 @EXPORT_OK = ( keys(%autoload), qw( regionselector expselect popselector ) );
59
60 $ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
61 $ENV{'SHELL'} = '/bin/sh';
62 $ENV{'IFS'} = " \t\n";
63 $ENV{'CDPATH'} = '';
64 $ENV{'ENV'} = '';
65 $ENV{'BASH_ENV'} = '';
66
67 my $freeside_uid = scalar(getpwnam('freeside'));
68 die "not running as the freeside user\n" if $> != $freeside_uid;
69
70 -e $dir or die "FATAL: $dir doesn't exist!";
71 -d $dir or die "FATAL: $dir isn't a directory!";
72 -r $dir or die "FATAL: Can't read $dir as freeside user!";
73 -x $dir or die "FATAL: $dir not searchable (executable) as freeside user!";
74
75 foreach my $autoload ( keys %autoload ) {
76
77   my $eval =
78   "sub $autoload { ". '
79                    my $param;
80                    if ( ref($_[0]) ) {
81                      $param = shift;
82                    } else {
83                      #warn scalar(@_). ": ". join(" / ", @_);
84                      $param = { @_ };
85                    }
86
87                    $param->{_packet} = \''. $autoload{$autoload}. '\';
88
89                    simple_packet($param);
90                  }';
91
92   eval $eval;
93   die $@ if $@;
94
95 }
96
97 sub simple_packet {
98   my $packet = shift;
99   warn "sending ". $packet->{_packet}. " to server"
100     if $DEBUG;
101   socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
102   connect(SOCK, sockaddr_un($socket)) or die "connect to $socket: $!";
103   nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
104   SOCK->flush;
105
106   #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
107
108   #block until there is a message on socket
109 #  my $w = new IO::Select;
110 #  $w->add(\*SOCK);
111 #  my @wait = $w->can_read;
112
113   warn "reading message from server"
114     if $DEBUG;
115
116   my $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
117   die $return->{'_error'} if defined $return->{_error} && $return->{_error};
118
119   warn "returning message to client"
120     if $DEBUG;
121
122   $return;
123 }
124
125 =head1 NAME
126
127 FS::SelfService - Freeside self-service API
128
129 =head1 SYNOPSIS
130
131   # password and shell account changes
132   use FS::SelfService qw(passwd chfn chsh);
133
134   # "my account" functionality
135   use FS::SelfService qw( login customer_info invoice cancel payment_info process_payment );
136
137   my $rv = login( { 'username' => $username,
138                     'domain'   => $domain,
139                     'password' => $password,
140                   }
141                 );
142
143   if ( $rv->{'error'} ) {
144     #handle login error...
145   } else {
146     #successful login
147     my $session_id = $rv->{'session_id'};
148   }
149
150   my $customer_info = customer_info( { 'session_id' => $session_id } );
151
152   #payment_info and process_payment are available in 1.5+ only
153   my $payment_info = payment_info( { 'session_id' => $session_id } );
154
155   #!!! process_payment example
156
157   #!!! list_pkgs example
158
159   #!!! order_pkg example
160
161   #!!! cancel_pkg example
162
163   # signup functionality
164   use FS::SelfService qw( signup_info new_customer );
165
166   my $signup_info = signup_info;
167
168   $rv = new_customer( {
169                         'first'            => $first,
170                         'last'             => $last,
171                         'company'          => $company,
172                         'address1'         => $address1,
173                         'address2'         => $address2,
174                         'city'             => $city,
175                         'state'            => $state,
176                         'zip'              => $zip,
177                         'country'          => $country,
178                         'daytime'          => $daytime,
179                         'night'            => $night,
180                         'fax'              => $fax,
181                         'payby'            => $payby,
182                         'payinfo'          => $payinfo,
183                         'paycvv'           => $paycvv,
184                         'paystart_month'   => $paystart_month
185                         'paystart_year'    => $paystart_year,
186                         'payissue'         => $payissue,
187                         'payip'            => $payip
188                         'paydate'          => $paydate,
189                         'payname'          => $payname,
190                         'invoicing_list'   => $invoicing_list,
191                         'referral_custnum' => $referral_custnum,
192                         'pkgpart'          => $pkgpart,
193                         'username'         => $username,
194                         '_password'        => $password,
195                         'popnum'           => $popnum,
196                         'agentnum'         => $agentnum,
197                       }
198                     );
199   
200   my $error = $rv->{'error'};
201   if ( $error eq '_decline' ) {
202     print_decline();
203   } elsif ( $error ) {
204     reprint_signup();
205   } else {
206     print_success();
207   }
208
209 =head1 DESCRIPTION
210
211 Use this API to implement your own client "self-service" module.
212
213 If you just want to customize the look of the existing "self-service" module,
214 see XXXX instead.
215
216 =head1 PASSWORD, GECOS, SHELL CHANGING FUNCTIONS
217
218 =over 4
219
220 =item passwd
221
222 =item chfn
223
224 =item chsh
225
226 =back
227
228 =head1 "MY ACCOUNT" FUNCTIONS
229
230 =over 4
231
232 =item login HASHREF
233
234 Creates a user session.  Takes a hash reference as parameter with the
235 following keys:
236
237 =over 4
238
239 =item username
240
241 Username
242
243 =item domain
244
245 Domain
246
247 =item password
248
249 Password
250
251 =back
252
253 Returns a hash reference with the following keys:
254
255 =over 4
256
257 =item error
258
259 Empty on success, or an error message on errors.
260
261 =item session_id
262
263 Session identifier for successful logins
264
265 =back
266
267 =item customer_info HASHREF
268
269 Returns general customer information.
270
271 Takes a hash reference as parameter with a single key: B<session_id>
272
273 Returns a hash reference with the following keys:
274
275 =over 4
276
277 =item name
278
279 Customer name
280
281 =item balance
282
283 Balance owed
284
285 =item open
286
287 Array reference of hash references of open inoices.  Each hash reference has
288 the following keys: invnum, date, owed
289
290 =item small_custview
291
292 An HTML fragment containing shipping and billing addresses.
293
294 =item The following fields are also returned
295
296 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
297
298 =back
299
300 =item edit_info HASHREF
301
302 Takes a hash reference as parameter with any of the following keys:
303
304 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
305
306 If a field exists, the customer record is updated with the new value of that
307 field.  If a field does not exist, that field is not changed on the customer
308 record.
309
310 Returns a hash reference with a single key, B<error>, empty on success, or an
311 error message on errors
312
313 =item invoice HASHREF
314
315 Returns an invoice.  Takes a hash reference as parameter with two keys:
316 session_id and invnum
317
318 Returns a hash reference with the following keys:
319
320 =over 4
321
322 =item error
323
324 Empty on success, or an error message on errors
325
326 =item invnum
327
328 Invoice number
329
330 =item invoice_text
331
332 Invoice text
333
334 =back
335
336 =item list_invoices HASHREF
337
338 Returns a list of all customer invoices.  Takes a hash references with a single
339 key, session_id.
340
341 Returns a hash reference with the following keys:
342
343 =over 4
344
345 =item error
346
347 Empty on success, or an error message on errors
348
349 =item invoices
350
351 Reference to array of hash references with the following keys:
352
353 =over 4
354
355 =item invnum
356
357 Invoice ID
358
359 =item _date
360
361 Invoice date, in UNIX epoch time
362
363 =back
364
365 =back
366
367 =item cancel HASHREF
368
369 Cancels this customer.
370
371 Takes a hash reference as parameter with a single key: B<session_id>
372
373 Returns a hash reference with a single key, B<error>, which is empty on
374 success or an error message on errors.
375
376 =item payment_info HASHREF
377
378 Returns information that may be useful in displaying a payment page.
379
380 Takes a hash reference as parameter with a single key: B<session_id>.
381
382 Returns a hash reference with the following keys:
383
384 =over 4
385
386 =item error
387
388 Empty on success, or an error message on errors
389
390 =item balance
391
392 Balance owed
393
394 =item payname
395
396 Exact name on credit card (CARD/DCRD)
397
398 =item address1
399
400 Address line one
401
402 =item address2
403
404 Address line two
405
406 =item city
407
408 City
409
410 =item state
411
412 State
413
414 =item zip
415
416 Zip or postal code
417
418 =item payby
419
420 Customer's current default payment type.
421
422 =item card_type
423
424 For CARD/DCRD payment types, the card type (Visa card, MasterCard, Discover card, American Express card, etc.)
425
426 =item payinfo
427
428 For CARD/DCRD payment types, the card number
429
430 =item month
431
432 For CARD/DCRD payment types, expiration month
433
434 =item year
435
436 For CARD/DCRD payment types, expiration year
437
438 =item cust_main_county
439
440 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.
441
442 =item states
443
444 Array reference of all states in the current default country.
445
446 =item card_types
447
448 Hash reference of card types; keys are card types, values are the exact strings
449 passed to the process_payment function
450
451 =item paybatch
452
453 Unique transaction identifier (prevents multiple charges), passed to the
454 process_payment function
455
456 =back
457
458 =item process_payment HASHREF
459
460 Processes a payment and possible change of address or payment type.  Takes a
461 hash reference as parameter with the following keys:
462
463 =over 4
464
465 =item session_id
466
467 Session identifier
468
469 =item save
470
471 If true, address and card information entered will be saved for subsequent
472 transactions.
473
474 =item auto
475
476 If true, future credit card payments will be done automatically (sets payby to
477 CARD).  If false, future credit card payments will be done on-demand (sets
478 payby to DCRD).  This option only has meaning if B<save> is set true.  
479
480 =item payname
481
482 Name on card
483
484 =item address1
485
486 Address line one
487
488 =item address2
489
490 Address line two
491
492 =item city
493
494 City
495
496 =item state
497
498 State
499
500 =item zip
501
502 Zip or postal code
503
504 =item payinfo
505
506 Card number
507
508 =item month
509
510 Card expiration month
511
512 =item year
513
514 Card expiration year
515
516 =item paybatch
517
518 Unique transaction identifier, returned from the payment_info function.
519 Prevents multiple charges.
520
521 =back
522
523 Returns a hash reference with a single key, B<error>, empty on success, or an
524 error message on errors
525
526 =item list_pkgs
527
528 Returns package information for this customer.
529
530 Takes a hash reference as parameter with a single key: B<session_id>
531
532 Returns a hash reference containing customer package information.  The hash reference contains the following keys:
533
534 =over 4
535
536 =item cust_pkg HASHREF
537
538 Array reference of hash references, each of which has the fields of a cust_pkg
539 record (see L<FS::cust_pkg>) as well as the fields below.  Note these are not
540 the internal FS:: objects, but hash references of columns and values.
541
542 =over 4
543
544 =item part_pkg fields
545
546 All fields of part_pkg (be careful with this information - it may reveal more
547 about your available packages than you would like users to know in aggregate) 
548
549 =cut
550
551 #XXX pare part_pkg fields down to a more secure subset
552
553 =item part_svc
554
555 An array of hash references, each of which has the following keys:
556
557 =over 4
558
559 =item part_svc fields
560
561 All fields of part_svc (be careful with this information - it may reveal more
562 about your available packages than you would like users to know in aggregate) 
563
564 =cut
565
566 #XXX pare part_svc fields down to a more secure subset
567
568 =back
569
570 =back
571
572 =item error
573
574 Empty on success, or an error message on errors.
575
576 =back
577
578 =item order_pkg
579
580 Orders a package for this customer.
581
582 Takes a hash reference as parameter with the following keys:
583
584 =over 4
585
586 =item session_id
587
588 Session identifier
589
590 =item pkgpart
591
592 pkgpart of package to order
593
594 =item svcpart
595
596 optional svcpart, required only if the package definition does not contain
597 one svc_acct service definition with quantity 1 (it may contain others with
598 quantity >1)
599
600 =item username
601
602 Username
603
604 =item _password
605
606 Password
607
608 =item sec_phrase
609
610 Optional security phrase
611
612 =item popnum
613
614 Optional Access number number
615
616 =back
617
618 Returns a hash reference with a single key, B<error>, empty on success, or an
619 error message on errors.  The special error '_decline' is returned for
620 declined transactions.
621
622 =item cancel_pkg
623
624 Cancels a package for this customer.
625
626 Takes a hash reference as parameter with the following keys:
627
628 =over 4
629
630 =item session_id
631
632 Session identifier
633
634 =item pkgpart
635
636 pkgpart of package to cancel
637
638 =back
639
640 Returns a hash reference with a single key, B<error>, empty on success, or an
641 error message on errors.
642
643 =back
644
645 =head1 SIGNUP FUNCTIONS
646
647 =over 4
648
649 =item signup_info HASHREF
650
651 Takes a hash reference as parameter with the following keys:
652
653 =over 4
654
655 =item session_id - Optional agent/reseller interface session
656
657 =back
658
659 Returns a hash reference containing information that may be useful in
660 displaying a signup page.  The hash reference contains the following keys:
661
662 =over 4
663
664 =item cust_main_county
665
666 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.
667
668 =item part_pkg
669
670 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
671 an agentnum specified explicitly via reseller interface session_id in the
672 options.
673
674 =item agent
675
676 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.
677
678 =item agentnum2part_pkg
679
680 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.
681
682 =item svc_acct_pop
683
684 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.
685
686 =item security_phrase
687
688 True if the "security_phrase" feature is enabled
689
690 =item payby
691
692 Array reference of acceptable payment types for signup
693
694 =over 4
695
696 =item CARD
697
698 credit card - automatic
699
700 =item DCRD
701
702 credit card - on-demand - version 1.5+ only
703
704 =item CHEK
705
706 electronic check - automatic
707
708 =item DCHK
709
710 electronic check - on-demand - version 1.5+ only
711
712 =item LECB
713
714 Phone bill billing
715
716 =item BILL
717
718 billing, not recommended for signups
719
720 =item COMP
721
722 free, definitely not recommended for signups
723
724 =item PREPAY
725
726 special billing type: applies a credit (see FS::prepay_credit) and sets billing type to BILL
727
728 =back
729
730 =item cvv_enabled
731
732 True if CVV features are available (1.5+ or 1.4.2 with CVV schema patch)
733
734 =item msgcat
735
736 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".
737
738 =item statedefault
739
740 Default state
741
742 =item countrydefault
743
744 Default country
745
746 =back
747
748 =item new_customer HASHREF
749
750 Creates a new customer.  Takes a hash reference as parameter with the
751 following keys:
752
753 =over 4
754
755 =item first
756
757 first name (required)
758
759 =item last
760
761 last name (required)
762
763 =item ss
764
765 (not typically collected; mostly used for ACH transactions)
766
767 =item company
768
769 Company name
770
771 =item address1 (required)
772
773 Address line one
774
775 =item address2
776
777 Address line two
778
779 =item city (required)
780
781 City
782
783 =item county
784
785 County
786
787 =item state (required)
788
789 State
790
791 =item zip (required)
792
793 Zip or postal code
794
795 =item daytime
796
797 Daytime phone number
798
799 =item night
800
801 Evening phone number
802
803 =item fax
804
805 Fax number
806
807 =item payby
808
809 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY (see L</signup_info> (required)
810
811 =item payinfo
812
813 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
814
815 =item paycvv
816
817 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
818
819 =item paydate
820
821 Expiration date for CARD/DCRD
822
823 =item payname
824
825 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
826
827 =item invoicing_list
828
829 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),
830
831 =item referral_custnum
832
833 referring customer number
834
835 =item pkgpart
836
837 pkgpart of initial package
838
839 =item username
840
841 Username
842
843 =item _password
844
845 Password
846
847 =item sec_phrase
848
849 Security phrase
850
851 =item popnum
852
853 Access number (index, not the literal number)
854
855 =item agentnum
856
857 Agent number
858
859 =back
860
861 Returns a hash reference with the following keys:
862
863 =over 4
864
865 =item error
866
867 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 Configuration | View/Edit message catalog)
868
869 =back
870
871 =item regionselector HASHREF | LIST
872
873 Takes as input a hashref or list of key/value pairs with the following keys:
874
875 =over 4
876
877 =item selected_county
878
879 Currently selected county
880
881 =item selected_state
882
883 Currently selected state
884
885 =item selected_country
886
887 Currently selected country
888
889 =item prefix
890
891 Specify a unique prefix string  if you intend to use the HTML output multiple time son one page.
892
893 =item onchange
894
895 Specify a javascript subroutine to call on changes
896
897 =item default_state
898
899 Default state
900
901 =item default_country
902
903 Default country
904
905 =item locales
906
907 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>.
908
909 =back
910
911 Returns a list consisting of three HTML fragments for county selection,
912 state selection and country selection, respectively.
913
914 =cut
915
916 #false laziness w/FS::cust_main_county (this is currently the "newest" version)
917 sub regionselector {
918   my $param;
919   if ( ref($_[0]) ) {
920     $param = shift;
921   } else {
922     $param = { @_ };
923   }
924   $param->{'selected_country'} ||= $param->{'default_country'};
925   $param->{'selected_state'} ||= $param->{'default_state'};
926
927   my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
928
929   my $countyflag = 0;
930
931   my %cust_main_county;
932
933 #  unless ( @cust_main_county ) { #cache 
934     #@cust_main_county = qsearch('cust_main_county', {} );
935     #foreach my $c ( @cust_main_county ) {
936     foreach my $c ( @{ $param->{'locales'} } ) {
937       #$countyflag=1 if $c->county;
938       $countyflag=1 if $c->{county};
939       #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
940       #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
941       $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
942     }
943 #  }
944   $countyflag=1 if $param->{selected_county};
945
946   my $script_html = <<END;
947     <SCRIPT>
948     function opt(what,value,text) {
949       var optionName = new Option(text, value, false, false);
950       var length = what.length;
951       what.options[length] = optionName;
952     }
953     function ${prefix}country_changed(what) {
954       country = what.options[what.selectedIndex].text;
955       for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
956           what.form.${prefix}state.options[i] = null;
957 END
958       #what.form.${prefix}state.options[0] = new Option('', '', false, true);
959
960   foreach my $country ( sort keys %cust_main_county ) {
961     $script_html .= "\nif ( country == \"$country\" ) {\n";
962     foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
963       my $text = $state || '(n/a)';
964       $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
965     }
966     $script_html .= "}\n";
967   }
968
969   $script_html .= <<END;
970     }
971     function ${prefix}state_changed(what) {
972 END
973
974   if ( $countyflag ) {
975     $script_html .= <<END;
976       state = what.options[what.selectedIndex].text;
977       country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
978       for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
979           what.form.${prefix}county.options[i] = null;
980 END
981
982     foreach my $country ( sort keys %cust_main_county ) {
983       $script_html .= "\nif ( country == \"$country\" ) {\n";
984       foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
985         $script_html .= "\nif ( state == \"$state\" ) {\n";
986           #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
987           foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
988             my $text = $county || '(n/a)';
989             $script_html .=
990               qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
991           }
992         $script_html .= "}\n";
993       }
994       $script_html .= "}\n";
995     }
996   }
997
998   $script_html .= <<END;
999     }
1000     </SCRIPT>
1001 END
1002
1003   my $county_html = $script_html;
1004   if ( $countyflag ) {
1005     $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
1006     $county_html .= '</SELECT>';
1007   } else {
1008     $county_html .=
1009       qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$param->{'selected_county'}">!;
1010   }
1011
1012   my $state_html = qq!<SELECT NAME="${prefix}state" !.
1013                    qq!onChange="${prefix}state_changed(this); $param->{'onchange'}">!;
1014   foreach my $state ( sort keys %{ $cust_main_county{$param->{'selected_country'}} } ) {
1015     my $text = $state || '(n/a)';
1016     my $selected = $state eq $param->{'selected_state'} ? 'SELECTED' : '';
1017     $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
1018   }
1019   $state_html .= '</SELECT>';
1020
1021   $state_html .= '</SELECT>';
1022
1023   my $country_html = qq!<SELECT NAME="${prefix}country" !.
1024                      qq!onChange="${prefix}country_changed(this); $param->{'onchange'}">!;
1025   my $countrydefault = $param->{default_country} || 'US';
1026   foreach my $country (
1027     sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
1028       keys %cust_main_county
1029   ) {
1030     my $selected = $country eq $param->{'selected_country'} ? ' SELECTED' : '';
1031     $country_html .= "\n<OPTION$selected>$country</OPTION>"
1032   }
1033   $country_html .= '</SELECT>';
1034
1035   ($county_html, $state_html, $country_html);
1036
1037 }
1038
1039 #=item expselect HASHREF | LIST
1040 #
1041 #Takes as input a hashref or list of key/value pairs with the following keys:
1042 #
1043 #=over 4
1044 #
1045 #=item prefix - Specify a unique prefix string  if you intend to use the HTML output multiple time son one page.
1046 #
1047 #=item date - current date, in yyyy-mm-dd or m-d-yyyy format
1048 #
1049 #=back
1050
1051 =item expselect PREFIX [ DATE ]
1052
1053 Takes as input a unique prefix string and the current expiration date, in
1054 yyyy-mm-dd or m-d-yyyy format
1055
1056 Returns an HTML fragments for expiration date selection.
1057
1058 =cut
1059
1060 sub expselect {
1061   #my $param;
1062   #if ( ref($_[0]) ) {
1063   #  $param = shift;
1064   #} else {
1065   #  $param = { @_ };
1066   #my $prefix = $param->{'prefix'};
1067   #my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
1068   #my $date =   exists($param->{'date'})   ? $param->{'date'}   : '';
1069   my $prefix = shift;
1070   my $date = scalar(@_) ? shift : '';
1071
1072   my( $m, $y ) = ( 0, 0 );
1073   if ( $date  =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #PostgreSQL date format
1074     ( $m, $y ) = ( $2, $1 );
1075   } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
1076     ( $m, $y ) = ( $1, $3 );
1077   }
1078   my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
1079   for ( 1 .. 12 ) {
1080     $return .= qq!<OPTION VALUE="$_"!;
1081     $return .= " SELECTED" if $_ == $m;
1082     $return .= ">$_";
1083   }
1084   $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
1085   my @t = localtime;
1086   my $thisYear = $t[5] + 1900;
1087   for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. ($thisYear+10) ) {
1088     $return .= qq!<OPTION VALUE="$_"!;
1089     $return .= " SELECTED" if $_ == $y;
1090     $return .= ">$_";
1091   }
1092   $return .= "</SELECT>";
1093
1094   $return;
1095 }
1096
1097 =item popselector HASHREF | LIST
1098
1099 Takes as input a hashref or list of key/value pairs with the following keys:
1100
1101 =over 4
1102
1103 =item popnum
1104
1105 Access number number
1106
1107 =item pops
1108
1109 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>.
1110
1111 =back
1112
1113 Returns an HTML fragment for access number selection.
1114
1115 =cut
1116
1117 #horrible false laziness with FS/FS/svc_acct_pop.pm::popselector
1118 sub popselector {
1119   my $param;
1120   if ( ref($_[0]) ) {
1121     $param = shift;
1122   } else {
1123     $param = { @_ };
1124   }
1125   my $popnum = $param->{'popnum'};
1126   my $pops = $param->{'pops'};
1127
1128   return '<INPUT TYPE="hidden" NAME="popnum" VALUE="">' unless @$pops;
1129   return $pops->[0]{city}. ', '. $pops->[0]{state}.
1130          ' ('. $pops->[0]{ac}. ')/'. $pops->[0]{exch}. '-'. $pops->[0]{loc}.
1131          '<INPUT TYPE="hidden" NAME="popnum" VALUE="'. $pops->[0]{popnum}. '">'
1132     if scalar(@$pops) == 1;
1133
1134   my %pop = ();
1135   my %popnum2pop = ();
1136   foreach (@$pops) {
1137     push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
1138     $popnum2pop{$_->{popnum}} = $_;
1139   }
1140
1141   my $text = <<END;
1142     <SCRIPT>
1143     function opt(what,href,text) {
1144       var optionName = new Option(text, href, false, false)
1145       var length = what.length;
1146       what.options[length] = optionName;
1147     }
1148 END
1149
1150   my $init_popstate = $param->{'init_popstate'};
1151   if ( $init_popstate ) {
1152     $text .= '<INPUT TYPE="hidden" NAME="init_popstate" VALUE="'.
1153              $init_popstate. '">';
1154   } else {
1155     $text .= <<END;
1156       function acstate_changed(what) {
1157         state = what.options[what.selectedIndex].text;
1158         what.form.popac.options.length = 0
1159         what.form.popac.options[0] = new Option("Area code", "-1", false, true);
1160 END
1161   } 
1162
1163   my @states = $init_popstate ? ( $init_popstate ) : keys %pop;
1164   foreach my $state ( sort { $a cmp $b } @states ) {
1165     $text .= "\nif ( state == \"$state\" ) {\n" unless $init_popstate;
1166
1167     foreach my $ac ( sort { $a cmp $b } keys %{ $pop{$state} }) {
1168       $text .= "opt(what.form.popac, \"$ac\", \"$ac\");\n";
1169       if ($ac eq $param->{'popac'}) {
1170         $text .= "what.form.popac.options[what.form.popac.length-1].selected = true;\n";
1171       }
1172     }
1173     $text .= "}\n" unless $init_popstate;
1174   }
1175   $text .= "popac_changed(what.form.popac)}\n";
1176
1177   $text .= <<END;
1178   function popac_changed(what) {
1179     ac = what.options[what.selectedIndex].text;
1180     what.form.popnum.options.length = 0;
1181     what.form.popnum.options[0] = new Option("City", "-1", false, true);
1182
1183 END
1184
1185   foreach my $state ( @states ) {
1186     foreach my $popac ( keys %{ $pop{$state} } ) {
1187       $text .= "\nif ( ac == \"$popac\" ) {\n";
1188
1189       foreach my $pop ( @{$pop{$state}->{$popac}}) {
1190         my $o_popnum = $pop->{popnum};
1191         my $poptext =  $pop->{city}. ', '. $pop->{state}.
1192                        ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1193
1194         $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n";
1195         if ($popnum == $o_popnum) {
1196           $text .= "what.form.popnum.options[what.form.popnum.length-1].selected = true;\n";
1197         }
1198       }
1199       $text .= "}\n";
1200     }
1201   }
1202
1203
1204   $text .= "}\n</SCRIPT>\n";
1205
1206   $text .=
1207     qq!<TABLE CELLPADDING="0"><TR><TD><SELECT NAME="acstate"! .
1208     qq!SIZE=1 onChange="acstate_changed(this)"><OPTION VALUE=-1>State!;
1209   $text .= "<OPTION" . ($_ eq $param->{'acstate'} ? " SELECTED" : "") .
1210            ">$_" foreach sort { $a cmp $b } @states;
1211   $text .= '</SELECT>'; #callback? return 3 html pieces?  #'</TD>';
1212
1213   $text .=
1214     qq!<SELECT NAME="popac" SIZE=1 onChange="popac_changed(this)">!.
1215     qq!<OPTION>Area code</SELECT></TR><TR VALIGN="top">!;
1216
1217   $text .= qq!<TR><TD><SELECT NAME="popnum" SIZE=1 STYLE="width: 20em"><OPTION>City!;
1218
1219
1220   #comment this block to disable initial list polulation
1221   my @initial_select = ();
1222   if ( scalar( @$pops ) > 100 ) {
1223     push @initial_select, $popnum2pop{$popnum} if $popnum2pop{$popnum};
1224   } else {
1225     @initial_select = @$pops;
1226   }
1227   foreach my $pop ( sort { $a->{state} cmp $b->{state} } @initial_select ) {
1228     $text .= qq!<OPTION VALUE="!. $pop->{popnum}. '"'.
1229              ( ( $popnum && $pop->{popnum} == $popnum ) ? ' SELECTED' : '' ). ">".
1230              $pop->{city}. ', '. $pop->{state}.
1231                ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1232   }
1233
1234   $text .= qq!</SELECT></TD></TR></TABLE>!;
1235
1236   $text;
1237
1238 }
1239
1240 =back
1241
1242 =head1 RESELLER FUNCTIONS
1243
1244 Note: Resellers can also use the B<signup_info> and B<new_customer> functions
1245 with their active session, and the B<customer_info> and B<order_pkg> functions
1246 with their active session and an additional I<custnum> parameter.
1247
1248 =over 4
1249
1250 =item agent_login
1251
1252 =item agent_info
1253
1254 =item agent_list_customers
1255
1256 =back
1257
1258 =head1 BUGS
1259
1260 =head1 SEE ALSO
1261
1262 L<freeside-selfservice-clientd>, L<freeside-selfservice-server>
1263
1264 =cut
1265
1266 1;
1267