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