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