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