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