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