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