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