doc: amount param to process_payment. just a bit important.
[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   '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 amount
471
472 Amount
473
474 =item save
475
476 If true, address and card information entered will be saved for subsequent
477 transactions.
478
479 =item auto
480
481 If true, future credit card payments will be done automatically (sets payby to
482 CARD).  If false, future credit card payments will be done on-demand (sets
483 payby to DCRD).  This option only has meaning if B<save> is set true.  
484
485 =item payname
486
487 Name on card
488
489 =item address1
490
491 Address line one
492
493 =item address2
494
495 Address line two
496
497 =item city
498
499 City
500
501 =item state
502
503 State
504
505 =item zip
506
507 Zip or postal code
508
509 =item payinfo
510
511 Card number
512
513 =item month
514
515 Card expiration month
516
517 =item year
518
519 Card expiration year
520
521 =item paybatch
522
523 Unique transaction identifier, returned from the payment_info function.
524 Prevents multiple charges.
525
526 =back
527
528 Returns a hash reference with a single key, B<error>, empty on success, or an
529 error message on errors
530
531 =item list_pkgs
532
533 Returns package information for this customer.
534
535 Takes a hash reference as parameter with a single key: B<session_id>
536
537 Returns a hash reference containing customer package information.  The hash reference contains the following keys:
538
539 =over 4
540
541 =item cust_pkg HASHREF
542
543 Array reference of hash references, each of which has the fields of a cust_pkg
544 record (see L<FS::cust_pkg>) as well as the fields below.  Note these are not
545 the internal FS:: objects, but hash references of columns and values.
546
547 =over 4
548
549 =item part_pkg fields
550
551 All fields of part_pkg (be careful with this information - it may reveal more
552 about your available packages than you would like users to know in aggregate) 
553
554 =cut
555
556 #XXX pare part_pkg fields down to a more secure subset
557
558 =item part_svc
559
560 An array of hash references, each of which has the following keys:
561
562 =over 4
563
564 =item part_svc fields
565
566 All fields of part_svc (be careful with this information - it may reveal more
567 about your available packages than you would like users to know in aggregate) 
568
569 =cut
570
571 #XXX pare part_svc fields down to a more secure subset
572
573 =back
574
575 =back
576
577 =item error
578
579 Empty on success, or an error message on errors.
580
581 =back
582
583 =item order_pkg
584
585 Orders a package for this customer.
586
587 Takes a hash reference as parameter with the following keys:
588
589 =over 4
590
591 =item session_id
592
593 Session identifier
594
595 =item pkgpart
596
597 pkgpart of package to order
598
599 =item svcpart
600
601 optional svcpart, required only if the package definition does not contain
602 one svc_acct service definition with quantity 1 (it may contain others with
603 quantity >1)
604
605 =item username
606
607 Username
608
609 =item _password
610
611 Password
612
613 =item sec_phrase
614
615 Optional security phrase
616
617 =item popnum
618
619 Optional Access number number
620
621 =back
622
623 Returns a hash reference with a single key, B<error>, empty on success, or an
624 error message on errors.  The special error '_decline' is returned for
625 declined transactions.
626
627 =item cancel_pkg
628
629 Cancels a package for this customer.
630
631 Takes a hash reference as parameter with the following keys:
632
633 =over 4
634
635 =item session_id
636
637 Session identifier
638
639 =item pkgpart
640
641 pkgpart of package to cancel
642
643 =back
644
645 Returns a hash reference with a single key, B<error>, empty on success, or an
646 error message on errors.
647
648 =back
649
650 =head1 SIGNUP FUNCTIONS
651
652 =over 4
653
654 =item signup_info HASHREF
655
656 Takes a hash reference as parameter with the following keys:
657
658 =over 4
659
660 =item session_id - Optional agent/reseller interface session
661
662 =back
663
664 Returns a hash reference containing information that may be useful in
665 displaying a signup page.  The hash reference contains the following keys:
666
667 =over 4
668
669 =item cust_main_county
670
671 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.
672
673 =item part_pkg
674
675 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
676 an agentnum specified explicitly via reseller interface session_id in the
677 options.
678
679 =item agent
680
681 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.
682
683 =item agentnum2part_pkg
684
685 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.
686
687 =item svc_acct_pop
688
689 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.
690
691 =item security_phrase
692
693 True if the "security_phrase" feature is enabled
694
695 =item payby
696
697 Array reference of acceptable payment types for signup
698
699 =over 4
700
701 =item CARD
702
703 credit card - automatic
704
705 =item DCRD
706
707 credit card - on-demand - version 1.5+ only
708
709 =item CHEK
710
711 electronic check - automatic
712
713 =item DCHK
714
715 electronic check - on-demand - version 1.5+ only
716
717 =item LECB
718
719 Phone bill billing
720
721 =item BILL
722
723 billing, not recommended for signups
724
725 =item COMP
726
727 free, definitely not recommended for signups
728
729 =item PREPAY
730
731 special billing type: applies a credit (see FS::prepay_credit) and sets billing type to BILL
732
733 =back
734
735 =item cvv_enabled
736
737 True if CVV features are available (1.5+ or 1.4.2 with CVV schema patch)
738
739 =item msgcat
740
741 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".
742
743 =item statedefault
744
745 Default state
746
747 =item countrydefault
748
749 Default country
750
751 =back
752
753 =item new_customer HASHREF
754
755 Creates a new customer.  Takes a hash reference as parameter with the
756 following keys:
757
758 =over 4
759
760 =item first
761
762 first name (required)
763
764 =item last
765
766 last name (required)
767
768 =item ss
769
770 (not typically collected; mostly used for ACH transactions)
771
772 =item company
773
774 Company name
775
776 =item address1 (required)
777
778 Address line one
779
780 =item address2
781
782 Address line two
783
784 =item city (required)
785
786 City
787
788 =item county
789
790 County
791
792 =item state (required)
793
794 State
795
796 =item zip (required)
797
798 Zip or postal code
799
800 =item daytime
801
802 Daytime phone number
803
804 =item night
805
806 Evening phone number
807
808 =item fax
809
810 Fax number
811
812 =item payby
813
814 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY (see L</signup_info> (required)
815
816 =item payinfo
817
818 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
819
820 =item paycvv
821
822 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
823
824 =item paydate
825
826 Expiration date for CARD/DCRD
827
828 =item payname
829
830 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
831
832 =item invoicing_list
833
834 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),
835
836 =item referral_custnum
837
838 referring customer number
839
840 =item pkgpart
841
842 pkgpart of initial package
843
844 =item username
845
846 Username
847
848 =item _password
849
850 Password
851
852 =item sec_phrase
853
854 Security phrase
855
856 =item popnum
857
858 Access number (index, not the literal number)
859
860 =item agentnum
861
862 Agent number
863
864 =back
865
866 Returns a hash reference with the following keys:
867
868 =over 4
869
870 =item error
871
872 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)
873
874 =back
875
876 =item regionselector HASHREF | LIST
877
878 Takes as input a hashref or list of key/value pairs with the following keys:
879
880 =over 4
881
882 =item selected_county
883
884 Currently selected county
885
886 =item selected_state
887
888 Currently selected state
889
890 =item selected_country
891
892 Currently selected country
893
894 =item prefix
895
896 Specify a unique prefix string  if you intend to use the HTML output multiple time son one page.
897
898 =item onchange
899
900 Specify a javascript subroutine to call on changes
901
902 =item default_state
903
904 Default state
905
906 =item default_country
907
908 Default country
909
910 =item locales
911
912 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>.
913
914 =back
915
916 Returns a list consisting of three HTML fragments for county selection,
917 state selection and country selection, respectively.
918
919 =cut
920
921 #false laziness w/FS::cust_main_county (this is currently the "newest" version)
922 sub regionselector {
923   my $param;
924   if ( ref($_[0]) ) {
925     $param = shift;
926   } else {
927     $param = { @_ };
928   }
929   $param->{'selected_country'} ||= $param->{'default_country'};
930   $param->{'selected_state'} ||= $param->{'default_state'};
931
932   my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
933
934   my $countyflag = 0;
935
936   my %cust_main_county;
937
938 #  unless ( @cust_main_county ) { #cache 
939     #@cust_main_county = qsearch('cust_main_county', {} );
940     #foreach my $c ( @cust_main_county ) {
941     foreach my $c ( @{ $param->{'locales'} } ) {
942       #$countyflag=1 if $c->county;
943       $countyflag=1 if $c->{county};
944       #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
945       #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
946       $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
947     }
948 #  }
949   $countyflag=1 if $param->{selected_county};
950
951   my $script_html = <<END;
952     <SCRIPT>
953     function opt(what,value,text) {
954       var optionName = new Option(text, value, false, false);
955       var length = what.length;
956       what.options[length] = optionName;
957     }
958     function ${prefix}country_changed(what) {
959       country = what.options[what.selectedIndex].text;
960       for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
961           what.form.${prefix}state.options[i] = null;
962 END
963       #what.form.${prefix}state.options[0] = new Option('', '', false, true);
964
965   foreach my $country ( sort keys %cust_main_county ) {
966     $script_html .= "\nif ( country == \"$country\" ) {\n";
967     foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
968       my $text = $state || '(n/a)';
969       $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
970     }
971     $script_html .= "}\n";
972   }
973
974   $script_html .= <<END;
975     }
976     function ${prefix}state_changed(what) {
977 END
978
979   if ( $countyflag ) {
980     $script_html .= <<END;
981       state = what.options[what.selectedIndex].text;
982       country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
983       for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
984           what.form.${prefix}county.options[i] = null;
985 END
986
987     foreach my $country ( sort keys %cust_main_county ) {
988       $script_html .= "\nif ( country == \"$country\" ) {\n";
989       foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
990         $script_html .= "\nif ( state == \"$state\" ) {\n";
991           #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
992           foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
993             my $text = $county || '(n/a)';
994             $script_html .=
995               qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
996           }
997         $script_html .= "}\n";
998       }
999       $script_html .= "}\n";
1000     }
1001   }
1002
1003   $script_html .= <<END;
1004     }
1005     </SCRIPT>
1006 END
1007
1008   my $county_html = $script_html;
1009   if ( $countyflag ) {
1010     $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
1011     $county_html .= '</SELECT>';
1012   } else {
1013     $county_html .=
1014       qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$param->{'selected_county'}">!;
1015   }
1016
1017   my $state_html = qq!<SELECT NAME="${prefix}state" !.
1018                    qq!onChange="${prefix}state_changed(this); $param->{'onchange'}">!;
1019   foreach my $state ( sort keys %{ $cust_main_county{$param->{'selected_country'}} } ) {
1020     my $text = $state || '(n/a)';
1021     my $selected = $state eq $param->{'selected_state'} ? 'SELECTED' : '';
1022     $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
1023   }
1024   $state_html .= '</SELECT>';
1025
1026   $state_html .= '</SELECT>';
1027
1028   my $country_html = qq!<SELECT NAME="${prefix}country" !.
1029                      qq!onChange="${prefix}country_changed(this); $param->{'onchange'}">!;
1030   my $countrydefault = $param->{default_country} || 'US';
1031   foreach my $country (
1032     sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
1033       keys %cust_main_county
1034   ) {
1035     my $selected = $country eq $param->{'selected_country'} ? ' SELECTED' : '';
1036     $country_html .= "\n<OPTION$selected>$country</OPTION>"
1037   }
1038   $country_html .= '</SELECT>';
1039
1040   ($county_html, $state_html, $country_html);
1041
1042 }
1043
1044 #=item expselect HASHREF | LIST
1045 #
1046 #Takes as input a hashref or list of key/value pairs with the following keys:
1047 #
1048 #=over 4
1049 #
1050 #=item prefix - Specify a unique prefix string  if you intend to use the HTML output multiple time son one page.
1051 #
1052 #=item date - current date, in yyyy-mm-dd or m-d-yyyy format
1053 #
1054 #=back
1055
1056 =item expselect PREFIX [ DATE ]
1057
1058 Takes as input a unique prefix string and the current expiration date, in
1059 yyyy-mm-dd or m-d-yyyy format
1060
1061 Returns an HTML fragments for expiration date selection.
1062
1063 =cut
1064
1065 sub expselect {
1066   #my $param;
1067   #if ( ref($_[0]) ) {
1068   #  $param = shift;
1069   #} else {
1070   #  $param = { @_ };
1071   #my $prefix = $param->{'prefix'};
1072   #my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
1073   #my $date =   exists($param->{'date'})   ? $param->{'date'}   : '';
1074   my $prefix = shift;
1075   my $date = scalar(@_) ? shift : '';
1076
1077   my( $m, $y ) = ( 0, 0 );
1078   if ( $date  =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #PostgreSQL date format
1079     ( $m, $y ) = ( $2, $1 );
1080   } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
1081     ( $m, $y ) = ( $1, $3 );
1082   }
1083   my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
1084   for ( 1 .. 12 ) {
1085     $return .= qq!<OPTION VALUE="$_"!;
1086     $return .= " SELECTED" if $_ == $m;
1087     $return .= ">$_";
1088   }
1089   $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
1090   my @t = localtime;
1091   my $thisYear = $t[5] + 1900;
1092   for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. ($thisYear+10) ) {
1093     $return .= qq!<OPTION VALUE="$_"!;
1094     $return .= " SELECTED" if $_ == $y;
1095     $return .= ">$_";
1096   }
1097   $return .= "</SELECT>";
1098
1099   $return;
1100 }
1101
1102 =item popselector HASHREF | LIST
1103
1104 Takes as input a hashref or list of key/value pairs with the following keys:
1105
1106 =over 4
1107
1108 =item popnum
1109
1110 Access number number
1111
1112 =item pops
1113
1114 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>.
1115
1116 =back
1117
1118 Returns an HTML fragment for access number selection.
1119
1120 =cut
1121
1122 #horrible false laziness with FS/FS/svc_acct_pop.pm::popselector
1123 sub popselector {
1124   my $param;
1125   if ( ref($_[0]) ) {
1126     $param = shift;
1127   } else {
1128     $param = { @_ };
1129   }
1130   my $popnum = $param->{'popnum'};
1131   my $pops = $param->{'pops'};
1132
1133   return '<INPUT TYPE="hidden" NAME="popnum" VALUE="">' unless @$pops;
1134   return $pops->[0]{city}. ', '. $pops->[0]{state}.
1135          ' ('. $pops->[0]{ac}. ')/'. $pops->[0]{exch}. '-'. $pops->[0]{loc}.
1136          '<INPUT TYPE="hidden" NAME="popnum" VALUE="'. $pops->[0]{popnum}. '">'
1137     if scalar(@$pops) == 1;
1138
1139   my %pop = ();
1140   my %popnum2pop = ();
1141   foreach (@$pops) {
1142     push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
1143     $popnum2pop{$_->{popnum}} = $_;
1144   }
1145
1146   my $text = <<END;
1147     <SCRIPT>
1148     function opt(what,href,text) {
1149       var optionName = new Option(text, href, false, false)
1150       var length = what.length;
1151       what.options[length] = optionName;
1152     }
1153 END
1154
1155   my $init_popstate = $param->{'init_popstate'};
1156   if ( $init_popstate ) {
1157     $text .= '<INPUT TYPE="hidden" NAME="init_popstate" VALUE="'.
1158              $init_popstate. '">';
1159   } else {
1160     $text .= <<END;
1161       function acstate_changed(what) {
1162         state = what.options[what.selectedIndex].text;
1163         what.form.popac.options.length = 0
1164         what.form.popac.options[0] = new Option("Area code", "-1", false, true);
1165 END
1166   } 
1167
1168   my @states = $init_popstate ? ( $init_popstate ) : keys %pop;
1169   foreach my $state ( sort { $a cmp $b } @states ) {
1170     $text .= "\nif ( state == \"$state\" ) {\n" unless $init_popstate;
1171
1172     foreach my $ac ( sort { $a cmp $b } keys %{ $pop{$state} }) {
1173       $text .= "opt(what.form.popac, \"$ac\", \"$ac\");\n";
1174       if ($ac eq $param->{'popac'}) {
1175         $text .= "what.form.popac.options[what.form.popac.length-1].selected = true;\n";
1176       }
1177     }
1178     $text .= "}\n" unless $init_popstate;
1179   }
1180   $text .= "popac_changed(what.form.popac)}\n";
1181
1182   $text .= <<END;
1183   function popac_changed(what) {
1184     ac = what.options[what.selectedIndex].text;
1185     what.form.popnum.options.length = 0;
1186     what.form.popnum.options[0] = new Option("City", "-1", false, true);
1187
1188 END
1189
1190   foreach my $state ( @states ) {
1191     foreach my $popac ( keys %{ $pop{$state} } ) {
1192       $text .= "\nif ( ac == \"$popac\" ) {\n";
1193
1194       foreach my $pop ( @{$pop{$state}->{$popac}}) {
1195         my $o_popnum = $pop->{popnum};
1196         my $poptext =  $pop->{city}. ', '. $pop->{state}.
1197                        ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1198
1199         $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n";
1200         if ($popnum == $o_popnum) {
1201           $text .= "what.form.popnum.options[what.form.popnum.length-1].selected = true;\n";
1202         }
1203       }
1204       $text .= "}\n";
1205     }
1206   }
1207
1208
1209   $text .= "}\n</SCRIPT>\n";
1210
1211   $text .=
1212     qq!<TABLE CELLPADDING="0"><TR><TD><SELECT NAME="acstate"! .
1213     qq!SIZE=1 onChange="acstate_changed(this)"><OPTION VALUE=-1>State!;
1214   $text .= "<OPTION" . ($_ eq $param->{'acstate'} ? " SELECTED" : "") .
1215            ">$_" foreach sort { $a cmp $b } @states;
1216   $text .= '</SELECT>'; #callback? return 3 html pieces?  #'</TD>';
1217
1218   $text .=
1219     qq!<SELECT NAME="popac" SIZE=1 onChange="popac_changed(this)">!.
1220     qq!<OPTION>Area code</SELECT></TR><TR VALIGN="top">!;
1221
1222   $text .= qq!<TR><TD><SELECT NAME="popnum" SIZE=1 STYLE="width: 20em"><OPTION>City!;
1223
1224
1225   #comment this block to disable initial list polulation
1226   my @initial_select = ();
1227   if ( scalar( @$pops ) > 100 ) {
1228     push @initial_select, $popnum2pop{$popnum} if $popnum2pop{$popnum};
1229   } else {
1230     @initial_select = @$pops;
1231   }
1232   foreach my $pop ( sort { $a->{state} cmp $b->{state} } @initial_select ) {
1233     $text .= qq!<OPTION VALUE="!. $pop->{popnum}. '"'.
1234              ( ( $popnum && $pop->{popnum} == $popnum ) ? ' SELECTED' : '' ). ">".
1235              $pop->{city}. ', '. $pop->{state}.
1236                ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1237   }
1238
1239   $text .= qq!</SELECT></TD></TR></TABLE>!;
1240
1241   $text;
1242
1243 }
1244
1245 =back
1246
1247 =head1 RESELLER FUNCTIONS
1248
1249 Note: Resellers can also use the B<signup_info> and B<new_customer> functions
1250 with their active session, and the B<customer_info> and B<order_pkg> functions
1251 with their active session and an additional I<custnum> parameter.
1252
1253 =over 4
1254
1255 =item agent_login
1256
1257 =item agent_info
1258
1259 =item agent_list_customers
1260
1261 =back
1262
1263 =head1 BUGS
1264
1265 =head1 SEE ALSO
1266
1267 L<freeside-selfservice-clientd>, L<freeside-selfservice-server>
1268
1269 =cut
1270
1271 1;
1272