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