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