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