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