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