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