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