99cbed8c7f184203020f56a4e022cfdc976c345a
[freeside.git] / fs_selfservice / FS-SelfService / SelfService.pm
1 package FS::SelfService;
2
3 use strict;
4 use vars qw( $VERSION @ISA @EXPORT_OK $DEBUG
5              $skip_uid_check $dir $socket %autoload $tag );
6 use Exporter;
7 use Socket;
8 use FileHandle;
9 #use IO::Handle;
10 use IO::Select;
11 use Storable 2.09 qw(nstore_fd fd_retrieve);
12
13 $VERSION = '0.03';
14
15 @ISA = qw( Exporter );
16
17 $DEBUG = 0;
18
19 $dir = "/usr/local/freeside";
20 $socket =  "$dir/selfservice_socket";
21 $socket .= '.'.$tag if defined $tag && length($tag);
22
23 #maybe should ask ClientAPI for this list
24 %autoload = (
25   'passwd'                    => 'passwd/passwd',
26   'chfn'                      => 'passwd/passwd',
27   'chsh'                      => 'passwd/passwd',
28   'login_info'                => 'MyAccount/login_info',
29   'login_banner_image'        => 'MyAccount/login_banner_image',
30   'login'                     => 'MyAccount/login',
31   'logout'                    => 'MyAccount/logout',
32   'switch_acct'               => 'MyAccount/switch_acct',
33   'customer_info'             => 'MyAccount/customer_info',
34   'customer_info_short'       => 'MyAccount/customer_info_short',
35   'billing_history'           => 'MyAccount/billing_history',
36   'edit_info'                 => 'MyAccount/edit_info',     #add to ss cgi!
37   'invoice'                   => 'MyAccount/invoice',
38   'invoice_pdf'               => 'MyAccount/invoice_pdf',
39   'legacy_invoice'            => 'MyAccount/legacy_invoice',
40   'legacy_invoice_pdf'        => 'MyAccount/legacy_invoice_pdf',
41   'invoice_logo'              => 'MyAccount/invoice_logo',
42   'list_invoices'             => 'MyAccount/list_invoices', #?
43   'cancel'                    => 'MyAccount/cancel',        #add to ss cgi!
44   'payment_info'              => 'MyAccount/payment_info',
45   'payment_info_renew_info'   => 'MyAccount/payment_info_renew_info',
46   'process_payment'           => 'MyAccount/process_payment',
47   'store_payment'             => 'MyAccount/store_payment',
48   'process_stored_payment'    => 'MyAccount/process_stored_payment',
49   'process_payment_order_pkg' => 'MyAccount/process_payment_order_pkg',
50   'process_payment_change_pkg' => 'MyAccount/process_payment_change_pkg',
51   'process_payment_order_renew' => 'MyAccount/process_payment_order_renew',
52   'process_prepay'            => 'MyAccount/process_prepay',
53   'realtime_collect'          => 'MyAccount/realtime_collect',
54   'list_pkgs'                 => 'MyAccount/list_pkgs',     #add to ss (added?)
55   'list_svcs'                 => 'MyAccount/list_svcs',     #add to ss (added?)
56   'list_svc_usage'            => 'MyAccount/list_svc_usage',   
57   'svc_status_html'           => 'MyAccount/svc_status_html',
58   'svc_status_hash'           => 'MyAccount/svc_status_hash',
59   'set_svc_status_hash'       => 'MyAccount/set_svc_status_hash',
60   'set_svc_status_listadd'    => 'MyAccount/set_svc_status_listadd',
61   'set_svc_status_listdel'    => 'MyAccount/set_svc_status_listdel',
62   'set_svc_status_vacationadd'=> 'MyAccount/set_svc_status_vacationadd',
63   'set_svc_status_vacationdel'=> 'MyAccount/set_svc_status_vacationdel',
64   'acct_forward_info'         => 'MyAccount/acct_forward_info',
65   'process_acct_forward'      => 'MyAccount/process_acct_forward',
66   'list_dsl_devices'          => 'MyAccount/list_dsl_devices',   
67   'add_dsl_device'            => 'MyAccount/add_dsl_device',   
68   'delete_dsl_device'         => 'MyAccount/delete_dsl_device',   
69   'port_graph'                => 'MyAccount/port_graph',   
70   'list_cdr_usage'            => 'MyAccount/list_cdr_usage',   
71   'list_support_usage'        => 'MyAccount/list_support_usage',   
72   'order_pkg'                 => 'MyAccount/order_pkg',     #add to ss cgi!
73   'change_pkg'                => 'MyAccount/change_pkg', 
74   'order_recharge'            => 'MyAccount/order_recharge',
75   'renew_info'                => 'MyAccount/renew_info',
76   'order_renew'               => 'MyAccount/order_renew',
77   'cancel_pkg'                => 'MyAccount/cancel_pkg',    #add to ss cgi!
78   'suspend_pkg'               => 'MyAccount/suspend_pkg',   #add to ss cgi!
79   'charge'                    => 'MyAccount/charge',        #?
80   'part_svc_info'             => 'MyAccount/part_svc_info',
81   'provision_acct'            => 'MyAccount/provision_acct',
82   'provision_phone'           => 'MyAccount/provision_phone',
83   'provision_external'        => 'MyAccount/provision_external',
84   'unprovision_svc'           => 'MyAccount/unprovision_svc',
85   'myaccount_passwd'          => 'MyAccount/myaccount_passwd',
86   'reset_passwd'              => 'MyAccount/reset_passwd',
87   'check_reset_passwd'        => 'MyAccount/check_reset_passwd',
88   'process_reset_passwd'      => 'MyAccount/process_reset_passwd',
89   'list_tickets'              => 'MyAccount/list_tickets',
90   'create_ticket'             => 'MyAccount/create_ticket',
91   'get_ticket'                => 'MyAccount/get_ticket',
92   'adjust_ticket_priority'    => 'MyAccount/adjust_ticket_priority',
93   'did_report'                => 'MyAccount/did_report',
94   'signup_info'               => 'Signup/signup_info',
95   'skin_info'                 => 'MyAccount/skin_info',
96   'access_info'               => 'MyAccount/access_info',
97   'domain_select_hash'        => 'Signup/domain_select_hash',  # expose?
98   'new_customer'              => 'Signup/new_customer',
99   'capture_payment'           => 'Signup/capture_payment',
100   #N/A 'clear_signup_cache'        => 'Signup/clear_cache',
101   'new_agent'                 => 'Agent/new_agent',
102   'agent_login'               => 'Agent/agent_login',
103   'agent_logout'              => 'Agent/agent_logout',
104   'agent_info'                => 'Agent/agent_info',
105   'agent_list_customers'      => 'Agent/agent_list_customers',
106   'check_username'            => 'Agent/check_username',
107   'suspend_username'          => 'Agent/suspend_username',
108   'unsuspend_username'        => 'Agent/unsuspend_username',
109   'mason_comp'                => 'MasonComponent/mason_comp',
110   'call_time'                 => 'PrepaidPhone/call_time',
111   'call_time_nanpa'           => 'PrepaidPhone/call_time_nanpa',
112   'phonenum_balance'          => 'PrepaidPhone/phonenum_balance',
113 );
114 @EXPORT_OK = (
115   keys(%autoload),
116   qw( regionselector regionselector_hashref location_form
117       expselect popselector domainselector didselector
118     )
119 );
120
121 $ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
122 $ENV{'SHELL'} = '/bin/sh';
123 $ENV{'IFS'} = " \t\n";
124 $ENV{'CDPATH'} = '';
125 $ENV{'ENV'} = '';
126 $ENV{'BASH_ENV'} = '';
127
128 #you can add BEGIN { $FS::SelfService::skip_uid_check = 1; } 
129 #if you grant appropriate permissions to whatever user
130 my $freeside_uid = scalar(getpwnam('freeside'));
131 die "not running as the freeside user\n"
132   if $> != $freeside_uid && ! $skip_uid_check;
133
134 -e $dir or die "FATAL: $dir doesn't exist!";
135 -d $dir or die "FATAL: $dir isn't a directory!";
136 -r $dir or die "FATAL: Can't read $dir as freeside user!";
137 -x $dir or die "FATAL: $dir not searchable (executable) as freeside user!";
138
139 foreach my $autoload ( keys %autoload ) {
140
141   my $eval =
142   "sub $autoload { ". '
143                    my $param;
144                    if ( ref($_[0]) ) {
145                      $param = shift;
146                    } else {
147                      #warn scalar(@_). ": ". join(" / ", @_);
148                      $param = { @_ };
149                    }
150
151                    $param->{_packet} = \''. $autoload{$autoload}. '\';
152
153                    simple_packet($param);
154                  }';
155
156   eval $eval;
157   die $@ if $@;
158
159 }
160
161 sub simple_packet {
162   my $packet = shift;
163   warn "sending ". $packet->{_packet}. " to server"
164     if $DEBUG;
165   socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
166   connect(SOCK, sockaddr_un($socket)) or die "connect to $socket: $!";
167   nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
168   SOCK->flush;
169
170   #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
171
172   #block until there is a message on socket
173 #  my $w = new IO::Select;
174 #  $w->add(\*SOCK);
175 #  my @wait = $w->can_read;
176
177   warn "reading message from server"
178     if $DEBUG;
179
180   my $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
181   die $return->{'_error'} if defined $return->{_error} && $return->{_error};
182
183   warn "returning message to client"
184     if $DEBUG;
185
186   $return;
187 }
188
189 =head1 NAME
190
191 FS::SelfService - Freeside self-service API
192
193 =head1 SYNOPSIS
194
195   # password and shell account changes
196   use FS::SelfService qw(passwd chfn chsh);
197
198   # "my account" functionality
199   use FS::SelfService qw( login customer_info invoice cancel payment_info process_payment );
200
201   my $rv = login( { 'username' => $username,
202                     'domain'   => $domain,
203                     'password' => $password,
204                   }
205                 );
206
207   if ( $rv->{'error'} ) {
208     #handle login error...
209   } else {
210     #successful login
211     my $session_id = $rv->{'session_id'};
212   }
213
214   my $customer_info = customer_info( { 'session_id' => $session_id } );
215
216   #payment_info and process_payment are available in 1.5+ only
217   my $payment_info = payment_info( { 'session_id' => $session_id } );
218
219   #!!! process_payment example
220
221   #!!! list_pkgs example
222
223   #!!! order_pkg example
224
225   #!!! cancel_pkg example
226
227   # signup functionality
228   use FS::SelfService qw( signup_info new_customer );
229
230   my $signup_info = signup_info;
231
232   $rv = new_customer( {
233                         'first'            => $first,
234                         'last'             => $last,
235                         'company'          => $company,
236                         'address1'         => $address1,
237                         'address2'         => $address2,
238                         'city'             => $city,
239                         'state'            => $state,
240                         'zip'              => $zip,
241                         'country'          => $country,
242                         'daytime'          => $daytime,
243                         'night'            => $night,
244                         'fax'              => $fax,
245                         'payby'            => $payby,
246                         'payinfo'          => $payinfo,
247                         'paycvv'           => $paycvv,
248                         'paystart_month'   => $paystart_month
249                         'paystart_year'    => $paystart_year,
250                         'payissue'         => $payissue,
251                         'payip'            => $payip
252                         'paydate'          => $paydate,
253                         'payname'          => $payname,
254                         'invoicing_list'   => $invoicing_list,
255                         'referral_custnum' => $referral_custnum,
256                         'agentnum'         => $agentnum,
257                         'pkgpart'          => $pkgpart,
258
259                         'username'         => $username,
260                         '_password'        => $password,
261                         'popnum'           => $popnum,
262                         #OR
263                         'countrycode'      => 1,
264                         'phonenum'         => $phonenum,
265                         'pin'              => $pin,
266                       }
267                     );
268   
269   my $error = $rv->{'error'};
270   if ( $error eq '_decline' ) {
271     print_decline();
272   } elsif ( $error ) {
273     reprint_signup();
274   } else {
275     print_success();
276   }
277
278 =head1 DESCRIPTION
279
280 Use this API to implement your own client "self-service" module.
281
282 If you just want to customize the look of the existing "self-service" module,
283 see XXXX instead.
284
285 =head1 PASSWORD, GECOS, SHELL CHANGING FUNCTIONS
286
287 =over 4
288
289 =item passwd
290
291 =item chfn
292
293 =item chsh
294
295 =back
296
297 =head1 "MY ACCOUNT" FUNCTIONS
298
299 =over 4
300
301 =item login HASHREF
302
303 Creates a user session.  Takes a hash reference as parameter with the
304 following keys:
305
306 =over 4
307
308 =item username
309
310 Username
311
312 =item domain
313
314 Domain
315
316 =item password
317
318 Password
319
320 =back
321
322 Returns a hash reference with the following keys:
323
324 =over 4
325
326 =item error
327
328 Empty on success, or an error message on errors.
329
330 =item session_id
331
332 Session identifier for successful logins
333
334 =back
335
336 =item customer_info HASHREF
337
338 Returns general customer information.
339
340 Takes a hash reference as parameter with a single key: B<session_id>
341
342 Returns a hash reference with the following keys:
343
344 =over 4
345
346 =item name
347
348 Customer name
349
350 =item balance
351
352 Balance owed
353
354 =item open
355
356 Array reference of hash references of open inoices.  Each hash reference has
357 the following keys: invnum, date, owed
358
359 =item small_custview
360
361 An HTML fragment containing shipping and billing addresses.
362
363 =item The following fields are also returned
364
365 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
366
367 =back
368
369 =item edit_info HASHREF
370
371 Takes a hash reference as parameter with any of the following keys:
372
373 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
374
375 If a field exists, the customer record is updated with the new value of that
376 field.  If a field does not exist, that field is not changed on the customer
377 record.
378
379 Returns a hash reference with a single key, B<error>, empty on success, or an
380 error message on errors
381
382 =item invoice HASHREF
383
384 Returns an invoice.  Takes a hash reference as parameter with two keys:
385 session_id and invnum
386
387 Returns a hash reference with the following keys:
388
389 =over 4
390
391 =item error
392
393 Empty on success, or an error message on errors
394
395 =item invnum
396
397 Invoice number
398
399 =item invoice_text
400
401 Invoice text
402
403 =back
404
405 =item list_invoices HASHREF
406
407 Returns a list of all customer invoices.  Takes a hash references with a single
408 key, session_id.
409
410 Returns a hash reference with the following keys:
411
412 =over 4
413
414 =item error
415
416 Empty on success, or an error message on errors
417
418 =item invoices
419
420 Reference to array of hash references with the following keys:
421
422 =over 4
423
424 =item invnum
425
426 Invoice ID
427
428 =item _date
429
430 Invoice date, in UNIX epoch time
431
432 =back
433
434 =back
435
436 =item cancel HASHREF
437
438 Cancels this customer.
439
440 Takes a hash reference as parameter with a single key: B<session_id>
441
442 Returns a hash reference with a single key, B<error>, which is empty on
443 success or an error message on errors.
444
445 =item payment_info HASHREF
446
447 Returns information that may be useful in displaying a payment page.
448
449 Takes a hash reference as parameter with a single key: B<session_id>.
450
451 Returns a hash reference with the following keys:
452
453 =over 4
454
455 =item error
456
457 Empty on success, or an error message on errors
458
459 =item balance
460
461 Balance owed
462
463 =item payname
464
465 Exact name on credit card (CARD/DCRD)
466
467 =item address1
468
469 Address line one
470
471 =item address2
472
473 Address line two
474
475 =item city
476
477 City
478
479 =item state
480
481 State
482
483 =item zip
484
485 Zip or postal code
486
487 =item payby
488
489 Customer's current default payment type.
490
491 =item card_type
492
493 For CARD/DCRD payment types, the card type (Visa card, MasterCard, Discover card, American Express card, etc.)
494
495 =item payinfo
496
497 For CARD/DCRD payment types, the card number
498
499 =item month
500
501 For CARD/DCRD payment types, expiration month
502
503 =item year
504
505 For CARD/DCRD payment types, expiration year
506
507 =item cust_main_county
508
509 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.
510
511 =item states
512
513 Array reference of all states in the current default country.
514
515 =item card_types
516
517 Hash reference of card types; keys are card types, values are the exact strings
518 passed to the process_payment function
519
520 =cut
521
522 #this doesn't actually work yet
523 #
524 #=item paybatch
525 #
526 #Unique transaction identifier (prevents multiple charges), passed to the
527 #process_payment function
528
529 =back
530
531 =item process_payment HASHREF
532
533 Processes a payment and possible change of address or payment type.  Takes a
534 hash reference as parameter with the following keys:
535
536 =over 4
537
538 =item session_id
539
540 Session identifier
541
542 =item amount
543
544 Amount
545
546 =item save
547
548 If true, address and card information entered will be saved for subsequent
549 transactions.
550
551 =item auto
552
553 If true, future credit card payments will be done automatically (sets payby to
554 CARD).  If false, future credit card payments will be done on-demand (sets
555 payby to DCRD).  This option only has meaning if B<save> is set true.  
556
557 =item payname
558
559 Name on card
560
561 =item address1
562
563 Address line one
564
565 =item address2
566
567 Address line two
568
569 =item city
570
571 City
572
573 =item state
574
575 State
576
577 =item zip
578
579 Zip or postal code
580
581 =item country
582
583 Two-letter country code
584
585 =item payinfo
586
587 Card number
588
589 =item month
590
591 Card expiration month
592
593 =item year
594
595 Card expiration year
596
597 =cut
598
599 #this doesn't actually work yet
600 #
601 #=item paybatch
602 #
603 #Unique transaction identifier, returned from the payment_info function.
604 #Prevents multiple charges.
605
606 =back
607
608 Returns a hash reference with a single key, B<error>, empty on success, or an
609 error message on errors.
610
611 =item process_payment_order_pkg
612
613 Combines the B<process_payment> and B<order_pkg> functions in one step.  If the
614 payment processes sucessfully, the package is ordered.  Takes a hash reference
615 as parameter with the keys of both methods.
616
617 Returns a hash reference with a single key, B<error>, empty on success, or an
618 error message on errors.
619
620 =item process_payment_change_pkg
621
622 Combines the B<process_payment> and B<change_pkg> functions in one step.  If the
623 payment processes sucessfully, the package is ordered.  Takes a hash reference
624 as parameter with the keys of both methods.
625
626 Returns a hash reference with a single key, B<error>, empty on success, or an
627 error message on errors.
628
629
630 =item process_payment_order_renew
631
632 Combines the B<process_payment> and B<order_renew> functions in one step.  If
633 the payment processes sucessfully, the renewal is processed.  Takes a hash
634 reference as parameter with the keys of both methods.
635
636 Returns a hash reference with a single key, B<error>, empty on success, or an
637 error message on errors.
638
639 =item list_pkgs
640
641 Returns package information for this customer.  For more detail on services,
642 see L</list_svcs>.
643
644 Takes a hash reference as parameter with a single key: B<session_id>
645
646 Returns a hash reference containing customer package information.  The hash reference contains the following keys:
647
648 =over 4
649
650 =item custnum
651
652 Customer number
653
654 =item error
655
656 Empty on success, or an error message on errors.
657
658 =item cust_pkg HASHREF
659
660 Array reference of hash references, each of which has the fields of a cust_pkg
661 record (see L<FS::cust_pkg>) as well as the fields below.  Note these are not
662 the internal FS:: objects, but hash references of columns and values.
663
664 =over 4
665
666 =item part_pkg fields
667
668 All fields of part_pkg for this specific cust_pkg (be careful with this
669 information - it may reveal more about your available packages than you would
670 like users to know in aggregate) 
671
672 =cut
673
674 #XXX pare part_pkg fields down to a more secure subset
675
676 =item part_svc
677
678 An array of hash references indicating information on unprovisioned services
679 available for provisioning for this specific cust_pkg.  Each has the following
680 keys:
681
682 =over 4
683
684 =item part_svc fields
685
686 All fields of part_svc (be careful with this information - it may reveal more
687 about your available packages than you would like users to know in aggregate) 
688
689 =cut
690
691 #XXX pare part_svc fields down to a more secure subset
692
693 =back
694
695 =item cust_svc
696
697 An array of hash references indicating information on the customer services
698 already provisioned for this specific cust_pkg.  Each has the following keys:
699
700 =over 4
701
702 =item label
703
704 Array reference with three elements: The first element is the name of this service.  The second element is a meaningful user-specific identifier for the service (i.e. username, domain or mail alias).  The last element is the table name of this service.
705
706 =back
707
708 =item svcnum
709
710 Primary key for this service
711
712 =item svcpart
713
714 Service definition (see L<FS::part_svc>)
715
716 =item pkgnum
717
718 Customer package (see L<FS::cust_pkg>)
719
720 =item overlimit
721
722 Blank if the service is not over limit, or the date the service exceeded its usage limit (as a UNIX timestamp).
723
724 =back
725
726 =back
727
728 =item list_svcs
729
730 Returns service information for this customer.
731
732 Takes a hash reference as parameter with a single key: B<session_id>
733
734 Returns a hash reference containing customer package information.  The hash reference contains the following keys:
735
736 =over 4
737
738 =item custnum
739
740 Customer number
741
742 =item svcs
743
744 An array of hash references indicating information on all of this customer's
745 services.  Each has the following keys:
746
747 =over 4
748
749 =item svcnum
750
751 Primary key for this service
752
753 =item label
754
755 Name of this service
756
757 =item value
758
759 Meaningful user-specific identifier for the service (i.e. username, domain, or
760 mail alias).
761
762 =back
763
764 Account (svc_acct) services also have the following keys:
765
766 =over 4
767
768 =item username
769
770 Username
771
772 =item email
773
774 username@domain
775
776 =item seconds
777
778 Seconds remaining
779
780 =item upbytes
781
782 Upload bytes remaining
783
784 =item downbytes
785
786 Download bytes remaining
787
788 =item totalbytes
789
790 Total bytes remaining
791
792 =item recharge_amount
793
794 Cost of a recharge
795
796 =item recharge_seconds
797
798 Number of seconds gained by recharge
799
800 =item recharge_upbytes
801
802 Number of upload bytes gained by recharge
803
804 =item recharge_downbytes
805
806 Number of download bytes gained by recharge
807
808 =item recharge_totalbytes
809
810 Number of total bytes gained by recharge
811
812 =back
813
814 =back
815
816 =item order_pkg
817
818 Orders a package for this customer.
819
820 Takes a hash reference as parameter with the following keys:
821
822 =over 4
823
824 =item session_id
825
826 Session identifier
827
828 =item pkgpart
829
830 Package to order (see L<FS::part_pkg>).
831
832 =item svcpart
833
834 Service to order (see L<FS::part_svc>).
835
836 Normally optional; required only to provision a non-svc_acct service, or if the
837 package definition does not contain one svc_acct service definition with
838 quantity 1 (it may contain others with quantity >1).  A svcpart of "none" can
839 also be specified to indicate that no initial service should be provisioned.
840
841 =back
842
843 Fields used when provisioning an svc_acct service:
844
845 =over 4
846
847 =item username
848
849 Username
850
851 =item _password
852
853 Password
854
855 =item sec_phrase
856
857 Optional security phrase
858
859 =item popnum
860
861 Optional Access number number
862
863 =back
864
865 Fields used when provisioning an svc_domain service:
866
867 =over 4
868
869 =item domain
870
871 Domain
872
873 =back
874
875 Fields used when provisioning an svc_phone service:
876
877 =over 4
878
879 =item phonenum
880
881 Phone number
882
883 =item pin
884
885 Voicemail PIN
886
887 =item sip_password
888
889 SIP password
890
891 =back
892
893 Fields used when provisioning an svc_external service:
894
895 =over 4
896
897 =item id
898
899 External numeric ID.
900
901 =item title
902
903 External text title.
904
905 =back
906
907 Fields used when provisioning an svc_pbx service:
908
909 =over 4
910
911 =item id
912
913 Numeric ID.
914
915 =item name
916
917 Text name.
918
919 =back
920
921 Returns a hash reference with a single key, B<error>, empty on success, or an
922 error message on errors.  The special error '_decline' is returned for
923 declined transactions.
924
925 =item change_pkg
926
927 Changes a package for this customer.
928
929 Takes a hash reference as parameter with the following keys:
930
931 =over 4
932
933 =item session_id
934
935 Session identifier
936
937 =item pkgnum
938
939 Existing customer package.
940
941 =item pkgpart
942
943 New package to order (see L<FS::part_pkg>).
944
945 =back
946
947 Returns a hash reference with the following keys:
948
949 =over 4
950
951 =item error
952
953 Empty on success, or an error message on errors.  
954
955 =item pkgnum
956
957 On success, the new pkgnum
958
959 =back
960
961
962 =item renew_info
963
964 Provides useful info for early renewals.
965
966 Takes a hash reference as parameter with the following keys:
967
968 =over 4
969
970 =item session_id
971
972 Session identifier
973
974 =back
975
976 Returns a hash reference.  On errors, it contains a single key, B<error>, with
977 the error message.  Otherwise, contains a single key, B<dates>, pointing to
978 an array refernce of hash references.  Each hash reference contains the
979 following keys:
980
981 =over 4
982
983 =item bill_date
984
985 (Future) Bill date.  Indicates a future date for which billing could be run.
986 Specified as a integer UNIX timestamp.  Pass this value to the B<order_renew>
987 function.
988
989 =item bill_date_pretty
990
991 (Future) Bill date as a human-readable string.  (Convenience for display;
992 subject to change, so best not to parse for the date.)
993
994 =item amount
995
996 Base amount which will be charged if renewed early as of this date.
997
998 =item renew_date
999
1000 Renewal date; i.e. even-futher future date at which the customer will be paid
1001 through if the early renewal is completed with the given B<bill-date>.
1002 Specified as a integer UNIX timestamp.
1003
1004 =item renew_date_pretty
1005
1006 Renewal date as a human-readable string.  (Convenience for display;
1007 subject to change, so best not to parse for the date.)
1008
1009 =item pkgnum
1010
1011 Package that will be renewed.
1012
1013 =item expire_date
1014
1015 Expiration date of the package that will be renewed.
1016
1017 =item expire_date_pretty
1018
1019 Expiration date of the package that will be renewed, as a human-readable
1020 string.  (Convenience for display; subject to change, so best not to parse for
1021 the date.)
1022
1023 =back
1024
1025 =item order_renew
1026
1027 Renews this customer early; i.e. runs billing for this customer in advance.
1028
1029 Takes a hash reference as parameter with the following keys:
1030
1031 =over 4
1032
1033 =item session_id
1034
1035 Session identifier
1036
1037 =item date
1038
1039 Integer date as returned by the B<renew_info> function, indicating the advance
1040 date for which to run billing.
1041
1042 =back
1043
1044 Returns a hash reference with a single key, B<error>, empty on success, or an
1045 error message on errors.
1046
1047 =item cancel_pkg
1048
1049 Cancels a package for this customer.
1050
1051 Takes a hash reference as parameter with the following keys:
1052
1053 =over 4
1054
1055 =item session_id
1056
1057 Session identifier
1058
1059 =item pkgpart
1060
1061 pkgpart of package to cancel
1062
1063 =back
1064
1065 Returns a hash reference with a single key, B<error>, empty on success, or an
1066 error message on errors.
1067
1068 =back
1069
1070 =head1 SIGNUP FUNCTIONS
1071
1072 =over 4
1073
1074 =item signup_info HASHREF
1075
1076 Takes a hash reference as parameter with the following keys:
1077
1078 =over 4
1079
1080 =item session_id - Optional agent/reseller interface session
1081
1082 =back
1083
1084 Returns a hash reference containing information that may be useful in
1085 displaying a signup page.  The hash reference contains the following keys:
1086
1087 =over 4
1088
1089 =item cust_main_county
1090
1091 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.
1092
1093 =item part_pkg
1094
1095 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
1096 an agentnum specified explicitly via reseller interface session_id in the
1097 options.
1098
1099 =item agent
1100
1101 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.
1102
1103 =item agentnum2part_pkg
1104
1105 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.
1106
1107 =item svc_acct_pop
1108
1109 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.
1110
1111 =item security_phrase
1112
1113 True if the "security_phrase" feature is enabled
1114
1115 =item payby
1116
1117 Array reference of acceptable payment types for signup
1118
1119 =over 4
1120
1121 =item CARD
1122
1123 credit card - automatic
1124
1125 =item DCRD
1126
1127 credit card - on-demand - version 1.5+ only
1128
1129 =item CHEK
1130
1131 electronic check - automatic
1132
1133 =item DCHK
1134
1135 electronic check - on-demand - version 1.5+ only
1136
1137 =item LECB
1138
1139 Phone bill billing
1140
1141 =item BILL
1142
1143 billing, not recommended for signups
1144
1145 =item COMP
1146
1147 free, definitely not recommended for signups
1148
1149 =item PREPAY
1150
1151 special billing type: applies a credit (see FS::prepay_credit) and sets billing type to BILL
1152
1153 =back
1154
1155 =item cvv_enabled
1156
1157 True if CVV features are available (1.5+ or 1.4.2 with CVV schema patch)
1158
1159 =item msgcat
1160
1161 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".
1162
1163 =item statedefault
1164
1165 Default state
1166
1167 =item countrydefault
1168
1169 Default country
1170
1171 =back
1172
1173 =item new_customer HASHREF
1174
1175 Creates a new customer.  Takes a hash reference as parameter with the
1176 following keys:
1177
1178 =over 4
1179
1180 =item first
1181
1182 first name (required)
1183
1184 =item last
1185
1186 last name (required)
1187
1188 =item ss
1189
1190 (not typically collected; mostly used for ACH transactions)
1191
1192 =item company
1193
1194 Company name
1195
1196 =item address1 (required)
1197
1198 Address line one
1199
1200 =item address2
1201
1202 Address line two
1203
1204 =item city (required)
1205
1206 City
1207
1208 =item county
1209
1210 County
1211
1212 =item state (required)
1213
1214 State
1215
1216 =item zip (required)
1217
1218 Zip or postal code
1219
1220 =item daytime
1221
1222 Daytime phone number
1223
1224 =item night
1225
1226 Evening phone number
1227
1228 =item fax
1229
1230 Fax number
1231
1232 =item payby
1233
1234 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY (see L</signup_info> (required)
1235
1236 =item payinfo
1237
1238 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
1239
1240 =item paycvv
1241
1242 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
1243
1244 =item paydate
1245
1246 Expiration date for CARD/DCRD
1247
1248 =item payname
1249
1250 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
1251
1252 =item invoicing_list
1253
1254 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),
1255
1256 =item referral_custnum
1257
1258 referring customer number
1259
1260 =item agentnum
1261
1262 Agent number
1263
1264 =item pkgpart
1265
1266 pkgpart of initial package
1267
1268 =item username
1269
1270 Username
1271
1272 =item _password
1273
1274 Password
1275
1276 =item sec_phrase
1277
1278 Security phrase
1279
1280 =item popnum
1281
1282 Access number (index, not the literal number)
1283
1284 =item countrycode
1285
1286 Country code (to be provisioned as a service)
1287
1288 =item phonenum
1289
1290 Phone number (to be provisioned as a service)
1291
1292 =item pin
1293
1294 Voicemail PIN
1295
1296 =back
1297
1298 Returns a hash reference with the following keys:
1299
1300 =over 4
1301
1302 =item error
1303
1304 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 Configuration | View/Edit message catalog)
1305
1306 =back
1307
1308 =item regionselector HASHREF | LIST
1309
1310 Takes as input a hashref or list of key/value pairs with the following keys:
1311
1312 =over 4
1313
1314 =item selected_county
1315
1316 Currently selected county
1317
1318 =item selected_state
1319
1320 Currently selected state
1321
1322 =item selected_country
1323
1324 Currently selected country
1325
1326 =item prefix
1327
1328 Specify a unique prefix string  if you intend to use the HTML output multiple time son one page.
1329
1330 =item onchange
1331
1332 Specify a javascript subroutine to call on changes
1333
1334 =item default_state
1335
1336 Default state
1337
1338 =item default_country
1339
1340 Default country
1341
1342 =item locales
1343
1344 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>.
1345
1346 =back
1347
1348 Returns a list consisting of three HTML fragments for county selection,
1349 state selection and country selection, respectively.
1350
1351 =cut
1352
1353 #false laziness w/FS::cust_main_county (this is currently the "newest" version)
1354 sub regionselector {
1355   my $param;
1356   if ( ref($_[0]) ) {
1357     $param = shift;
1358   } else {
1359     $param = { @_ };
1360   }
1361   $param->{'selected_country'} ||= $param->{'default_country'};
1362   $param->{'selected_state'} ||= $param->{'default_state'};
1363
1364   my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
1365
1366   my $countyflag = 0;
1367
1368   my %cust_main_county;
1369
1370 #  unless ( @cust_main_county ) { #cache 
1371     #@cust_main_county = qsearch('cust_main_county', {} );
1372     #foreach my $c ( @cust_main_county ) {
1373     foreach my $c ( @{ $param->{'locales'} } ) {
1374       #$countyflag=1 if $c->county;
1375       $countyflag=1 if $c->{county};
1376       #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
1377       #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
1378       $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
1379     }
1380 #  }
1381   $countyflag=1 if $param->{selected_county};
1382
1383   my $script_html = <<END;
1384     <SCRIPT>
1385     function opt(what,value,text) {
1386       var optionName = new Option(text, value, false, false);
1387       var length = what.length;
1388       what.options[length] = optionName;
1389     }
1390     function ${prefix}country_changed(what) {
1391       country = what.options[what.selectedIndex].text;
1392       for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
1393           what.form.${prefix}state.options[i] = null;
1394 END
1395       #what.form.${prefix}state.options[0] = new Option('', '', false, true);
1396
1397   foreach my $country ( sort keys %cust_main_county ) {
1398     $script_html .= "\nif ( country == \"$country\" ) {\n";
1399     foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
1400       my $text = $state || '(n/a)';
1401       $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
1402     }
1403     $script_html .= "}\n";
1404   }
1405
1406   $script_html .= <<END;
1407     }
1408     function ${prefix}state_changed(what) {
1409 END
1410
1411   if ( $countyflag ) {
1412     $script_html .= <<END;
1413       state = what.options[what.selectedIndex].text;
1414       country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
1415       for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
1416           what.form.${prefix}county.options[i] = null;
1417 END
1418
1419     foreach my $country ( sort keys %cust_main_county ) {
1420       $script_html .= "\nif ( country == \"$country\" ) {\n";
1421       foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
1422         $script_html .= "\nif ( state == \"$state\" ) {\n";
1423           #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
1424           foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
1425             my $text = $county || '(n/a)';
1426             $script_html .=
1427               qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
1428           }
1429         $script_html .= "}\n";
1430       }
1431       $script_html .= "}\n";
1432     }
1433   }
1434
1435   $script_html .= <<END;
1436     }
1437     </SCRIPT>
1438 END
1439
1440   my $county_html = $script_html;
1441   if ( $countyflag ) {
1442     $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
1443     foreach my $county ( 
1444       sort keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}} }
1445     ) {
1446       my $text = $county || '(n/a)';
1447       $county_html .= qq!<OPTION VALUE="$county"!.
1448                       ($county eq $param->{'selected_county'} ? 
1449                         ' SELECTED>' : 
1450                         '>'
1451                       ).
1452                       $text.
1453                       '</OPTION>';
1454     }
1455     $county_html .= '</SELECT>';
1456   } else {
1457     $county_html .=
1458       qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$param->{'selected_county'}">!;
1459   }
1460
1461   my $state_html = qq!<SELECT NAME="${prefix}state" !.
1462                    qq!onChange="${prefix}state_changed(this); $param->{'onchange'}">!;
1463   foreach my $state ( sort keys %{ $cust_main_county{$param->{'selected_country'}} } ) {
1464     my $text = $state || '(n/a)';
1465     my $selected = $state eq $param->{'selected_state'} ? 'SELECTED' : '';
1466     $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
1467   }
1468   $state_html .= '</SELECT>';
1469
1470   my $country_html = '';
1471   if ( scalar( keys %cust_main_county ) > 1 )  {
1472
1473     $country_html = qq(<SELECT NAME="${prefix}country" ).
1474                     qq(onChange="${prefix}country_changed(this); ).
1475                                  $param->{'onchange'}.
1476                                '"'.
1477                       '>';
1478     my $countrydefault = $param->{default_country} || 'US';
1479     foreach my $country (
1480       sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
1481         keys %cust_main_county
1482     ) {
1483       my $selected = $country eq $param->{'selected_country'}
1484                        ? ' SELECTED'
1485                        : '';
1486       $country_html .= "\n<OPTION$selected>$country</OPTION>"
1487     }
1488     $country_html .= '</SELECT>';
1489   } else {
1490
1491     $country_html = qq(<INPUT TYPE="hidden" NAME="${prefix}country" ).
1492                             ' VALUE="'. (keys %cust_main_county )[0]. '">';
1493
1494   }
1495
1496   ($county_html, $state_html, $country_html);
1497
1498 }
1499
1500 sub regionselector_hashref {
1501   my ($county_html, $state_html, $country_html) = regionselector(@_);
1502   {
1503     'county_html'  => $county_html,
1504     'state_html'   => $state_html,
1505     'country_html' => $country_html,
1506   };
1507 }
1508
1509 =item location_form HASHREF | LIST
1510
1511 Takes as input a hashref or list of key/value pairs with the following keys:
1512
1513 =over 4
1514
1515 =item session_id
1516
1517 Current customer session_id
1518
1519 =item no_asterisks
1520
1521 Omit red asterisks from required fields.
1522
1523 =item address1_label
1524
1525 Label for first address line.
1526
1527 =back
1528
1529 Returns an HTML fragment for a location form (address, city, state, zip,
1530 country)
1531
1532 =cut
1533
1534 sub location_form {
1535   my $param;
1536   if ( ref($_[0]) ) {
1537     $param = shift;
1538   } else {
1539     $param = { @_ };
1540   }
1541
1542   my $session_id = delete $param->{'session_id'};
1543
1544   my $rv = mason_comp( 'session_id' => $session_id,
1545                        'comp'       => '/elements/location.html',
1546                        'args'       => [ %$param ],
1547                      );
1548
1549   #hmm.
1550   $rv->{'error'} || $rv->{'output'};
1551
1552 }
1553
1554
1555 #=item expselect HASHREF | LIST
1556 #
1557 #Takes as input a hashref or list of key/value pairs with the following keys:
1558 #
1559 #=over 4
1560 #
1561 #=item prefix - Specify a unique prefix string  if you intend to use the HTML output multiple time son one page.
1562 #
1563 #=item date - current date, in yyyy-mm-dd or m-d-yyyy format
1564 #
1565 #=back
1566
1567 =item expselect PREFIX [ DATE ]
1568
1569 Takes as input a unique prefix string and the current expiration date, in
1570 yyyy-mm-dd or m-d-yyyy format
1571
1572 Returns an HTML fragments for expiration date selection.
1573
1574 =cut
1575
1576 sub expselect {
1577   #my $param;
1578   #if ( ref($_[0]) ) {
1579   #  $param = shift;
1580   #} else {
1581   #  $param = { @_ };
1582   #my $prefix = $param->{'prefix'};
1583   #my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
1584   #my $date =   exists($param->{'date'})   ? $param->{'date'}   : '';
1585   my $prefix = shift;
1586   my $date = scalar(@_) ? shift : '';
1587
1588   my( $m, $y ) = ( 0, 0 );
1589   if ( $date  =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #PostgreSQL date format
1590     ( $m, $y ) = ( $2, $1 );
1591   } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
1592     ( $m, $y ) = ( $1, $3 );
1593   }
1594   my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
1595   for ( 1 .. 12 ) {
1596     $return .= qq!<OPTION VALUE="$_"!;
1597     $return .= " SELECTED" if $_ == $m;
1598     $return .= ">$_";
1599   }
1600   $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
1601   my @t = localtime;
1602   my $thisYear = $t[5] + 1900;
1603   for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. ($thisYear+10) ) {
1604     $return .= qq!<OPTION VALUE="$_"!;
1605     $return .= " SELECTED" if $_ == $y;
1606     $return .= ">$_";
1607   }
1608   $return .= "</SELECT>";
1609
1610   $return;
1611 }
1612
1613 =item popselector HASHREF | LIST
1614
1615 Takes as input a hashref or list of key/value pairs with the following keys:
1616
1617 =over 4
1618
1619 =item popnum
1620
1621 Access number number
1622
1623 =item pops
1624
1625 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>.
1626
1627 =back
1628
1629 Returns an HTML fragment for access number selection.
1630
1631 =cut
1632
1633 #horrible false laziness with FS/FS/svc_acct_pop.pm::popselector
1634 sub popselector {
1635   my $param;
1636   if ( ref($_[0]) ) {
1637     $param = shift;
1638   } else {
1639     $param = { @_ };
1640   }
1641   my $popnum = $param->{'popnum'};
1642   my $pops = $param->{'pops'};
1643
1644   return '<INPUT TYPE="hidden" NAME="popnum" VALUE="">' unless @$pops;
1645   return $pops->[0]{city}. ', '. $pops->[0]{state}.
1646          ' ('. $pops->[0]{ac}. ')/'. $pops->[0]{exch}. '-'. $pops->[0]{loc}.
1647          '<INPUT TYPE="hidden" NAME="popnum" VALUE="'. $pops->[0]{popnum}. '">'
1648     if scalar(@$pops) == 1;
1649
1650   my %pop = ();
1651   my %popnum2pop = ();
1652   foreach (@$pops) {
1653     push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
1654     $popnum2pop{$_->{popnum}} = $_;
1655   }
1656
1657   my $text = <<END;
1658     <SCRIPT>
1659     function opt(what,href,text) {
1660       var optionName = new Option(text, href, false, false)
1661       var length = what.length;
1662       what.options[length] = optionName;
1663     }
1664 END
1665
1666   my $init_popstate = $param->{'init_popstate'};
1667   if ( $init_popstate ) {
1668     $text .= '<INPUT TYPE="hidden" NAME="init_popstate" VALUE="'.
1669              $init_popstate. '">';
1670   } else {
1671     $text .= <<END;
1672       function acstate_changed(what) {
1673         state = what.options[what.selectedIndex].text;
1674         what.form.popac.options.length = 0
1675         what.form.popac.options[0] = new Option("Area code", "-1", false, true);
1676 END
1677   } 
1678
1679   my @states = $init_popstate ? ( $init_popstate ) : keys %pop;
1680   foreach my $state ( sort { $a cmp $b } @states ) {
1681     $text .= "\nif ( state == \"$state\" ) {\n" unless $init_popstate;
1682
1683     foreach my $ac ( sort { $a cmp $b } keys %{ $pop{$state} }) {
1684       $text .= "opt(what.form.popac, \"$ac\", \"$ac\");\n";
1685       if ($ac eq $param->{'popac'}) {
1686         $text .= "what.form.popac.options[what.form.popac.length-1].selected = true;\n";
1687       }
1688     }
1689     $text .= "}\n" unless $init_popstate;
1690   }
1691   $text .= "popac_changed(what.form.popac)}\n";
1692
1693   $text .= <<END;
1694   function popac_changed(what) {
1695     ac = what.options[what.selectedIndex].text;
1696     what.form.popnum.options.length = 0;
1697     what.form.popnum.options[0] = new Option("City", "-1", false, true);
1698
1699 END
1700
1701   foreach my $state ( @states ) {
1702     foreach my $popac ( keys %{ $pop{$state} } ) {
1703       $text .= "\nif ( ac == \"$popac\" ) {\n";
1704
1705       foreach my $pop ( @{$pop{$state}->{$popac}}) {
1706         my $o_popnum = $pop->{popnum};
1707         my $poptext =  $pop->{city}. ', '. $pop->{state}.
1708                        ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1709
1710         $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n";
1711         if ($popnum == $o_popnum) {
1712           $text .= "what.form.popnum.options[what.form.popnum.length-1].selected = true;\n";
1713         }
1714       }
1715       $text .= "}\n";
1716     }
1717   }
1718
1719
1720   $text .= "}\n</SCRIPT>\n";
1721
1722   $param->{'acstate'} = '' unless defined($param->{'acstate'});
1723
1724   $text .=
1725     qq!<TABLE CELLPADDING="0"><TR><TD><SELECT NAME="acstate"! .
1726     qq!SIZE=1 onChange="acstate_changed(this)"><OPTION VALUE=-1>State!;
1727   $text .= "<OPTION" . ($_ eq $param->{'acstate'} ? " SELECTED" : "") .
1728            ">$_" foreach sort { $a cmp $b } @states;
1729   $text .= '</SELECT>'; #callback? return 3 html pieces?  #'</TD>';
1730
1731   $text .=
1732     qq!<SELECT NAME="popac" SIZE=1 onChange="popac_changed(this)">!.
1733     qq!<OPTION>Area code</SELECT></TR><TR VALIGN="top">!;
1734
1735   $text .= qq!<TR><TD><SELECT NAME="popnum" SIZE=1 STYLE="width: 20em"><OPTION>City!;
1736
1737
1738   #comment this block to disable initial list polulation
1739   my @initial_select = ();
1740   if ( scalar( @$pops ) > 100 ) {
1741     push @initial_select, $popnum2pop{$popnum} if $popnum2pop{$popnum};
1742   } else {
1743     @initial_select = @$pops;
1744   }
1745   foreach my $pop ( sort { $a->{state} cmp $b->{state} } @initial_select ) {
1746     $text .= qq!<OPTION VALUE="!. $pop->{popnum}. '"'.
1747              ( ( $popnum && $pop->{popnum} == $popnum ) ? ' SELECTED' : '' ). ">".
1748              $pop->{city}. ', '. $pop->{state}.
1749                ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
1750   }
1751
1752   $text .= qq!</SELECT></TD></TR></TABLE>!;
1753
1754   $text;
1755
1756 }
1757
1758 =item domainselector HASHREF | LIST
1759
1760 Takes as input a hashref or list of key/value pairs with the following keys:
1761
1762 =over 4
1763
1764 =item pkgnum
1765
1766 Package number
1767
1768 =item domsvc
1769
1770 Service number of the selected item.
1771
1772 =back
1773
1774 Returns an HTML fragment for domain selection.
1775
1776 =cut
1777
1778 sub domainselector {
1779   my $param;
1780   if ( ref($_[0]) ) {
1781     $param = shift;
1782   } else {
1783     $param = { @_ };
1784   }
1785   my $domsvc= $param->{'domsvc'};
1786   my $rv = 
1787       domain_select_hash(map {$_ => $param->{$_}} qw(pkgnum svcpart pkgpart) );
1788   my $domains = $rv->{'domains'};
1789   $domsvc = $rv->{'domsvc'} unless $domsvc;
1790
1791   return '<INPUT TYPE="hidden" NAME="domsvc" VALUE="">'
1792     unless scalar(keys %$domains);
1793
1794   if (scalar(keys %$domains) == 1) {
1795     my $key;
1796     foreach(keys %$domains) {
1797       $key = $_;
1798     }
1799     return '<TR><TD ALIGN="right">Domain</TD><TD>'. $domains->{$key}.
1800            '<INPUT TYPE="hidden" NAME="domsvc" VALUE="'. $key. '"></TD></TR>'
1801   }
1802
1803   my $text .= qq!<TR><TD ALIGN="right">Domain</TD><TD><SELECT NAME="domsvc" SIZE=1 STYLE="width: 20em">!;
1804
1805   $text .= '<OPTION>(Choose Domain)' unless $domsvc;
1806
1807   foreach my $domain ( sort { $domains->{$a} cmp $domains->{$b} } keys %$domains ) {
1808     $text .= qq!<OPTION VALUE="!. $domain. '"'.
1809              ( ( $domsvc && $domain == $domsvc ) ? ' SELECTED' : '' ). ">".
1810              $domains->{$domain};
1811   }
1812
1813   $text .= qq!</SELECT></TD></TR>!;
1814
1815   $text;
1816
1817 }
1818
1819 =item didselector HASHREF | LIST
1820
1821 Takes as input a hashref or list of key/value pairs with the following keys:
1822
1823 =over 4
1824
1825 =item field
1826
1827 Field name for the returned HTML fragment.
1828
1829 =item svcpart
1830
1831 Service definition (see L<FS::part_svc>)
1832
1833 =back
1834
1835 Returns an HTML fragment for DID selection.
1836
1837 =cut
1838
1839 sub didselector {
1840   my $param;
1841   if ( ref($_[0]) ) {
1842     $param = shift;
1843   } else {
1844     $param = { @_ };
1845   }
1846
1847   my $rv = mason_comp( 'comp'=>'/elements/select-did.html',
1848                        'args'=>[ %$param ],
1849                      );
1850
1851   #hmm.
1852   $rv->{'error'} || $rv->{'output'};
1853
1854 }
1855
1856 =back
1857
1858 =head1 RESELLER FUNCTIONS
1859
1860 Note: Resellers can also use the B<signup_info> and B<new_customer> functions
1861 with their active session, and the B<customer_info> and B<order_pkg> functions
1862 with their active session and an additional I<custnum> parameter.
1863
1864 For the most part, development of the reseller web interface has been
1865 superceded by agent-virtualized access to the backend.
1866
1867 =over 4
1868
1869 =item agent_login
1870
1871 Agent login
1872
1873 =item agent_info
1874
1875 Agent info
1876
1877 =item agent_list_customers
1878
1879 List agent's customers.
1880
1881 =back
1882
1883 =head1 BUGS
1884
1885 =head1 SEE ALSO
1886
1887 L<freeside-selfservice-clientd>, L<freeside-selfservice-server>
1888
1889 =cut
1890
1891 1;
1892