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