remove debugging
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
1 package FS::ClientAPI::MyAccount;
2
3 use strict;
4 use vars qw($cache);
5 use subs qw(_cache);
6 use Digest::MD5 qw(md5_hex);
7 use Date::Format;
8 use Business::CreditCard;
9 use Time::Duration;
10 use FS::CGI qw(small_custview); #doh
11 use FS::UI::Web;
12 use FS::UI::bytecount;
13 use FS::Conf;
14 use FS::Record qw(qsearch qsearchs);
15 use FS::Msgcat qw(gettext);
16 use FS::Misc qw(card_types);
17 use FS::ClientAPI_SessionCache;
18 use FS::svc_acct;
19 use FS::svc_domain;
20 use FS::svc_external;
21 use FS::part_svc;
22 use FS::cust_main;
23 use FS::cust_bill;
24 use FS::cust_main_county;
25 use FS::cust_pkg;
26 use FS::payby;
27 use HTML::Entities;
28
29 #false laziness with FS::cust_main
30 BEGIN {
31   eval "use Time::Local;";
32   die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
33     if $] < 5.006 && !defined($Time::Local::VERSION);
34   eval "use Time::Local qw(timelocal_nocheck);";
35 }
36
37 use vars qw( @cust_main_editable_fields );
38 @cust_main_editable_fields = qw(
39   first last company address1 address2 city
40     county state zip country daytime night fax
41   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
42     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
43   payby payinfo payname paystart_month paystart_year payissue payip
44 );
45
46 use subs qw(_provision);
47
48 sub _cache {
49   $cache ||= new FS::ClientAPI_SessionCache( {
50                'namespace' => 'FS::ClientAPI::MyAccount',
51              } );
52 }
53
54 #false laziness w/FS::ClientAPI::passwd::passwd
55 sub login {
56   my $p = shift;
57
58   my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
59     or return { error => 'Domain '. $p->{'domain'}. ' not found' };
60
61   my $svc_acct = qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
62                                          'domsvc'    => $svc_domain->svcnum, }
63                          );
64   return { error => 'User not found.' } unless $svc_acct;
65
66   my $conf = new FS::Conf;
67   my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
68   return { error => 'Only primary user may log in.' } 
69     if $conf->exists('selfservice_server-primary_only')
70        && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
71
72   return { error => 'Incorrect password.' }
73     unless $svc_acct->check_password($p->{'password'});
74
75   my $session = {
76     'svcnum' => $svc_acct->svcnum,
77   };
78
79   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
80   if ( $cust_pkg ) {
81     my $cust_main = $cust_pkg->cust_main;
82     $session->{'custnum'} = $cust_main->custnum;
83   }
84
85   my $session_id;
86   do {
87     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
88   } until ( ! defined _cache->get($session_id) ); #just in case
89
90   _cache->set( $session_id, $session, '1 hour' );
91
92   return { 'error'      => '',
93            'session_id' => $session_id,
94          };
95 }
96
97 sub logout {
98   my $p = shift;
99   if ( $p->{'session_id'} ) {
100     _cache->remove($p->{'session_id'});
101     return { 'error' => '' };
102   } else {
103     return { 'error' => "Can't resume session" }; #better error message
104   }
105 }
106
107 sub customer_info {
108   my $p = shift;
109
110   my($context, $session, $custnum) = _custoragent_session_custnum($p);
111   return { 'error' => $session } if $context eq 'error';
112
113   my %return;
114   if ( $custnum ) { #customer record
115
116     my $search = { 'custnum' => $custnum };
117     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
118     my $cust_main = qsearchs('cust_main', $search )
119       or return { 'error' => "unknown custnum $custnum" };
120
121     $return{balance} = $cust_main->balance;
122
123     $return{tickets} = [ ($cust_main->tickets) ];
124
125     my @open = map {
126                      {
127                        invnum => $_->invnum,
128                        date   => time2str("%b %o, %Y", $_->_date),
129                        owed   => $_->owed,
130                      };
131                    } $cust_main->open_cust_bill;
132     $return{open_invoices} = \@open;
133
134     my $conf = new FS::Conf;
135     $return{small_custview} =
136       small_custview( $cust_main, $conf->config('countrydefault') );
137
138     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
139
140     for (@cust_main_editable_fields) {
141       $return{$_} = $cust_main->get($_);
142     }
143
144     if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
145       $return{payinfo} = $cust_main->paymask;
146       @return{'month', 'year'} = $cust_main->paydate_monthyear;
147     }
148
149     $return{'invoicing_list'} =
150       join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
151     $return{'postal_invoicing'} =
152       0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
153
154     if (scalar($conf->config('support_packages'))) {
155       my $support = 0;
156       foreach ($cust_main->support_services) {
157         $support += $_->svc_x->seconds;
158       }
159       $return{support_time} = (($support < 0) ? '-' : '' ). int(abs($support)/3600)."h".sprintf("%02d",(abs($support)%3600)/60)."m";
160     }
161
162   } elsif ( $session->{'svcnum'} ) { #no customer record
163
164     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
165       or die "unknown svcnum";
166     $return{name} = $svc_acct->email;
167
168   } else {
169
170     return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
171
172   }
173
174   return { 'error'          => '',
175            'custnum'        => $custnum,
176            %return,
177          };
178
179 }
180
181 sub edit_info {
182   my $p = shift;
183   my $session = _cache->get($p->{'session_id'})
184     or return { 'error' => "Can't resume session" }; #better error message
185
186   my $custnum = $session->{'custnum'}
187     or return { 'error' => "no customer record" };
188
189   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
190     or return { 'error' => "unknown custnum $custnum" };
191
192   my $new = new FS::cust_main { $cust_main->hash };
193   $new->set( $_ => $p->{$_} )
194     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
195
196   if ( $p->{'payby'} =~ /^(CARD|DCRD)$/ ) {
197     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
198     if ( $new->payinfo eq $cust_main->paymask ) {
199       $new->payinfo($cust_main->payinfo);
200     } else {
201       $new->paycvv($p->{'paycvv'});
202     }
203   }
204
205   my @invoicing_list;
206   if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
207     #false laziness with httemplate/edit/process/cust_main.cgi
208     @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
209     push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
210   } else {
211     @invoicing_list = $cust_main->invoicing_list;
212   }
213
214   my $error = $new->replace($cust_main, \@invoicing_list);
215   return { 'error' => $error } if $error;
216   #$cust_main = $new;
217   
218   return { 'error' => '' };
219 }
220
221 sub payment_info {
222   my $p = shift;
223   my $session = _cache->get($p->{'session_id'})
224     or return { 'error' => "Can't resume session" }; #better error message
225
226   ##
227   #generic
228   ##
229
230   use vars qw($payment_info); #cache for performance
231   unless ( $payment_info ) {
232
233     my $conf = new FS::Conf;
234     my %states = map { $_->state => 1 }
235                    qsearch('cust_main_county', {
236                      'country' => $conf->config('countrydefault') || 'US'
237                    } );
238
239     $payment_info = {
240
241       #list all counties/states/countries
242       'cust_main_county' => 
243         [ map { $_->hashref } qsearch('cust_main_county', {}) ],
244
245       #shortcut for one-country folks
246       'states' =>
247         [ sort { $a cmp $b } keys %states ],
248
249       'card_types' => card_types(),
250
251       'paytypes' => [ @FS::cust_main::paytypes ],
252
253       'stateid_label' => FS::Msgcat::_gettext('stateid'),
254       'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'),
255
256       'show_ss'  => $conf->exists('show_ss'),
257       'show_stateid' => $conf->exists('show_stateid'),
258       'show_paystate' => $conf->exists('show_bankstate'),
259     };
260
261   }
262
263   ##
264   #customer-specific
265   ##
266
267   my %return = %$payment_info;
268
269   my $custnum = $session->{'custnum'};
270
271   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
272     or return { 'error' => "unknown custnum $custnum" };
273
274   $return{balance} = $cust_main->balance;
275
276   $return{payname} = $cust_main->payname
277                      || ( $cust_main->first. ' '. $cust_main->get('last') );
278
279   $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
280
281   $return{payby} = $cust_main->payby;
282   $return{stateid_state} = $cust_main->stateid_state;
283
284   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
285     $return{card_type} = cardtype($cust_main->payinfo);
286     $return{payinfo} = $cust_main->payinfo;
287
288     @return{'month', 'year'} = $cust_main->paydate_monthyear;
289
290   }
291
292   if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
293     my ($payinfo1, $payinfo2) = split '@', $cust_main->payinfo;
294     $return{payinfo1} = $payinfo1;
295     $return{payinfo2} = $payinfo2;
296     $return{paytype}  = $cust_main->paytype;
297     $return{paystate} = $cust_main->paystate;
298
299   }
300
301   #doubleclick protection
302   my $_date = time;
303   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
304
305   return { 'error' => '',
306            %return,
307          };
308
309 };
310
311 #some false laziness with httemplate/process/payment.cgi - look there for
312 #ACH and CVV support stuff
313 sub process_payment {
314
315   my $p = shift;
316
317   my $session = _cache->get($p->{'session_id'})
318     or return { 'error' => "Can't resume session" }; #better error message
319
320   my %return;
321
322   my $custnum = $session->{'custnum'};
323
324   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
325     or return { 'error' => "unknown custnum $custnum" };
326
327   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
328     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
329   my $payname = $1;
330
331   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
332     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
333   my $paybatch = $1;
334
335   $p->{'payby'} =~ /^([A-Z]{4})$/
336     or return { 'error' => "illegal_payby " . $p->{'payby'} };
337   my $payby = $1;
338
339   my $payinfo;
340   my $paycvv = '';
341   if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
342   
343     $p->{'payinfo1'} =~ /^(\d+)$/
344       or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
345     my $payinfo1 = $1;
346      $p->{'payinfo2'} =~ /^(\d+)$/
347       or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
348     my $payinfo2 = $1;
349     $payinfo = $payinfo1. '@'. $payinfo2;
350    
351   } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) {
352    
353     $payinfo = $p->{'payinfo'};
354     $payinfo =~ s/\D//g;
355     $payinfo =~ /^(\d{13,16})$/
356       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
357     $payinfo = $1;
358     validate($payinfo)
359       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
360     return { 'error' => gettext('unknown_card_type') }
361       if cardtype($payinfo) eq "Unknown";
362
363     if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
364       if ( cardtype($payinfo) eq 'American Express card' ) {
365         $p->{'paycvv'} =~ /^\s*(\d{4})\s*$/
366           or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
367         $paycvv = $1;
368       } else {
369         $p->{'paycvv'} =~ /^\s*(\d{3})\s*$/
370           or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
371         $paycvv = $1;
372       }
373     }
374   
375   } else {
376     die "unknown payby $payby";
377   }
378
379   my %payby2fields = (
380     'CARD' => [ qw( paystart_month paystart_year payissue address1 address2 city state zip payip ) ],
381     'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ],
382   );
383
384   my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $p->{'amount'},
385     'quiet'    => 1,
386     'payinfo'  => $payinfo,
387     'paydate'  => $p->{'year'}. '-'. $p->{'month'}. '-01',
388     'payname'  => $payname,
389     'paybatch' => $paybatch,
390     'paycvv'   => $paycvv,
391     map { $_ => $p->{$_} } @{ $payby2fields{$payby} }
392   );
393   return { 'error' => $error } if $error;
394
395   $cust_main->apply_payments;
396
397   if ( $p->{'save'} ) {
398     my $new = new FS::cust_main { $cust_main->hash };
399     if ($payby eq 'CARD' || $payby eq 'DCRD') {
400       $new->set( $_ => $p->{$_} )
401         foreach qw( payname paystart_month paystart_year payissue payip
402                     address1 address2 city state zip payinfo );
403       $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
404     } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
405       $new->set( $_ => $p->{$_} )
406         foreach qw( payname payip paytype paystate
407                     stateid stateid_state );
408       $new->set( 'payinfo' => $payinfo );
409       $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' );
410     }
411     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
412     my $error = $new->replace($cust_main);
413     return { 'error' => $error } if $error;
414     $cust_main = $new;
415   }
416
417   return { 'error' => '' };
418
419 }
420
421 sub process_prepay {
422
423   my $p = shift;
424
425   my $session = _cache->get($p->{'session_id'})
426     or return { 'error' => "Can't resume session" }; #better error message
427
428   my %return;
429
430   my $custnum = $session->{'custnum'};
431
432   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
433     or return { 'error' => "unknown custnum $custnum" };
434
435   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = ( 0, 0, 0, 0, 0 );
436   my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
437                                            \$amount,
438                                            \$seconds,
439                                            \$upbytes,
440                                            \$downbytes,
441                                            \$totalbytes,
442                                          );
443
444   return { 'error' => $error } if $error;
445
446   return { 'error'     => '',
447            'amount'    => $amount,
448            'seconds'   => $seconds,
449            'duration'  => duration_exact($seconds),
450            'upbytes'   => $upbytes,
451            'upload'    => FS::UI::bytecount::bytecount_unexact($upbytes),
452            'downbytes' => $downbytes,
453            'download'  => FS::UI::bytecount::bytecount_unexact($downbytes),
454            'totalbytes'=> $totalbytes,
455            'totalload' => FS::UI::bytecount::bytecount_unexact($totalbytes),
456          };
457
458 }
459
460 sub invoice {
461   my $p = shift;
462   my $session = _cache->get($p->{'session_id'})
463     or return { 'error' => "Can't resume session" }; #better error message
464
465   my $custnum = $session->{'custnum'};
466
467   my $invnum = $p->{'invnum'};
468
469   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
470                                           'custnum' => $custnum } )
471     or return { 'error' => "Can't find invnum" };
472
473   #my %return;
474
475   return { 'error'        => '',
476            'invnum'       => $invnum,
477            'invoice_text' => join('', $cust_bill->print_text ),
478            'invoice_html' => $cust_bill->print_html,
479          };
480
481 }
482
483 sub invoice_logo {
484   my $p = shift;
485
486   #sessioning for this?  how do we get the session id to the backend invoice
487   # template so it can add it to the link, blah
488
489   my $templatename = $p->{'templatename'};
490
491   #false laziness-ish w/view/cust_bill-logo.cgi
492
493   my $conf = new FS::Conf;
494   if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
495     $templatename = "_$1";
496   } else {
497     $templatename = '';
498   }
499
500   my $filename = "logo$templatename.png";
501
502   return { 'error'        => '',
503            'logo'         => $conf->config_binary($filename),
504            'content_type' => 'image/png', #should allow gif, jpg too
505          };
506 }
507
508
509 sub list_invoices {
510   my $p = shift;
511   my $session = _cache->get($p->{'session_id'})
512     or return { 'error' => "Can't resume session" }; #better error message
513
514   my $custnum = $session->{'custnum'};
515
516   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
517     or return { 'error' => "unknown custnum $custnum" };
518
519   my @cust_bill = $cust_main->cust_bill;
520
521   return  { 'error'       => '',
522             'invoices'    =>  [ map { { 'invnum' => $_->invnum,
523                                         '_date'  => $_->_date,
524                                       }
525                                     } @cust_bill
526                               ]
527           };
528 }
529
530 sub cancel {
531   my $p = shift;
532   my $session = _cache->get($p->{'session_id'})
533     or return { 'error' => "Can't resume session" }; #better error message
534
535   my $custnum = $session->{'custnum'};
536
537   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
538     or return { 'error' => "unknown custnum $custnum" };
539
540   my @errors = $cust_main->cancel( 'quiet'=>1 );
541
542   my $error = scalar(@errors) ? join(' / ', @errors) : '';
543
544   return { 'error' => $error };
545
546 }
547
548 sub list_pkgs {
549   my $p = shift;
550
551   my($context, $session, $custnum) = _custoragent_session_custnum($p);
552   return { 'error' => $session } if $context eq 'error';
553
554   my $search = { 'custnum' => $custnum };
555   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
556   my $cust_main = qsearchs('cust_main', $search )
557     or return { 'error' => "unknown custnum $custnum" };
558
559   #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
560
561   my $conf = new FS::Conf;
562
563   { 'svcnum'   => $session->{'svcnum'},
564     'custnum'  => $custnum,
565     'cust_pkg' => [ map {
566                           { $_->hash,
567                             $_->part_pkg->hash,
568                             part_svc =>
569                               [ map $_->hashref, $_->available_part_svc ],
570                             cust_svc => 
571                               [ map { my $ref = { $_->hash,
572                                                   label => [ $_->label ],
573                                                 };
574                                       $ref->{_password} = $_->svc_x->_password
575                                         if $context eq 'agent'
576                                         && $conf->exists('agent-showpasswords')
577                                         && $_->part_svc->svcdb eq 'svc_acct';
578                                       $ref;
579                                     } $_->cust_svc
580                               ],
581                           };
582                         } $cust_main->ncancelled_pkgs
583                   ],
584     'small_custview' =>
585       small_custview( $cust_main, $conf->config('countrydefault') ),
586   };
587
588 }
589
590 sub list_svcs {
591   my $p = shift;
592
593   my($context, $session, $custnum) = _custoragent_session_custnum($p);
594   return { 'error' => $session } if $context eq 'error';
595
596   my $search = { 'custnum' => $custnum };
597   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
598   my $cust_main = qsearchs('cust_main', $search )
599     or return { 'error' => "unknown custnum $custnum" };
600
601   my @cust_svc = ();
602   #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
603   foreach my $cust_pkg ( $p->{'ncancelled'} 
604                          ? $cust_main->ncancelled_pkgs
605                          : $cust_main->unsuspended_pkgs ) {
606     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
607   }
608   @cust_svc = grep { $_->part_svc->svcdb eq $p->{'svcdb'} } @cust_svc
609     if $p->{'svcdb'};
610
611   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
612   #              @svc_x;
613
614   { 
615     #no#'svcnum'   => $session->{'svcnum'},
616     'custnum'  => $custnum,
617     'svcs'     => [ map { 
618                           my $svc_x = $_->svc_x;
619                           my($label, $value) = $_->label;
620                           my $part_pkg = $svc_x->cust_svc->cust_pkg->part_pkg;
621
622                           { 'svcnum'    => $_->svcnum,
623                             'label'     => $label,
624                             'value'     => $value,
625                             'username'  => $svc_x->username,
626                             'email'     => $svc_x->email,
627                             'seconds'   => $svc_x->seconds,
628                             'upbytes'   => FS::UI::bytecount::display_bytecount($svc_x->upbytes),
629                             'downbytes' => FS::UI::bytecount::display_bytecount($svc_x->downbytes),
630                             'totalbytes'=> FS::UI::bytecount::display_bytecount($svc_x->totalbytes),
631                             'recharge_amount' => $part_pkg->option('recharge_amount', 1),
632                             'recharge_seconds' => $part_pkg->option('recharge_seconds', 1),
633                             'recharge_upbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_upbytes', 1)),
634                             'recharge_downbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_downbytes', 1)),
635                             'recharge_totalbytes' => FS::UI::bytecount::display_bytecount($part_pkg->option('recharge_totalbytes', 1)),
636                             # more...
637                           };
638                         }
639                         @cust_svc
640                   ],
641   };
642
643 }
644
645 sub list_svc_usage {
646   my $p = shift;
647
648   my($context, $session, $custnum) = _custoragent_session_custnum($p);
649   return { 'error' => $session } if $context eq 'error';
650
651   my $search = { 'svcnum' => $p->{'svcnum'} };
652   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
653   my $svc_acct = qsearchs ( 'svc_acct', $search );
654   return { 'error' => 'No service selected in list_svc_usage' } 
655     unless $svc_acct;
656
657   my $freq   = $svc_acct->cust_svc->cust_pkg->part_pkg->freq;
658   my $start  = $svc_acct->cust_svc->cust_pkg->setup;
659   #my $end    = $svc_acct->cust_svc->cust_pkg->bill; # or time?
660   my $end    = time;
661
662   unless($p->{beginning}){
663     $p->{beginning} = $svc_acct->cust_svc->cust_pkg->last_bill;
664     $p->{ending} = $end;
665   }
666   my @usage = ();
667
668   foreach my $part_export ( 
669     map { qsearch ( 'part_export', { 'exporttype' => $_ } ) }
670     qw (sqlradius sqlradius_withdomain')
671   ) {
672
673     push @usage, @ { $part_export->usage_sessions($p->{beginning},
674                                                   $p->{ending},
675                                                   $svc_acct)
676                    };
677   }
678
679   #kinda false laziness with FS::cust_main::bill, but perhaps
680   #we should really change this bit to DateTime and DateTime::Duration
681   #
682   #change this bit to use Date::Manip? CAREFUL with timezones (see
683   # mailing list archive)
684   my ($nsec,$nmin,$nhour,$nmday,$nmon,$nyear) =
685     (localtime($p->{ending}) )[0,1,2,3,4,5];
686   my ($psec,$pmin,$phour,$pmday,$pmon,$pyear) =
687     (localtime($p->{beginning}) )[0,1,2,3,4,5];
688
689   if ( $freq =~ /^\d+$/ ) {
690     $nmon += $freq;
691     until ( $nmon < 12 ) { $nmon -= 12; $nyear++; }
692     $pmon -= $freq;
693     until ( $pmon >= 0 ) { $pmon += 12; $pyear--; }
694   } elsif ( $freq =~ /^(\d+)w$/ ) {
695     my $weeks = $1;
696     $nmday += $weeks * 7;
697     $pmday -= $weeks * 7;
698   } elsif ( $freq =~ /^(\d+)d$/ ) {
699     my $days = $1;
700     $nmday += $days;
701     $pmday -= $days;
702   } elsif ( $freq =~ /^(\d+)h$/ ) {
703     my $hours = $1;
704     $nhour += $hours;
705     $phour -= $hours;
706   } else {
707     return { 'error' => "unparsable frequency: ". $freq };
708   }
709   
710   my $previous  = timelocal_nocheck($psec,$pmin,$phour,$pmday,$pmon,$pyear);
711   my $next      = timelocal_nocheck($nsec,$nmin,$nhour,$nmday,$nmon,$nyear);
712
713
714   { 
715     'error'     => '',
716     'svcnum'    => $p->{svcnum},
717     'beginning' => $p->{beginning},
718     'ending'    => $p->{ending},
719     'previous'  => ($previous > $start) ? $previous : $start,
720     'next'      => ($next < $end) ? $next : $end,
721     'usage'     => \@usage,
722   };
723 }
724
725 sub order_pkg {
726   my $p = shift;
727
728   my($context, $session, $custnum) = _custoragent_session_custnum($p);
729   return { 'error' => $session } if $context eq 'error';
730
731   my $search = { 'custnum' => $custnum };
732   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
733   my $cust_main = qsearchs('cust_main', $search )
734     or return { 'error' => "unknown custnum $custnum" };
735
736   #false laziness w/ClientAPI/Signup.pm
737
738   my $cust_pkg = new FS::cust_pkg ( {
739     'custnum' => $custnum,
740     'pkgpart' => $p->{'pkgpart'},
741   } );
742   my $error = $cust_pkg->check;
743   return { 'error' => $error } if $error;
744
745   my @svc = ();
746   unless ( $p->{'svcpart'} eq 'none' ) {
747
748     my $svcdb;
749     my $svcpart = '';
750     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
751       $svcpart = $1;
752       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
753       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
754       $svcdb = $part_svc->svcdb;
755     } else {
756       $svcdb = 'svc_acct';
757     }
758     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
759
760     my %fields = (
761       'svc_acct'     => [ qw( username domsvc _password sec_phrase popnum ) ],
762       'svc_domain'   => [ qw( domain ) ],
763       'svc_external' => [ qw( id title ) ],
764     );
765   
766     my $svc_x = "FS::$svcdb"->new( {
767       'svcpart'   => $svcpart,
768       map { $_ => $p->{$_} } @{$fields{$svcdb}}
769     } );
770     
771     if ( $svcdb eq 'svc_acct' ) {
772       my @acct_snarf;
773       my $snarfnum = 1;
774       while ( length($p->{"snarf_machine$snarfnum"}) ) {
775         my $acct_snarf = new FS::acct_snarf ( {
776           'machine'   => $p->{"snarf_machine$snarfnum"},
777           'protocol'  => $p->{"snarf_protocol$snarfnum"},
778           'username'  => $p->{"snarf_username$snarfnum"},
779           '_password' => $p->{"snarf_password$snarfnum"},
780         } );
781         $snarfnum++;
782         push @acct_snarf, $acct_snarf;
783       }
784       $svc_x->child_objects( \@acct_snarf );
785     }
786     
787     my $y = $svc_x->setdefault; # arguably should be in new method
788     return { 'error' => $y } if $y && !ref($y);
789   
790     $error = $svc_x->check;
791     return { 'error' => $error } if $error;
792
793     push @svc, $svc_x;
794
795   }
796
797   use Tie::RefHash;
798   tie my %hash, 'Tie::RefHash';
799   %hash = ( $cust_pkg => \@svc );
800   #msgcat
801   $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
802   return { 'error' => $error } if $error;
803
804   my $conf = new FS::Conf;
805   if ( $conf->exists('signup_server-realtime') ) {
806
807     my $bill_error = _do_bop_realtime( $cust_main );
808
809     if ($bill_error) {
810       $cust_pkg->cancel('quiet'=>1);
811       return $bill_error;
812     } else {
813       $cust_pkg->reexport;
814     }
815
816   } else {
817     $cust_pkg->reexport;
818   }
819
820   return { error => '', pkgnum => $cust_pkg->pkgnum };
821
822 }
823
824 sub change_pkg {
825   my $p = shift;
826
827   my($context, $session, $custnum) = _custoragent_session_custnum($p);
828   return { 'error' => $session } if $context eq 'error';
829
830   my $search = { 'custnum' => $custnum };
831   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
832   my $cust_main = qsearchs('cust_main', $search )
833     or return { 'error' => "unknown custnum $custnum" };
834
835   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
836     or return { 'error' => "unknown package $p->{pkgnum}" };
837
838   my @newpkg;
839   my $error = FS::cust_pkg::order( $custnum,
840                                    [$p->{pkgpart}],
841                                    [$p->{pkgnum}],
842                                    \@newpkg,
843                                  );
844
845   my $conf = new FS::Conf;
846   if ( $conf->exists('signup_server-realtime') ) {
847
848     my $bill_error = _do_bop_realtime( $cust_main );
849
850     if ($bill_error) {
851       $newpkg[0]->suspend;
852       return $bill_error;
853     } else {
854       $newpkg[0]->reexport;
855     }
856
857   } else {  
858     $newpkg[0]->reexport;
859   }
860
861   return { error => '', pkgnum => $cust_pkg->pkgnum };
862
863 }
864
865 sub order_recharge {
866   my $p = shift;
867
868   my($context, $session, $custnum) = _custoragent_session_custnum($p);
869   return { 'error' => $session } if $context eq 'error';
870
871   my $search = { 'custnum' => $custnum };
872   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
873   my $cust_main = qsearchs('cust_main', $search )
874     or return { 'error' => "unknown custnum $custnum" };
875
876   my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } )
877     or return { 'error' => "unknown service " . $p->{'svcnum'} };
878
879   my $svc_x = $cust_svc->svc_x;
880   my $part_pkg = $cust_svc->cust_pkg->part_pkg;
881
882   my %vhash =
883     map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) } 
884     qw ( recharge_seconds recharge_upbytes recharge_downbytes
885          recharge_totalbytes );
886   my $amount = $part_pkg->option('recharge_amount', 1); 
887   
888   my ($l, $v, $d) = $cust_svc->label;  # blah
889   my $pkg = "Recharge $v"; 
890
891   my $bill_error = $cust_main->charge($amount, $pkg,
892      "time: $vhash{seconds}, up: $vhash{upbytes}," . 
893      "down: $vhash{downbytes}, total: $vhash{totalbytes}",
894      $part_pkg->taxclass); #meh
895
896   my $conf = new FS::Conf;
897   if ( $conf->exists('signup_server-realtime') && !$bill_error ) {
898
899     $bill_error = _do_bop_realtime( $cust_main );
900
901     if ($bill_error) {
902       return $bill_error;
903     } else {
904       my $error = $svc_x->recharge (\%vhash);
905       return { 'error' => $error } if $error;
906     }
907
908   } else {  
909     my $error = $bill_error;
910     $error ||= $svc_x->recharge (\%vhash);
911     return { 'error' => $error } if $error;
912   }
913
914   return { error => '', svc => $cust_svc->part_svc->svc };
915
916 }
917
918 sub _do_bop_realtime {
919   my ($cust_main) = @_;
920
921     my $old_balance = $cust_main->balance;
922
923     my $bill_error =    $cust_main->bill
924                      || $cust_main->apply_payments_and_credits
925                      || $cust_main->collect('realtime' => 1);
926
927     if (    $cust_main->balance > $old_balance
928          && $cust_main->balance > 0
929          && $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ) {
930       #this makes sense.  credit is "un-doing" the invoice
931       $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
932                           'self-service decline' );
933       $cust_main->apply_credits( 'order' => 'newest' );
934
935       return { 'error' => '_decline', 'bill_error' => $bill_error };
936     }
937
938     '';
939 }
940
941 sub cancel_pkg {
942   my $p = shift;
943   my $session = _cache->get($p->{'session_id'})
944     or return { 'error' => "Can't resume session" }; #better error message
945
946   my $custnum = $session->{'custnum'};
947
948   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
949     or return { 'error' => "unknown custnum $custnum" };
950
951   my $pkgnum = $p->{'pkgnum'};
952
953   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
954                                         'pkgnum'  => $pkgnum,   } )
955     or return { 'error' => "unknown pkgnum $pkgnum" };
956
957   my $error = $cust_pkg->cancel( 'quiet'=>1 );
958   return { 'error' => $error };
959
960 }
961
962 sub provision_acct {
963   my $p = shift;
964
965   return { 'error' => gettext('passwords_dont_match') }
966     if $p->{'_password'} ne $p->{'_password2'};
967   return { 'error' => gettext('empty_password') }
968     unless length($p->{'_password'});
969
970   _provision( 'FS::svc_acct',
971               [qw(username _password)],
972               [qw(username _password)],
973               $p,
974               @_
975             );
976 }
977
978 sub provision_external {
979   my $p = shift;
980   #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
981   _provision( 'FS::svc_external',
982               [],
983               [qw(id title)],
984               $p,
985               @_
986             );
987 }
988
989 sub _provision {
990   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
991
992   my($context, $session, $custnum) = _custoragent_session_custnum($p);
993   return { 'error' => $session } if $context eq 'error';
994
995   my $search = { 'custnum' => $custnum };
996   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
997   my $cust_main = qsearchs('cust_main', $search )
998     or return { 'error' => "unknown custnum $custnum" };
999
1000   my $pkgnum = $p->{'pkgnum'};
1001
1002   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1003                                         'pkgnum'  => $pkgnum,
1004                                                                } )
1005     or return { 'error' => "unknown pkgnum $pkgnum" };
1006
1007   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
1008     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
1009
1010   my $svc_x = $class->new( {
1011     'pkgnum'  => $p->{'pkgnum'},
1012     'svcpart' => $p->{'svcpart'},
1013     map { $_ => $p->{$_} } @$fields
1014   } );
1015   my $error = $svc_x->insert;
1016   $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
1017     unless $error;
1018
1019   return { 'svc'   => $part_svc->svc,
1020            'error' => $error,
1021            map { $_ => $svc_x->get($_) } @$return_fields
1022          };
1023
1024 }
1025
1026 sub part_svc_info {
1027   my $p = shift;
1028
1029   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1030   return { 'error' => $session } if $context eq 'error';
1031
1032   my $search = { 'custnum' => $custnum };
1033   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1034   my $cust_main = qsearchs('cust_main', $search )
1035     or return { 'error' => "unknown custnum $custnum" };
1036
1037   my $pkgnum = $p->{'pkgnum'};
1038
1039   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1040                                         'pkgnum'  => $pkgnum,
1041                                                                } )
1042     or return { 'error' => "unknown pkgnum $pkgnum" };
1043
1044   my $svcpart = $p->{'svcpart'};
1045
1046   my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
1047                                       'svcpart' => $svcpart,           } )
1048     or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
1049   my $part_svc = $pkg_svc->part_svc;
1050
1051   my $conf = new FS::Conf;
1052
1053   return {
1054     'svc'     => $part_svc->svc,
1055     'svcdb'   => $part_svc->svcdb,
1056     'pkgnum'  => $pkgnum,
1057     'svcpart' => $svcpart,
1058     'custnum' => $custnum,
1059
1060     'security_phrase' => 0, #XXX !
1061     'svc_acct_pop'    => [], #XXX !
1062     'popnum'          => '',
1063     'init_popstate'   => '',
1064     'popac'           => '',
1065     'acstate'         => '',
1066
1067     'small_custview' =>
1068       small_custview( $cust_main, $conf->config('countrydefault') ),
1069
1070   };
1071
1072 }
1073
1074 sub unprovision_svc {
1075   my $p = shift;
1076
1077   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1078   return { 'error' => $session } if $context eq 'error';
1079
1080   my $search = { 'custnum' => $custnum };
1081   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1082   my $cust_main = qsearchs('cust_main', $search )
1083     or return { 'error' => "unknown custnum $custnum" };
1084
1085   my $svcnum = $p->{'svcnum'};
1086
1087   my $cust_svc = qsearchs('cust_svc', { 'svcnum'  => $svcnum, } )
1088     or return { 'error' => "unknown svcnum $svcnum" };
1089
1090   return { 'error' => "Service $svcnum does not belong to customer $custnum" }
1091     unless $cust_svc->cust_pkg->custnum == $custnum;
1092
1093   my $conf = new FS::Conf;
1094
1095   return { 'svc'   => $cust_svc->part_svc->svc,
1096            'error' => $cust_svc->cancel,
1097            'small_custview' =>
1098              small_custview( $cust_main, $conf->config('countrydefault') ),
1099          };
1100
1101 }
1102
1103 sub myaccount_passwd {
1104   my $p = shift;
1105   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1106   return { 'error' => $session } if $context eq 'error';
1107
1108   return { 'error' => "New passwords don't match." }
1109     if $p->{'new_password'} ne $p->{'new_password2'};
1110
1111   return { 'error' => 'Enter new password' }
1112     unless length($p->{'new_password'});
1113
1114   #my $search = { 'custnum' => $custnum };
1115   #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1116   $custnum =~ /^(\d+)$/ or die "illegal custnum";
1117   my $search = " AND custnum = $1";
1118   $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
1119
1120   my $svc_acct = qsearchs( {
1121     'table'     => 'svc_acct',
1122     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
1123                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
1124                    'LEFT JOIN cust_main USING ( custnum ) ',
1125     'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
1126     'extra_sql' => $search, #important
1127   } )
1128     or return { 'error' => "Service not found" };
1129
1130   $svc_acct->_password($p->{'new_password'});
1131   my $error = $svc_acct->replace();
1132
1133   my($label, $value) = $svc_acct->cust_svc->label;
1134
1135   return { 'error' => $error,
1136            'label' => $label,
1137            'value' => $value,
1138          };
1139
1140 }
1141
1142 #--
1143
1144 sub _custoragent_session_custnum {
1145   my $p = shift;
1146
1147   my($context, $session, $custnum);
1148   if ( $p->{'session_id'} ) {
1149
1150     $context = 'customer';
1151     $session = _cache->get($p->{'session_id'})
1152       or return ( 'error' => "Can't resume session" ); #better error message
1153     $custnum = $session->{'custnum'};
1154
1155   } elsif ( $p->{'agent_session_id'} ) {
1156
1157     $context = 'agent';
1158     my $agent_cache = new FS::ClientAPI_SessionCache( {
1159       'namespace' => 'FS::ClientAPI::Agent',
1160     } );
1161     $session = $agent_cache->get($p->{'agent_session_id'})
1162       or return ( 'error' => "Can't resume session" ); #better error message
1163     $custnum = $p->{'custnum'};
1164
1165   } else {
1166     return ( 'error' => "Can't resume session" ); #better error message
1167   }
1168
1169   ($context, $session, $custnum);
1170
1171 }
1172
1173 1;
1174