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