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