RT# 79902 - fixed so change payment information link will still work when upgraded...
[freeside.git] / fs_selfservice / FS-SelfService / cgi / selfservice.cgi
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw($DEBUG $cgi $session_id $pw_session_id $form_max $template_dir);
5 use subs qw(do_template);
6 use CGI;
7 use CGI::Carp qw(fatalsToBrowser);
8 use CGI::Cookie;
9 use Text::Template;
10 use HTML::Entities;
11 use Date::Format;
12 use Date::Parse 'str2time';
13 use Number::Format 1.50;
14 use FS::SelfService qw(
15   access_info login_info login customer_info edit_info invoice
16   payment_info process_payment realtime_collect process_prepay
17   list_pkgs order_pkg signup_info order_recharge
18   part_svc_info provision_acct provision_external provision_phone provision_forward
19   unprovision_svc change_pkg suspend_pkg domainselector
20   list_svcs list_svc_usage list_cdr_usage list_support_usage
21   myaccount_passwd list_invoices create_ticket get_ticket did_report
22   adjust_ticket_priority
23   mason_comp port_graph
24   start_thirdparty finish_thirdparty
25   reset_passwd check_reset_passwd process_reset_passwd
26   validate_passwd
27   billing_history
28 );
29
30 $template_dir = '.';
31
32 $DEBUG = 0;
33
34 $form_max = 255;
35
36 $cgi = new CGI;
37
38 #order|pw_list XXX ???
39 my @actions = ( qw(
40   myaccount
41   tktcreate
42   tktview
43   ticket_priority
44   didreport
45   invoices
46   view_invoice
47   make_payment
48   make_ach_payment
49   make_term_payment
50   make_thirdparty_payment
51   post_thirdparty_payment
52   finish_thirdparty_payment
53   cancel_thirdparty_payment
54   payment_results
55   ach_payment_results
56   recharge_prepay
57   recharge_results
58   logout
59   change_bill
60   change_ship
61   change_pay
62   process_change_bill
63   process_change_ship
64   process_change_pay
65   customer_order_pkg
66   process_order_pkg
67   customer_change_pkg
68   process_change_pkg
69   process_order_recharge
70   provision
71   provision_svc
72   process_svc_acct
73   process_svc_phone
74   process_svc_external
75   process_svc_forward
76   delete_svc
77   view_usage
78   view_usage_details
79   view_cdr_details
80   view_support_details
81   view_port_graph
82   real_port_graph
83   change_password
84   process_change_password
85   customer_suspend_pkg
86   process_suspend_pkg
87   history
88   validate_password
89 ));
90
91 my @nologin_actions = (qw(
92   forgot_password
93   do_forgot_password
94   process_forgot_password
95   do_process_forgot_password
96   process_forgot_password_session
97   validate_password_nologin
98 ));
99 push @actions, @nologin_actions;
100 my %nologin_actions = map { $_=>1 } @nologin_actions;
101
102 my $action = 'myaccount'; # sensible default
103
104 if ( $cgi->param('action') =~ /^process_forgot_password_session_(\w+)$/ ) {
105   $action = 'process_forgot_password_session';
106   $pw_session_id = $1;
107 } elsif ( $cgi->param('action') =~ /^(\w+)$/ ) {
108   if (grep {$_ eq $1} @actions) {
109     $action = $1;
110   } else {
111     warn "WARNING: unrecognized action '$1'\n";
112   }
113 }
114 unless ( $nologin_actions{$action} ) {
115
116   my %cookies = CGI::Cookie->fetch;
117
118   my $login_rv = {};
119
120   if ( exists($cookies{'session'}) ) {
121
122     $session_id = $cookies{'session'}->value;
123
124     if ( $session_id eq 'login' ) {
125       # then we've just come back from the login page
126
127       $cgi->param('password') =~ /^(.{0,$form_max})$/;
128       my $password = $1;
129
130       if ( $cgi->param('email') =~ /^\s*([a-z0-9_\-\.\+\@]{1,$form_max})\s*$/i ) {
131
132         my $email = $1;
133         $login_rv = login(
134           'email'    => $email,
135           'password' => $password
136         );
137
138         if ( $login_rv->{'error'} ) {
139           my $ip = $cgi->remote_addr();
140           warn("login failure [email $email] [ip $ip] [error $login_rv->{error}]");
141         } else {
142           #successful login
143         }
144
145         $session_id = $login_rv->{'session_id'};
146
147       } else {
148
149         $cgi->param('username') =~ /^\s*([a-z0-9_\-\.\&]{0,$form_max})\s*$/i;
150         my $username = $1;
151
152         $cgi->param('domain') =~ /^\s*([\w\-\.]{0,$form_max})\s*$/;
153         my $domain = $1;
154
155         if ( $username and $domain and $password ) {
156
157           # authenticate
158           $login_rv = login(
159             'username' => $username,
160             'domain'   => $domain,
161             'password' => $password,
162           );
163           $session_id = $login_rv->{'session_id'};
164
165         } elsif ( $username or $domain or $password ) {
166         
167           my $error = 'Illegal '; #XXX localization...
168           my $count = 0;
169           if ( !$username ) {
170             $error .= 'username';
171             $count++;
172           }
173           if ( !$domain )  {
174             $error .= ', ' if $count;
175             $error .= 'domain';
176             $count++;
177           }
178           if ( !$password ) {
179             $error .= ', ' if $count;
180             $error .= 'and ' if $count > 1;
181             $error .= 'password';
182             $count++;
183           }
184           $error .= '.';
185           $login_rv = {
186             'username'  => $username,
187             'domain'    => $domain,
188             'password'  => $password,
189             'error'     => $error,
190           };
191           $session_id = undef; # attempt login again
192
193         }
194
195       } # else there was no input, so show no error message
196
197     } # else session_id ne 'login'
198
199   } # else there is no session cookie
200
201   if ( !$session_id ) {
202     # show the login page
203     $session_id = 'login'; # set state
204     my $login_info = login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
205
206     do_template('login', { %$login_rv, %$login_info });
207     exit;
208   }
209
210   # at this point $session_id is a real session
211
212 }
213
214 warn "calling $action sub\n"
215   if $DEBUG;
216 $FS::SelfService::DEBUG = $DEBUG;
217 my $result = eval "&$action();";
218 die $@ if $@;
219
220 warn Dumper($result) if $DEBUG;
221
222 if ( $result->{error} && ( $result->{error} eq "Can't resume session"
223   || $result->{error} eq "Expired session") ) { #ick
224
225   $session_id = 'login';
226   my $login_info = login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
227   do_template('login', $login_info);
228   exit;
229 }
230
231 #warn $result->{'open_invoices'};
232 #warn scalar(@{$result->{'open_invoices'}});
233
234 warn "processing template $action\n"
235   if $DEBUG;
236 do_template($action, {
237   'session_id' => $session_id,
238   'action'     => $action, #so the menu knows what tab we're on...
239   #%{ payment_info( 'session_id' => $session_id ) },  # cust_paybys for the menu
240   %{$result}
241 });
242
243 #--
244
245 use Data::Dumper;
246 sub myaccount { 
247   customer_info( 'session_id' => $session_id ); 
248 }
249
250 sub change_bill { my $payment_info =
251                     payment_info( 'session_id' => $session_id );
252                   return $payment_info if ( $payment_info->{'error'} );
253                   my $customer_info =
254                     customer_info( 'session_id' => $session_id );
255                   return { 
256                     %$payment_info,
257                     %$customer_info,
258                   };
259                 }
260 sub change_ship { change_bill(@_); }
261 sub change_pay { change_bill(@_); }
262
263 sub _process_change_info { 
264   my ($erroraction, @fields) = @_;
265
266   my $results = '';
267
268   $results ||= edit_info (
269     'session_id' => $session_id,
270     map { ($_ => $cgi->param($_)) } grep { defined($cgi->param($_)) } @fields,
271   );
272
273
274   if ( $results->{'error'} ) {
275     no strict 'refs';
276     $action = $erroraction;
277     return {
278       $cgi->Vars,
279       %{&$action()},
280       'error' => '<FONT COLOR="#FF0000">'. $results->{'error'}. '</FONT>',
281     };
282   } else {
283     return $results;
284   }
285 }
286
287 sub process_change_bill {
288         _process_change_info( 'change_bill', 
289           qw( first last company address1 address2 city state
290               county zip country daytime night fax )
291         );
292 }
293
294 sub process_change_ship {
295         my @list = map { "ship_$_" }
296                      qw( first last company address1 address2 city state
297                          county zip country daytime night fax 
298                        );
299         if ($cgi->param('same') eq 'Y') {
300           foreach (@list) { $cgi->param($_, '') }
301         }
302
303         _process_change_info( 'change_ship', @list );
304 }
305
306 sub process_change_pay {
307         my $postal = $cgi->param( 'postal_invoicing' );
308         my $payby  = $cgi->param( 'payby' );
309         $cgi->param('paydate', $cgi->param('year') . '-' . $cgi->param('month') . '-01');
310         my @list =
311           qw( payby payinfo payinfo1 payinfo2 month year paydate payname custpaybynum
312               address1 address2 city county state zip country auto paytype
313               paystate ss stateid stateid_state invoicing_list
314             );
315         push @list, 'postal_invoicing' if $postal;
316         unless (    $payby ne 'BILL'
317                  || $postal
318                  || $cgi->param( 'invoicing_list' )
319                )
320         {
321           $action = 'change_pay';
322           return {
323             %{&change_pay()},
324             $cgi->Vars,
325             'error' => '<FONT COLOR="#FF0000">Postal or email required.</FONT>',
326           };
327         }
328
329         if (FS::SelfService->can('update_payby')) {
330           if ($cgi->param( 'custpaybynum' )) { _process_change_payby( 'change_pay', @list ); }
331           else { _process_insert_payby( 'change_pay', @list ); }
332         }
333         else { _process_change_info( 'change_pay', @list ); }
334 }
335
336 sub view_invoice {
337
338   $cgi->param('invnum') =~ /^(\d+)$/ or die "illegal invnum";
339   my $invnum = $1;
340
341   invoice( 'session_id' => $session_id,
342            'invnum'     => $invnum,
343          );
344
345 }
346
347 sub invoices {
348   list_invoices( 'session_id' => $session_id, );
349 }
350
351 sub history {
352   billing_history( 'session_id' => $session_id, );
353 }
354
355 sub tktcreate {
356   my $customer_info = customer_info( 'session_id' => $session_id );
357   return $customer_info if ( $customer_info->{'error'} );
358
359   my $requestor = "";
360   if ( $customer_info->{'invoicing_list'} ) {
361     my @requestor = split( /\s*\,\s*/, $customer_info->{'invoicing_list'} );
362     $requestor = $requestor[0] if scalar(@requestor);
363   }
364
365   return { 'requestor' => $requestor }
366     unless ($cgi->param('subject') && $cgi->param('message') &&
367         length($cgi->param('subject')) && length($cgi->param('message')));
368     
369  create_ticket( 'session_id' => $session_id,
370                         'subject' => $cgi->param('subject'),
371                         'message' => $cgi->param('message'), 
372                         'requestor' => $requestor,
373             );
374 }
375
376 sub tktview {
377  get_ticket(    'session_id' => $session_id,
378                 'ticket_id' => ($cgi->param('ticket_id') || ''),
379                 'subject'   => ($cgi->param('subject') || ''),
380                 'reply'     => ($cgi->param('reply') || ''),
381             );
382 }
383
384 sub ticket_priority {
385   my %values;
386   foreach ( $cgi->param ) {
387     if ( /^ticket(\d+)$/ ) {
388       # a 'ticket1001' param implies the existence of a 'priority1001' param
389       # but if that's empty, we need to send it as empty rather than forget
390       # it.
391       $values{$1} = $cgi->param("priority$1") || '';
392     }
393   }
394   $action = 'myaccount';
395   # this returns an updated customer_info for myaccount
396   adjust_ticket_priority( 'session_id' => $session_id,
397                           'values'     => \%values );
398 }
399
400 sub customer_order_pkg {
401   my $init_data = signup_info( 'customer_session_id' => $session_id );
402   return $init_data if ( $init_data->{'error'} );
403
404   my $customer_info = customer_info( 'session_id' => $session_id );
405   return $customer_info if ( $customer_info->{'error'} );
406
407   my $pkgselect = mason_comp(
408     'session_id' => $session_id,
409     'comp'       => '/edit/cust_main/first_pkg/select-part_pkg.html',
410     'args'       => [ 'password_verify' => 1,
411                       'onchange'        => 'enable_order_pkg()',
412                       'relurls'         => 1,
413                       'empty_label'     => 'Select package',
414                     ],
415   );
416
417   $pkgselect = $pkgselect->{'error'} || $pkgselect->{'output'};
418
419   return {
420     ( map { $_ => $init_data->{$_} }
421           qw( part_pkg security_phrase svc_acct_pop ),
422     ),
423     %$customer_info,
424     'pkg_selector' => $pkgselect,
425   };
426 }
427
428 sub customer_change_pkg {
429   my $init_data = signup_info( 'customer_session_id' => $session_id );
430   return $init_data if ( $init_data->{'error'} );
431
432   my $customer_info = customer_info( 'session_id' => $session_id );
433   return $customer_info if ( $customer_info->{'error'} );
434
435   return {
436     ( map { $_ => $init_data->{$_} }
437           qw( part_pkg security_phrase svc_acct_pop ),
438     ),
439     ( map { $_ => $cgi->param($_) }
440         qw( pkgnum pkg )
441     ),
442     %$customer_info,
443   };
444 }
445
446 sub process_order_pkg {
447
448   my $results = '';
449
450   my @params = (qw( custnum pkgpart ));
451   my $svcdb = '';
452   if ( $cgi->param('pkgpart_svcpart') =~ /^(\d+)_(\d+)$/ ) {
453     $cgi->param('pkgpart', $1);
454     $cgi->param('svcpart', $2);
455     push @params, 'svcpart';
456     $svcdb = $cgi->param('svcdb');
457     push @params, 'domsvc' if $svcdb eq 'svc_acct';
458   } else {
459     $svcdb = 'svc_acct';
460   }
461
462   if ( $svcdb eq 'svc_acct' ) {
463
464     push @params, qw( username _password _password2 sec_phrase popnum );
465
466     unless ( length($cgi->param('_password')) ) {
467       my $init_data = signup_info( 'customer_session_id' => $session_id );
468       $results = { 'error' => $init_data->{msgcat}{empty_password} };
469       $results = { 'error' => $init_data->{error} } if($init_data->{error});
470     }
471     if ( $cgi->param('_password') ne $cgi->param('_password2') ) {
472       my $init_data = signup_info( 'customer_session_id' => $session_id );
473       $results = { 'error' => $init_data->{msgcat}{passwords_dont_match} };
474       $results = { 'error' => $init_data->{error} } if($init_data->{error});
475       $cgi->param('_password', '');
476       $cgi->param('_password2', '');
477     }
478
479   } elsif ( $svcdb eq 'svc_phone' ) {
480
481     push @params, qw( phonenum sip_password pin phone_name );
482
483   } else {
484     die "$svcdb not handled on process_order_pkg yet";
485   }
486
487   $results ||= order_pkg (
488     'session_id' => $session_id,
489     map { $_ => $cgi->param($_) } @params
490   );
491
492
493   if ( $results->{'error'} ) {
494     $action = 'customer_order_pkg';
495     return {
496       $cgi->Vars,
497       %{customer_order_pkg()},
498       'error' => '<FONT COLOR="#FF0000">'. $results->{'error'}. '</FONT>',
499     };
500   } else {
501     return $results;
502   }
503
504 }
505
506 sub process_change_pkg {
507
508   my $results = '';
509
510   $results ||= change_pkg (
511     'session_id' => $session_id,
512     map { $_ => $cgi->param($_) }
513         qw( pkgpart pkgnum )
514   );
515
516
517   if ( $results->{'error'} ) {
518     $action = 'customer_change_pkg';
519     return {
520       $cgi->Vars,
521       %{customer_change_pkg()},
522       'error' => '<FONT COLOR="#FF0000">'. $results->{'error'}. '</FONT>',
523     };
524   } else {
525     return $results;
526   }
527
528 }
529
530 sub process_suspend_pkg {
531   my $results = '';
532   $results = suspend_pkg (
533     'session_id' => $session_id,
534     map { $_ => $cgi->param($_) } 
535       qw( pkgnum )
536     );
537   if ( $results->{'error'} ) {
538     $action = 'provision';
539     return {
540       'error' => '<FONT COLOR="#FF0000">'. $results->{'error'}. '</FONT>',
541     }
542   }
543   else {
544     return $results;
545   }
546 }
547
548 sub process_order_recharge {
549
550   my $results = '';
551
552   $results ||= order_recharge (
553     'session_id' => $session_id,
554     map { $_ => $cgi->param($_) }
555         qw( svcnum )
556   );
557
558
559   if ( $results->{'error'} ) {
560     $action = 'view_usage';
561     if ($results->{'error'} eq '_decline') {
562       $results->{'error'} = "There has been an error processing your account.  Please contact customer support."
563     }
564     return {
565       $cgi->Vars,
566       %{view_usage()},
567       'error' => '<FONT COLOR="#FF0000">'. $results->{'error'}. '</FONT>',
568     };
569   } else {
570     return $results;
571   }
572
573 }
574
575 sub make_payment {
576
577   my $payment_info = payment_info( 'session_id' => $session_id );
578
579   my $amount = 
580     ($payment_info->{'balance'} && ($payment_info->{'balance'} > 0))
581     ? $payment_info->{'balance'}
582     : '';
583
584   my $tr_amount_fee = mason_comp(
585     'session_id' => $session_id,
586     'comp'       => '/elements/tr-amount_fee.html',
587     'args'       => [ 'amount' => $amount,
588                     ],
589   );
590
591   $tr_amount_fee = $tr_amount_fee->{'error'} || $tr_amount_fee->{'output'};
592
593   $payment_info->{'tr_amount_fee'} = $tr_amount_fee;
594
595   $payment_info;
596 }
597
598 sub payment_results {
599
600   use Business::CreditCard 0.35;
601
602   #we should only do basic checking here for DoS attacks and things
603   #that couldn't be constructed by the web form...  let process_payment() do
604   #the rest, it gives better error messages
605
606   $cgi->param('amount') =~ /^\s*(\d+(\.\d{2})?)\s*$/
607     or return { 'error' => "Illegal amount: ". $cgi->param('amount') }; #!!!
608   my $amount = $1;
609
610   my $payinfo = $cgi->param('payinfo');
611   $payinfo =~ s/[^\dx]//g;
612   $payinfo =~ /^([\dx]{13,19}|[\dx]{8,9})$/
613     #or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
614     or return { 'error' => "illegal card" }; #!!!
615   $payinfo = $1;
616   unless ( $payinfo =~ /x/ ) {
617     validate($payinfo)
618       #or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
619       or return { 'error' => "invalid card" }; #!!!
620   }
621
622   if ( $cgi->param('card_type') ) {
623     cardtype($payinfo) eq $cgi->param('card_type')
624       #or $error ||= $init_data->{msgcat}{not_a}. $cgi->param('CARD_type');
625       or return { 'error' => "not a ". $cgi->param('card_type') };
626   }
627
628   $cgi->param('paycvv') =~ /^\s*(.{0,4})\s*$/ or die "illegal CVV2";
629   my $paycvv = $1;
630
631   $cgi->param('month') =~ /^(\d{2})/ or die "illegal month";
632   my $month = $1;
633   $cgi->param('year') =~ /^(\d{4})/ or die "illegal year";
634   my $year = $1;
635
636   $cgi->param('payname') =~ /^(.{0,80})$/ or die "illegal payname";
637   my $payname = $1;
638
639   $cgi->param('address1') =~ /^(.{0,80})$/ or die "illegal address1";
640   my $address1 = $1;
641
642   $cgi->param('address2') =~ /^(.{0,80})$/ or die "illegal address2";
643   my $address2 = $1;
644
645   $cgi->param('city') =~ /^(.{0,80})$/ or die "illegal city";
646   my $city = $1;
647
648   $cgi->param('state') =~ /^(.{0,80})$/ or die "illegal state";
649   my $state = $1;
650
651   $cgi->param('zip') =~ /^(.{0,10})$/ or die "illegal zip";
652   my $zip = $1;
653
654   $cgi->param('country') =~ /^(.{0,2})$/ or die "illegal country";
655   my $country = $1;
656
657   my $save = 0;
658   $save = 1 if $cgi->param('save');
659
660   my $auto = 0;
661   $auto = 1 if $cgi->param('auto');
662
663   $cgi->param('payunique') =~ /^([\w\-\.]*)$/ or die "illegal payunique";
664   my $payunique = $1;
665
666   $cgi->param('paybatch') =~ /^([\w\-\.]*)$/ or die "illegal paybatch";
667   my $paybatch = $1;
668
669   $cgi->param('discount_term') =~ /^(\d*)$/ or die "illegal discount_term";
670   my $discount_term = $1;
671
672
673   process_payment(
674     'session_id' => $session_id,
675     'payby'      => 'CARD',
676     'amount'     => $amount,
677     'payinfo'    => $payinfo,
678     'paycvv'     => $paycvv,
679     'month'      => $month,
680     'year'       => $year,
681     'payname'    => $payname,
682     'address1'   => $address1,
683     'address2'   => $address2,
684     'city'       => $city,
685     'state'      => $state,
686     'zip'        => $zip,
687     'country'    => $country,
688     'save'       => $save,
689     'auto'       => $auto,
690     'payunique'  => $payunique,
691     'paybatch'   => $paybatch,
692     'discount_term' => $discount_term,
693   );
694
695 }
696
697 sub make_ach_payment {
698   payment_info( 'session_id' => $session_id );
699 }
700
701 sub ach_payment_results {
702
703   #we should only do basic checking here for DoS attacks and things
704   #that couldn't be constructed by the web form...  let process_payment() do
705   #the rest, it gives better error messages
706
707   $cgi->param('amount') =~ /^\s*(\d+(\.\d{2})?)\s*$/
708     or die "illegal amount"; #!!!
709   my $amount = $1;
710
711   my $payinfo1 = $cgi->param('payinfo1');
712   $payinfo1 =~ s/[^\dx]//g;
713   $payinfo1 =~ /^([\dx]+)$/
714     or die "illegal account"; #!!!
715   $payinfo1 = $1;
716
717   my $payinfo2 = $cgi->param('payinfo2');
718   $payinfo2 =~ s/[^\dx]//g;
719   $payinfo2 =~ /^([\dx]+)$/
720     or die "illegal ABA/routing code"; #!!!
721   $payinfo2 = $1;
722
723   $cgi->param('payname') =~ /^(.{0,80})$/ or die "illegal payname";
724   my $payname = $1;
725
726   $cgi->param('paystate') =~ /^(.{0,2})$/ or die "illegal paystate";
727   my $paystate = $1;
728
729   $cgi->param('paytype') =~ /^(.{0,80})$/ or die "illegal paytype";
730   my $paytype = $1;
731
732   $cgi->param('ss') =~ /^(.{0,80})$/ or die "illegal ss";
733   my $ss = $1;
734
735   $cgi->param('stateid') =~ /^(.{0,80})$/ or die "illegal stateid";
736   my $stateid = $1;
737
738   $cgi->param('stateid_state') =~ /^(.{0,2})$/ or die "illegal stateid_state";
739   my $stateid_state = $1;
740
741   my $save = 0;
742   $save = 1 if $cgi->param('save');
743
744   my $auto = 0;
745   $auto = 1 if $cgi->param('auto');
746
747   $cgi->param('paybatch') =~ /^([\w\-\.]+)$/ or die "illegal paybatch";
748   my $paybatch = $1;
749
750   process_payment(
751     'session_id' => $session_id,
752     'payby'      => 'CHEK',
753     'amount'     => $amount,
754     'payinfo1'   => $payinfo1,
755     'payinfo2'   => $payinfo2,
756     'month'      => '12',
757     'year'       => '2037',
758     'payname'    => $payname,
759     'paytype'    => $paytype,
760     'paystate'   => $paystate,
761     'ss'         => $ss,
762     'stateid'    => $stateid,
763     'stateid_state' => $stateid_state,
764     'save'       => $save,
765     'auto'       => $auto,
766     'paybatch'   => $paybatch,
767   );
768
769 }
770
771 sub make_thirdparty_payment {
772   my $payment_info = payment_info('session_id' => $session_id);
773   $cgi->param('payby_method') =~ /^(CC|ECHECK|PAYPAL)$/
774     or die "illegal payby method";
775   $payment_info->{'payby_method'} = $1;
776   $payment_info->{'error'} = $cgi->param('error');
777
778   $payment_info;
779 }
780
781 sub post_thirdparty_payment {
782   $cgi->param('payby_method') =~ /^(CC|ECHECK|PAYPAL)$/
783     or die "illegal payby method";
784   my $method = $1;
785   $cgi->param('amount') =~ /^(\d+(\.\d*)?)$/
786     or die "illegal amount";
787   my $amount = $1;
788   my $result = start_thirdparty(
789     'session_id' => $session_id,
790     'method' => $method, 
791     'amount' => $amount,
792   );
793   if ( $result->{error} ) {
794     $cgi->param('action', 'make_thirdparty_payment');
795     $cgi->param('error', $result->{error});
796     print $cgi->redirect( $cgi->self_url );
797     exit;
798   }
799
800   $result;
801 }
802
803 sub finish_thirdparty_payment {
804   my %param = $cgi->Vars;
805   finish_thirdparty( 'session_id' => $session_id, %param );
806   # result contains either 'error' => error message, or the payment details
807 }
808
809 sub cancel_thirdparty_payment {
810   $action = 'make_thirdparty_payment';
811   finish_thirdparty( 'session_id' => $session_id, '_cancel' => 1 );
812 }
813
814 sub make_term_payment {
815   $cgi->param('amount') =~ /^(\d+\.\d{2})$/
816     or die "illegal payment amount";
817   my $balance = $1;
818   $cgi->param('discount_term') =~ /^(\d+)$/
819     or die "illegal discount term";
820   my $discount_term = $1;
821   $action = 'make_payment';
822   ({ %{payment_info( 'session_id' => $session_id )},
823     'balance' => $balance,
824     'discount_term' => $discount_term,
825   })
826 }
827
828 sub recharge_prepay {
829   customer_info( 'session_id' => $session_id );
830 }
831
832 sub recharge_results {
833
834   my $prepaid_cardnum = $cgi->param('prepaid_cardnum');
835   $prepaid_cardnum =~ s/\W//g;
836   $prepaid_cardnum =~ /^(\w*)$/ or die "illegal prepaid card number";
837   $prepaid_cardnum = $1;
838
839   process_prepay ( 'session_id'     => $session_id,
840                    'prepaid_cardnum' => $prepaid_cardnum,
841                  );
842 }
843
844 sub logout {
845   FS::SelfService::logout( 'session_id' => $session_id );
846 }
847
848 sub didreport {
849   my $result = did_report( 'session_id' => $session_id, 
850             'format' => $cgi->param('type'),
851             'recentonly' => $cgi->param('recentonly'),
852         );
853   die $result->{'error'} if exists $result->{'error'} && $result->{'error'};
854   $result;
855 }
856
857 sub provision {
858   my $result = list_pkgs( 'session_id' => $session_id );
859   die $result->{'error'} if exists $result->{'error'} && $result->{'error'};
860   $result->{'pkgpart'} = $cgi->param('pkgpart') if $cgi->param('pkgpart');
861   $result->{'filter'} = $cgi->param('filter') if $cgi->param('filter');
862   $result;
863 }
864
865 sub provision_svc {
866
867   my $result = part_svc_info(
868     'session_id' => $session_id,
869     map { $_ => ($cgi->param($_) || '') } qw( pkgnum svcpart svcnum ),
870   );
871   die $result->{'error'} if exists $result->{'error'} && $result->{'error'};
872
873   $result->{'svcdb'} =~ /^svc_(.*)$/
874     #or return { 'error' => 'Unknown svcdb '. $result->{'svcdb'} };
875     or die 'Unknown svcdb '. $result->{'svcdb'};
876   $action .= "_$1";
877
878   $result->{'numavail'} = $cgi->param('numavail');
879   $result->{'lnp'} = $cgi->param('lnp');
880
881   $result;
882 }
883
884 sub process_svc_phone {
885     my @bulkdid = $cgi->param('bulkdid');
886     my $phonenum = $cgi->param('phonenum');
887     my $lnp = $cgi->param('lnp');
888
889     my $result;
890     if($lnp) {
891         $result = provision_phone (
892             'session_id' => $session_id,
893             'countrycode' => '1',
894              map { $_ => $cgi->param($_) } qw( pkgnum svcpart phonenum 
895                 lnp_desired_due_date lnp_other_provider 
896                 lnp_other_provider_account )
897         );
898     } else {
899         $result = provision_phone (
900             'session_id' => $session_id,
901             'bulkdid' => [ @bulkdid ],
902             'countrycode' => '1',
903              map { $_ => $cgi->param($_) } qw( pkgnum svcpart phonenum svcnum email forwarddst )
904         );
905     }
906
907     if ( exists $result->{'error'} && $result->{'error'} ) { 
908         $action = 'provision_svc_phone';
909         return {
910           $cgi->Vars,
911           %{ part_svc_info( 'session_id' => $session_id,
912                         map { $_ => $cgi->param($_) } qw( pkgnum svcpart svcnum )
913               )
914           },
915           'error' => $result->{'error'},
916         };
917   }
918
919   $result;
920 }
921
922 sub process_svc_acct {
923
924   my $result = provision_acct (
925     'session_id' => $session_id,
926     map { $_ => $cgi->param($_) } qw(
927       pkgnum svcpart username domsvc _password _password2 sec_phrase popnum )
928   );
929
930   if ( exists $result->{'error'} && $result->{'error'} ) { 
931     #warn "$result $result->{'error'}"; 
932     $action = 'provision_svc_acct';
933     return {
934       $cgi->Vars,
935       %{ part_svc_info( 'session_id' => $session_id,
936                         map { $_ => $cgi->param($_) } qw( pkgnum svcpart )
937                       )
938       },
939       'error' => $result->{'error'},
940     };
941   } else {
942     #warn "$result $result->{'error'}"; 
943     return $result;
944   }
945
946 }
947
948 sub process_svc_external {
949   provision_external (
950     'session_id' => $session_id,
951     map { $_ => $cgi->param($_) } qw( pkgnum svcpart )
952   );
953 }
954
955 sub process_svc_forward {
956
957   my $result = provision_forward (
958     'session_id' => $session_id,
959     map { $_ => $cgi->param($_) || '' } qw(
960       pkgnum svcpart srcsvc src dstsvc dst )
961   );
962
963   if ( exists $result->{'error'} && $result->{'error'} ) { 
964     #warn "$result $result->{'error'}"; 
965     $action = 'provision_svc_forward';
966     return {
967       $cgi->Vars,
968       %{ part_svc_info( 'session_id' => $session_id,
969                         map { $_ => $cgi->param($_) } qw( svcnum pkgnum svcpart )
970                       )
971       },
972       'error' => $result->{'error'},
973     };
974   } else {
975     #just go to setup services page, results will be visible there
976     $action = 'provision';
977     return provision();
978   }
979
980 }
981
982 sub delete_svc {
983   unprovision_svc(
984     'session_id' => $session_id,
985     'svcnum'     => $cgi->param('svcnum'),
986   );
987 }
988
989 sub view_usage {
990   my $res = list_svcs(
991     'session_id'  => $session_id,
992     'svcdb'       => [ 'svc_acct', 'svc_broadband', 'svc_phone', 'svc_port', 'svc_pbx' ],
993     'ncancelled'  => 1,
994   );
995   if ($res->{hide_usage}) {
996     $action = 'myaccount';
997     return myaccount();
998   } else {
999     return $res;
1000   }
1001 }
1002
1003 sub real_port_graph {
1004     my $svcnum = $cgi->param('svcnum');
1005     my $res = port_graph(
1006             'session_id'  => $session_id,
1007             'svcnum'      => $svcnum,
1008             'beginning'   => str2time($cgi->param('start')." 00:00:00"),
1009             'ending'      => str2time($cgi->param('end')  ." 23:59:59"),
1010             );
1011     my @usage = @{$res->{'usage'}};
1012     my $png = $usage[0]->{'png'};
1013     { 'content' => $png, 'format' => 'png' };
1014 }
1015
1016 sub view_port_graph {
1017     my $svcnum = $cgi->param('svcnum');
1018     { 'svcnum' => $svcnum,
1019       'start' => $cgi->param($svcnum.'_start'),
1020       'end' => $cgi->param($svcnum.'_end'),
1021     }
1022 }
1023
1024 sub view_usage_details {
1025       list_svc_usage(
1026         'session_id'  => $session_id,
1027         'svcnum'      => $cgi->param('svcnum'),
1028         'beginning'   => $cgi->param('beginning') || '',
1029         'ending'      => $cgi->param('ending') || '',
1030       );
1031 }
1032
1033 sub view_cdr_details {
1034   list_cdr_usage(
1035     'session_id'  => $session_id,
1036     'svcnum'      => $cgi->param('svcnum'),
1037     'beginning'   => $cgi->param('beginning') || '',
1038     'ending'      => $cgi->param('ending') || '',
1039     'inbound'     => $cgi->param('inbound') || 0,
1040   );
1041 }
1042
1043 sub view_support_details {
1044   list_support_usage(
1045     'session_id'  => $session_id,
1046     'svcnum'      => $cgi->param('svcnum'),
1047     'beginning'   => $cgi->param('beginning') || '',
1048     'ending'      => $cgi->param('ending') || '',
1049   );
1050 }
1051
1052 sub change_password {
1053   list_svcs(
1054     'session_id' => $session_id,
1055     'svcdb'      => 'svc_acct',
1056   );
1057 };
1058
1059 sub process_change_password {
1060
1061   my $result = myaccount_passwd(
1062     'session_id'    => $session_id,
1063     map { $_ => $cgi->param($_) } qw( svcnum new_password new_password2 )
1064   );
1065
1066   if ( exists $result->{'error'} && $result->{'error'} ) { 
1067
1068     $action = 'change_password';
1069     return {
1070       $cgi->Vars,
1071       %{ list_svcs( 'session_id' => $session_id,
1072                     'svcdb'      => 'svc_acct',
1073                   )
1074        },
1075       #'svcnum' => $cgi->param('svcnum'),
1076       'error'  => $result->{'error'}
1077     };
1078
1079  } else {
1080
1081    return $result;
1082
1083  }
1084
1085 }
1086
1087 sub forgot_password {
1088   login_info( 'agentnum' => scalar($cgi->param('agentnum')) );
1089 }
1090
1091 sub do_forgot_password {
1092   reset_passwd(
1093     map { $_ => scalar($cgi->param($_)) }
1094       qw( agentnum email username domain )
1095   );
1096 }
1097
1098 sub process_forgot_password {
1099   check_reset_passwd(
1100     map { $_ => scalar($cgi->param($_)) }
1101       qw( session_id )
1102   );
1103 }
1104
1105 sub process_forgot_password_session {
1106   $action = 'process_forgot_password';
1107   check_reset_passwd(
1108     'session_id' => $pw_session_id,
1109   );
1110 }
1111
1112 sub do_process_forgot_password {
1113   process_reset_passwd(
1114     map { $_ => scalar($cgi->param($_)) }
1115       qw( session_id new_password new_password2 )
1116   );
1117 }
1118
1119 sub validate_password {
1120   validate_passwd(
1121     'session_id' => $session_id,
1122     map { $_ => scalar($cgi->param($_)) }
1123       qw( fieldid svcnum check_password )
1124   )
1125 }
1126
1127 sub validate_password_nologin {
1128   $action = 'validate_password'; #use same landing page
1129   validate_passwd(
1130     map { $_ => scalar($cgi->param($_)) }
1131       qw( fieldid check_password agentnum )
1132   )
1133 }
1134
1135 #--
1136
1137 sub do_template {
1138   my $name = shift;
1139   my $fill_in = shift;
1140
1141   $cgi->delete_all();
1142   $fill_in->{'selfurl'} = $cgi->self_url;
1143   $fill_in->{'cgi'} = \$cgi;
1144   $fill_in->{'error'} = $cgi->param('error') if $cgi->param('error');
1145
1146   my $access_info = ($session_id and $session_id ne 'login')
1147                       ? access_info( 'session_id' => $session_id )
1148                       : {};
1149   $fill_in->{$_} = $access_info->{$_} foreach keys %$access_info;
1150
1151   # update the user's authentication
1152   my $timeout = $access_info->{'timeout'} || '3600';
1153   my $cookie = CGI::Cookie->new('-name'     => 'session',
1154                                 '-value'    => $session_id,
1155                                 '-expires'  => '+'.$timeout.'s',
1156                                 #'-secure'   => 1, # would be a good idea...
1157                                );
1158   if ( $name eq 'logout' ) {
1159     $cookie->expires(0);
1160   }
1161
1162   if ( $fill_in->{'format'} ) {
1163     # then override content-type, and return $fill_in->{'content'} instead
1164     # of filling in a template
1165     if ( $fill_in->{'format'} eq 'csv') {
1166       print $cgi->header('-expires' => 'now',
1167         '-Content-Type' => 'text/csv',
1168         '-Content-Disposition' => "attachment;filename=output.csv",
1169       );
1170     } elsif ( $fill_in->{'format'} eq 'xls' ) {
1171       print $cgi->header('-expires' => 'now',
1172         '-Content-Type' => 'application/vnd.ms-excel',
1173         '-Content-Disposition' => "attachment;filename=output.xls",
1174         '-Content-Length' => length($fill_in->{'content'}),
1175       );
1176     } elsif ( $fill_in->{'format'} eq 'png' ) {
1177       print $cgi->header('-expires' => 'now',
1178         '-Content-Type' => 'image/png',
1179       );
1180     }
1181     print $fill_in->{'content'};
1182   } else { # the usual case
1183     my $source = "$template_dir/$name.html";
1184     my $template = new Text::Template(
1185       TYPE       => 'FILE',
1186       SOURCE     => $source,
1187       DELIMITERS => [ '<%=', '%>' ],
1188       UNTAINT    => 1,
1189     )
1190       or die $Text::Template::ERROR;
1191
1192     my $data = $template->fill_in( 
1193       PACKAGE => 'FS::SelfService::_selfservicecgi',
1194       HASH    => $fill_in,
1195     ) || "Error processing template $source"; # at least print _something_
1196     print $cgi->header( '-cookie' => $cookie,
1197                         '-expires' => 'now' );
1198     print $data;
1199   }
1200 }
1201
1202 #*FS::SelfService::_selfservicecgi::include = \&Text::Template::fill_in_file;
1203
1204 package FS::SelfService::_selfservicecgi;
1205
1206 use HTML::Entities;
1207 use FS::SelfService qw(
1208     regionselector popselector domainselector location_form didselector mason_comp
1209 );
1210
1211 #false laziness w/agent.cgi
1212 use vars qw(@INCLUDE_ARGS);
1213 sub include {
1214   my $name = shift;
1215
1216   @INCLUDE_ARGS = @_;
1217
1218   my $template = new Text::Template( TYPE   => 'FILE',
1219                                      SOURCE => "$main::template_dir/$name.html",
1220                                      DELIMITERS => [ '<%=', '%>' ],
1221                                      UNTAINT => 1,                   
1222                                    )
1223     or die $Text::Template::ERROR;
1224
1225   $template->fill_in( PACKAGE => 'FS::SelfService::_selfservicecgi',
1226                       #HASH    => $fill_in
1227                     );
1228
1229 }