49e61ab7b6fdefb7fcc2d035f96cfd2ab15545a0
[freeside.git] / FS / FS / ClientAPI / MyAccount.pm
1 package FS::ClientAPI::MyAccount;
2
3 use 5.008; #require 5.8+ for Time::Local 1.05+
4 use strict;
5 use vars qw( $cache $DEBUG $me );
6 use subs qw( _cache _provision );
7 use IO::Scalar;
8 use Data::Dumper;
9 use Digest::MD5 qw(md5_hex);
10 use Digest::SHA qw(sha512_hex);
11 use Date::Format;
12 use Time::Duration;
13 use Time::Local qw(timelocal_nocheck);
14 use Business::CreditCard 0.35;
15 use HTML::Entities;
16 use Text::CSV_XS;
17 use Spreadsheet::WriteExcel;
18 use OLE::Storage_Lite;
19 use FS::UI::Web::small_custview qw(small_custview); #less doh
20 use FS::UI::Web;
21 use FS::UI::bytecount qw( display_bytecount );
22 use FS::Conf;
23 #use FS::UID qw(dbh);
24 use FS::Record qw(qsearch qsearchs dbh);
25 use FS::Msgcat qw(gettext);
26 use FS::Misc qw(card_types money_pretty);
27 use FS::Misc::DateTime qw(parse_datetime);
28 use FS::TicketSystem;
29 use FS::ClientAPI_SessionCache;
30 use FS::cust_svc;
31 use FS::svc_acct;
32 use FS::svc_forward;
33 use FS::svc_domain;
34 use FS::svc_phone;
35 use FS::svc_external;
36 use FS::svc_dsl;
37 use FS::dsl_device;
38 use FS::part_svc;
39 use FS::cust_main;
40 use FS::cust_bill;
41 use FS::legacy_cust_bill;
42 use FS::cust_main_county;
43 use FS::part_pkg;
44 use FS::cust_pkg;
45 use FS::payby;
46 use FS::acct_rt_transaction;
47 use FS::msg_template;
48 use FS::contact;
49 use FS::cust_contact;
50 use FS::cust_location;
51 use FS::cust_payby;
52
53 # for code organization
54 use FS::ClientAPI::MyAccount::contact;
55 use FS::ClientAPI::MyAccount::quotation;
56
57 $DEBUG = 0;
58 $me = '[FS::ClientAPI::MyAccount]';
59
60 use vars qw( @cust_main_editable_fields @location_editable_fields );
61 @cust_main_editable_fields = qw(
62   first last company daytime night fax mobile
63   locale
64   ss stateid stateid_state
65 );
66 @location_editable_fields = qw(
67   address1 address2 city county state zip country
68 );
69
70
71 BEGIN { #preload to reduce time customer_info takes
72   if ( $FS::TicketSystem::system ) {
73     warn "$me: initializing ticket system\n" if $DEBUG;
74     FS::TicketSystem->init();
75   }
76 }
77
78 sub _cache {
79   $cache ||= new FS::ClientAPI_SessionCache( {
80                'namespace' => 'FS::ClientAPI::MyAccount',
81              } );
82 }
83
84 sub skin_info {
85   my $p = shift;
86
87   my($context, $session, $custnum) = _custoragent_session_custnum($p);
88   #return { 'error' => $session } if $context eq 'error';
89
90   my $agentnum = '';
91   if ( $context eq 'customer' && $custnum ) {
92
93     my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
94       or die dbh->errstr;
95
96     $sth->execute($custnum) or die $sth->errstr;
97
98     $agentnum = $sth->fetchrow_arrayref->[0]
99       or die "no agentnum for custnum $custnum";
100
101   #} elsif ( $context eq 'agent' ) {
102   } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) {
103     $agentnum = $1;
104   }
105   $p->{'agentnum'} = $agentnum;
106
107   my $conf = new FS::Conf;
108
109   #false laziness w/Signup.pm
110
111   my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum");
112
113   if ( $skin_info_cache_agent ) {
114
115     warn "$me loading cached skin info for agentnum $agentnum\n"
116       if $DEBUG > 1;
117
118   } else {
119
120     warn "$me populating skin info cache for agentnum $agentnum\n"
121       if $DEBUG > 1;
122
123     $skin_info_cache_agent = {
124       'agentnum' => $agentnum,
125       ( map { $_ => scalar( $conf->config($_, $agentnum) ) }
126         qw( company_name date_format ) ),
127       ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) }
128         qw( body_bgcolor box_bgcolor stripe1_bgcolor stripe2_bgcolor
129             text_color link_color vlink_color hlink_color alink_color
130             font title_color title_align title_size menu_bgcolor menu_fontsize
131           )
132       ),
133       'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ],
134       ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
135         qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo enable_payment_without_balance )
136       ),
137       ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) }
138         qw( title_left_image title_right_image
139             menu_top_image menu_body_image menu_bottom_image
140           )
141       ),
142       'logo' => scalar($conf->config_binary('logo.png', $agentnum )),
143       ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
144         qw( head body_header body_footer company_address ) ),
145       'money_char' => $conf->config("money_char") || '$',
146       'menu' => join("\n", $conf->config("ng_selfservice-menu", $agentnum ) ) ||
147                 'main.php Home
148
149                  services.php Services
150                  services.php My Services
151                  services_new.php Order a new service
152
153                  personal.php Profile
154                  personal.php Personal Information
155                  password.php Change Password
156
157                  payment.php Payments
158                  payment_cc.php Credit Card Payment
159                  payment_ach.php Electronic Check Payment
160                  payment_paypal.php PayPal Payment
161                  payment_webpay.php Webpay Payments
162
163                  usage.php Usage
164                  usage_data.php Data usage
165                  usage_cdr.php Call usage
166
167                  tickets.php Help Desk
168                  tickets.php Open Tickets
169                  tickets_resolved.php Resolved Tickets
170                  ticket_create.php Create a new ticket
171
172                  docs.php FAQs
173
174                  logout.php Logout
175                 ',
176     };
177
178     _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
179
180   }
181
182   #{ %$skin_info_cache_agent };
183   $skin_info_cache_agent;
184
185 }
186
187 sub login_info {
188   my $p = shift;
189
190   my $conf = new FS::Conf;
191
192   my %info = (
193     %{ skin_info($p) },
194     'phone_login'  => $conf->exists('selfservice_server-phone_login'),
195     'single_domain'=> scalar($conf->config('selfservice_server-single_domain')),
196     'banner_url'       => scalar($conf->config('selfservice-login_banner_url')),
197     'banner_image_md5' => 
198       md5_hex($conf->config_binary('selfservice-login_banner_image')),
199   );
200
201   return \%info;
202
203 }
204
205 sub login_banner_image {
206   my $p = shift;
207   my $conf = new FS::Conf;
208   my $image = $conf->config_binary('selfservice-login_banner_image');
209   return { 
210     'md5'   => md5_hex($image),
211     'image' => $image,
212   };
213 }
214
215 #false laziness w/FS::ClientAPI::passwd::passwd
216 sub login {
217   my $p = shift;
218
219   my $conf = new FS::Conf;
220
221   my $svc_x = '';
222   my $session = {};
223   if ( $p->{'domain'} eq 'svc_phone'
224        && $conf->exists('selfservice_server-phone_login') ) { 
225
226     my $svc_phone = qsearchs( 'svc_phone', { 'phonenum' => $p->{'username'} } );
227     return { error => 'Number not found.' } unless $svc_phone;
228
229     #XXX?
230     #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
231     #return { error => 'Only primary user may log in.' } 
232     #  if $conf->exists('selfservice_server-primary_only')
233     #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
234
235     return { error => 'Incorrect PIN.' }
236       unless $svc_phone->check_pin($p->{'password'});
237
238     $svc_x = $svc_phone;
239
240   } elsif ( $p->{email}
241               && (my $contact = FS::contact->by_selfservice_email($p->{email}))
242           )
243   {
244     return { error => 'Incorrect contact password.' }
245       unless $contact->authenticate_password($p->{'password'});
246
247     $session->{'contactnum'} = $contact->contactnum;
248
249     my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
250     if ( scalar(@cust_contact) == 1 ) {
251       $session->{'custnum'} = $cust_contact[0]->custnum;
252     } elsif ( scalar(@cust_contact) ) {
253       $session->{'customers'} = { map { $_->custnum => $_->cust_main->name }
254                                     @cust_contact
255                                 };
256     } else {
257       return { error => 'No customer self-service access for contact' }; #??
258     }
259
260   } else {
261
262     ( $p->{username}, $p->{domain} ) = split('@', $p->{email}) if $p->{email};
263
264     my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
265       or return { error => 'Domain '. $p->{'domain'}. ' not found' };
266
267     my @svc_acct = qsearch( 'svc_acct', { 'username'  => $p->{'username'},
268                                           'domsvc'    => $svc_domain->svcnum, }
269                           );
270
271     if ( $conf->exists('selfservice_server-login_svcpart') ) {
272       my @svcpart = $conf->config('selfservice_server-login_svcpart');
273       @svc_acct = grep { my $svcpart = $_->cust_svc->svcpart;
274                          scalar( grep( $_ eq $svcpart, @svcpart ) );
275                        }
276                     @svc_acct;
277     }
278
279     if ( $conf->exists('selfservice_server-primary_only') ) {
280         @svc_acct =
281           grep {
282             my $cust_svc = $_->cust_svc;
283             $cust_svc->cust_pkg->part_pkg->svcpart([qw( svc_acct svc_phone )])
284               == $cust_svc->svcpart
285           }
286           @svc_acct;
287     }
288
289     return { error => 'User not found.' } unless @svc_acct;
290
291     return { error => 'Multiple users.' } if scalar(@svc_acct) > 1;
292
293     my $svc_acct = $svc_acct[0];
294
295     if ( $conf->exists('selfservice_server-login_svcpart') ) {
296       my @svcpart = $conf->config('selfservice_server-login_svcpart');
297       my $svcpart = $svc_acct->cust_svc->svcpart;
298       return { error => 'Invalid user.' } 
299         unless grep($_ eq $svcpart, @svcpart);
300     }
301
302     return { error => 'Incorrect password.' }
303       unless $svc_acct->check_password($p->{'password'});
304
305     $svc_x = $svc_acct;
306
307   }
308
309   if ( $svc_x ) {
310
311     $session->{'svcnum'} = $svc_x->svcnum;
312
313     my $cust_svc = $svc_x->cust_svc;
314     my $cust_pkg = $cust_svc->cust_pkg;
315     if ( $cust_pkg ) {
316       my $cust_main = $cust_pkg->cust_main;
317       $session->{'custnum'} = $cust_main->custnum;
318       if ( $conf->exists('pkg-balances') ) {
319         my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
320                             $cust_main->ncancelled_pkgs;
321         $session->{'pkgnum'} = $cust_pkg->pkgnum
322           if scalar(@cust_pkg) > 1;
323       }
324     }
325
326     #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
327     #return { error => 'Only primary user may log in.' } 
328     #  if $conf->exists('selfservice_server-primary_only')
329     #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
330     my $part_pkg = $cust_pkg->part_pkg;
331     return { error => 'Only primary user may log in.' }
332       if $conf->exists('selfservice_server-primary_only')
333          && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
334
335   }
336
337   my $session_id;
338   do {
339     $session_id = sha512_hex(time(). {}. rand(). $$)
340   } until ( ! defined _cache->get($session_id) ); #just in case
341
342   my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
343   _cache->set( $session_id, $session, $timeout );
344
345   return { 'error'      => '',
346            'session_id' => $session_id,
347            %$session,
348          };
349 }
350
351 sub logout {
352   my $p = shift;
353   my $skin_info = skin_info($p);
354   if ( $p->{'session_id'} ) {
355     _cache->remove($p->{'session_id'});
356     return { %$skin_info, 'error' => '' };
357   } else {
358     return { %$skin_info, 'error' => "Can't resume session" }; #better error message
359   }
360 }
361
362 sub switch_acct {
363   my $p = shift;
364
365   my($context, $session, $custnum) = _custoragent_session_custnum($p);
366   return { 'error' => $session } if $context eq 'error';
367
368   my $svc_acct = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct' )
369     or return { 'error' => "Service not found" };
370
371   $session->{'svcnum'} = $svc_acct->svcnum;
372
373   my $conf = new FS::Conf;
374   my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
375   _cache->set( $p->{'session_id'}, $session, $timeout );
376
377   return { 'error' => '' };
378
379 }
380
381 sub switch_cust {
382   my $p = shift;
383   my($context, $session, $custnum) = _custoragent_session_custnum($p);
384   return { 'error' => $session } if $context eq 'error';
385
386   $session->{'custnum'} = $p->{'custnum'}
387     if exists $session->{'customers'}{ $p->{'custnum'} };
388
389   my $conf = new FS::Conf;
390   my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
391   _cache->set( $p->{'session_id'}, $session, $timeout );
392
393   return { 'error'      => '',
394            %{ customer_info( { session_id=>$p->{'session_id'} } ) },
395          };
396 }
397
398 sub payment_gateway {
399   # internal use only
400   # takes a cust_main and a cust_payby entry, returns the payment_gateway
401   my $conf = new FS::Conf;
402   my $cust_main = shift;
403   my $cust_payby = shift;
404   return '' if ! FS::payby->realtime($cust_payby);
405   my $pg = $cust_main->agent->payment_gateway(
406     'method'  => FS::payby->payby2bop($cust_payby),
407     'nofatal' => 1
408   );
409   return $pg;
410 }
411
412 sub access_info {
413   my $p = shift;
414
415   my $conf = new FS::Conf;
416
417   my $info = skin_info($p);
418
419   use vars qw( $cust_paybys ); #cache for performance
420   unless ( $cust_paybys ) {
421
422     my %cust_paybys = map { $_ => 1 }
423                       map { FS::payby->payby2payment($_) }
424                           $conf->config('signup_server-payby');
425
426     $cust_paybys = [ keys %cust_paybys ];
427
428   }
429   $info->{'cust_paybys'} = $cust_paybys;
430
431   my($context, $session, $custnum) = _custoragent_session_custnum($p);
432   return { 'error' => $session } if $context eq 'error';
433
434   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
435
436   $info->{'hide_payment_fields'} = [ 
437     map { 
438       my $pg = $cust_main && payment_gateway($cust_main, $_);
439       $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
440     } @{ $info->{cust_paybys} }
441   ];
442
443   $info->{'self_suspend_reason'} = 
444       $conf->config('selfservice-self_suspend_reason',
445                       $cust_main ? $cust_main->agentnum : ''
446                    );
447
448   $info->{'edit_ticket_subject'} =
449       $conf->exists('ticket_system-selfservice_edit_subject') && 
450       $cust_main && $cust_main->edit_subject;
451
452   $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
453
454   $info->{'hide_usage'} = $conf->exists('selfservice_hide-usage');
455
456   return { %$info,
457            'custnum'       => $custnum,
458            'access_pkgnum' => $session->{'pkgnum'},
459            'access_svcnum' => $session->{'svcnum'},
460          };
461 }
462
463 sub customer_info {
464   my $p = shift;
465
466   my($context, $session, $custnum) = _custoragent_session_custnum($p);
467   return { 'error' => $session } if $context eq 'error';
468
469   my %return;
470
471   my $conf = new FS::Conf;
472   $return{'require_address2'} = $conf->exists('cust_main-require_address2');
473
474 #  if ( $FS::TicketSystem::system ) {
475 #    warn "$me customer_info: initializing ticket system\n" if $DEBUG;
476 #    FS::TicketSystem->init();
477 #  }
478  
479   if ( $custnum ) { #customer record
480
481     %return = ( %return, %{ customer_info_short($p) } );
482
483     #redundant with customer_info_short, but we need it for several things below
484     my $search = { 'custnum' => $custnum };
485     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
486     my $cust_main = qsearchs('cust_main', $search )
487       or return { 'error' => "customer_info: unknown custnum $custnum" };
488
489     my $list_tickets = list_tickets($p);
490     $return{'tickets'} = $list_tickets->{'tickets'};
491
492     if ( $session->{'pkgnum'} ) {
493       #XXX open invoices in the pkg-balances case
494     } else {
495       $return{'money_char'} = $conf->config("money_char") || '$';
496       my @open = map {
497                        {
498                          invnum     => $_->invnum,
499                          date       => time2str("%b %o, %Y", $_->_date),
500                          owed       => $_->owed,
501                          charged    => $_->charged,
502                        };
503                      } $cust_main->open_cust_bill;
504       $return{open_invoices} = \@open;
505
506       my $sql = 'SELECT MAX(_date) FROM cust_bill WHERE custnum = ?';
507       my $sth = dbh->prepare($sql) or die  dbh->errstr;
508       $sth->execute($custnum)      or die $sth->errstr;
509       $return{'last_invoice_date'} = $sth->fetchrow_arrayref->[0];
510       $return{'last_invoice_date_pretty'} =
511         time2str('%m/%d/%Y', $return{'last_invoice_date'} );
512     }
513
514     #customer_info_short always has nobalance on..
515     $return{small_custview} =
516       small_custview( $cust_main,
517                       $return{countrydefault},
518                       ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance
519                     );
520
521     $return{has_ship_address} = $cust_main->has_ship_address;
522     $return{status} = $cust_main->status_label; #$cust_main->status; #better to break anyone obscurely testing for strings in self-service than to have to upgrade every front-end to get the new status to display
523     $return{statuscolor} = $cust_main->statuscolor;
524     $return{status_label} = $cust_main->status_label;
525
526     # compatibility: some places in selfservice use this to determine
527     # if there's a ship address
528     if ( $return{has_ship_address} ) {
529       $return{ship_last}  = $cust_main->last;
530       $return{ship_first} = $cust_main->first;
531     }
532
533     if (scalar($conf->config('support_packages'))) {
534       my @support_services = ();
535       foreach ($cust_main->support_services) {
536         my $seconds = $_->svc_x->seconds || 0;
537         my $time_remaining = (($seconds < 0) ? '-' : '' ).
538                              int(abs($seconds)/3600)."h".
539                              sprintf("%02d",(abs($seconds)%3600)/60)."m";
540         my $cust_pkg = $_->cust_pkg;
541         my $pkgnum = '';
542         my $pkg = '';
543         $pkgnum = $cust_pkg->pkgnum if $cust_pkg;
544         $pkg = $cust_pkg->part_pkg->pkg if $cust_pkg;
545         push @support_services, { svcnum => $_->svcnum,
546                                   time => $time_remaining,
547                                   pkgnum => $pkgnum,
548                                   pkg => $pkg,
549                                 };
550       }
551       $return{support_services} = \@support_services;
552     }
553
554     if ( $conf->config('prepayment_discounts-credit_type') ) {
555       #need to eval?
556       $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
557     }
558
559   } elsif ( $session->{'svcnum'} ) { #no customer record
560
561     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
562       or die "unknown svcnum";
563     $return{name} = $svc_acct->email;
564
565   } else {
566
567     return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
568
569   }
570
571   return { 'error'   => '',
572            'custnum' => $custnum,
573            %return,
574          };
575
576 }
577
578 sub customer_info_short {
579   my $p = shift;
580
581   my($context, $session, $custnum) = _custoragent_session_custnum($p);
582   return { 'error' => $session } if $context eq 'error';
583
584   my %return;
585
586   my $conf = new FS::Conf;
587
588   if ( $custnum ) { #customer record
589
590     my $search = { 'custnum' => $custnum };
591     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
592     my $cust_main = qsearchs('cust_main', $search )
593       or return { 'error' => "customer_info_short: unknown custnum $custnum" };
594
595     $return{display_custnum} = $cust_main->display_custnum;
596     $return{max_invnum}      = $cust_main->max_invnum;
597
598     if ( $session->{'pkgnum'} ) { 
599       $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
600       #next_bill_date from cust_pkg?
601     } else {
602       $return{balance} = $cust_main->balance;
603       $return{next_bill_date} = $cust_main->next_bill_date;
604       $return{next_bill_date_pretty} =
605         $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
606                                 : '(none)';
607     }
608     $return{balance_pretty} = money_pretty($return{balance});
609
610     $return{countrydefault} = scalar($conf->config('countrydefault'));
611
612     $return{small_custview} =
613       small_custview( $cust_main,
614                       $return{countrydefault},
615                       1, ##nobalance
616                     );
617
618     $return{first}  = $cust_main->first;
619     $return{'last'} = $cust_main->get('last');
620     $return{name}   = $cust_main->first. ' '. $cust_main->get('last');
621
622     #none of these are terribly expensive if we want 'em...
623     for (@cust_main_editable_fields) {
624       $return{$_} = $cust_main->get($_);
625     }
626     #maybe a little more expensive, but it should be cached by now
627     for (@location_editable_fields) {
628       $return{$_} = $cust_main->bill_location->get($_)
629         if $cust_main->bill_locationnum;
630       $return{'ship_'.$_} = $cust_main->ship_location->get($_)
631         if $cust_main->ship_locationnum;
632     }
633  
634     my @invoicing_list = $cust_main->invoicing_list;
635     $return{'invoicing_list'} =
636       join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
637     $return{'postal_invoicing'} =
638       0 < ( grep { $_ eq 'POST' } @invoicing_list );
639
640     if ( $session->{'svcnum'} ) {
641       my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} });
642       $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc;
643       $return{'svcnum'} = $session->{'svcnum'};
644     }
645
646   } elsif ( $session->{'svcnum'} ) { #no customer record
647
648     #uuh, not supproted yet... die?
649     return { 'error' => 'customer_info_short not yet supported as agent' };
650
651   } else {
652
653     return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
654
655   }
656
657   # this is here because this routine is called by both fs_ and ng_ main pages, where it appears
658   # it is not customer-specific, though it is only shown to authenticated customers
659   # it is not currently agent-specific, though at some point it might be
660   $return{'announcement'} = join(' ',$conf->config('selfservice-announcement')) || '';
661
662   return { 'error'          => '',
663            'custnum'        => $custnum,
664            %return,
665          };
666 }
667
668 sub customer_recurring {
669   my $p = shift;
670
671   my($context, $session, $custnum) = _custoragent_session_custnum($p);
672   return { 'error' => $session } if $context eq 'error';
673
674   my %return;
675
676   my $conf = new FS::Conf;
677
678   my $search = { 'custnum' => $custnum };
679   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
680   my $cust_main = qsearchs('cust_main', $search )
681     or return { 'error' => "customer_info_short: unknown custnum $custnum" };
682
683   $return{'display_recurring'} = [ $cust_main->display_recurring ];
684
685   return { 'error'          => '',
686            'custnum'        => $custnum,
687            %return,
688          };
689 }
690
691 sub billing_history {
692   my $p = shift;
693
694   my($context, $session, $custnum) = _custoragent_session_custnum($p);
695   return { 'error' => $session } if $context eq 'error';
696
697   return { 'error' => 'No customer' } unless $custnum;
698
699   my $search = { 'custnum' => $custnum };
700   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
701   my $cust_main = qsearchs('cust_main', $search )
702     or return { 'error' => "unknown custnum $custnum" };
703
704   my %return = ();
705
706   if ( $session->{'pkgnum'} ) { 
707     #$return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
708     #next_bill_date from cust_pkg?
709     return { 'error' => 'No history for package' };
710   }
711
712   $return{balance} = $cust_main->balance;
713   $return{balance_pretty} = money_pretty($return{balance});
714   $return{next_bill_date} = $cust_main->next_bill_date;
715   $return{next_bill_date_pretty} =
716     $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
717                             : '(none)';
718
719   my $conf = new FS::Conf;
720
721   $return{'history'} = [
722     $cust_main->payment_history(
723       'line_items' => $conf->exists('selfservice-billing_history-line_items'),
724       'reverse_sort' => 1,
725     )
726   ];
727
728   $return{'money_char'} = $conf->config("money_char") || '$',
729
730   return \%return;
731
732 }
733
734 sub edit_info {
735   my $p = shift;
736   my $session = _cache->get($p->{'session_id'})
737     or return { 'error' => "Can't resume session" }; #better error message
738
739   my $custnum = $session->{'custnum'}
740     or return { 'error' => "no customer record" };
741
742   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
743     or return { 'error' => "unknown custnum $custnum" };
744
745   my $new = new FS::cust_main { $cust_main->hash };
746
747   $new->set( $_ => $p->{$_} )
748     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
749
750   if ( exists($p->{address1}) ) {
751     my $bill_location = FS::cust_location->new({
752         map { $_ => $p->{$_} } @location_editable_fields
753     });
754     # if this is unchanged from before, cust_main::replace will ignore it
755     $new->set('bill_location' => $bill_location);
756   }
757
758   if ( exists($p->{ship_address1}) ) {
759     my $ship_location = FS::cust_location->new({
760         map { $_ => $p->{"ship_$_"} } @location_editable_fields
761     });
762     if ( !grep { length($p->{"ship_$_"}) } @location_editable_fields ) {
763       # Selfservice unfortunately tries to indicate "same as billing 
764       # address" by sending all fields empty.  Did this ever work?
765       $ship_location = $cust_main->bill_location;
766     }
767     $new->set('ship_location' => $ship_location);
768   }
769   # but if it hasn't been passed in at all, leave ship_location alone--
770   # DON'T change it to match bill_location.
771
772   my $conf = new FS::Conf;
773
774   my @invoicing_list;
775   if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
776     #false laziness with httemplate/edit/process/cust_main.cgi
777     @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
778     push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
779   } else {
780     @invoicing_list = $cust_main->invoicing_list;
781   }
782
783   my $error = $new->replace($cust_main, \@invoicing_list);
784   return { 'error' => $error } if $error;
785   #$cust_main = $new;
786   
787   return { 'error' => '' };
788 }
789
790 sub payment_info {
791   my $p = shift;
792   my $session = _cache->get($p->{'session_id'})
793     or return { 'error' => "Can't resume session" }; #better error message
794
795   ##
796   #generic
797   ##
798
799   my $conf = new FS::Conf;
800   use vars qw($payment_info); #cache for performance
801   unless ( $payment_info ) {
802
803     my %states = map { $_->state => 1 }
804                    qsearch('cust_main_county', {
805                      'country' => $conf->config('countrydefault') || 'US'
806                    } );
807
808     my %cust_paybys = map { $_ => 1 }
809                       map { FS::payby->payby2payment($_) }
810                           $conf->config('signup_server-payby');
811
812     my @cust_paybys = keys %cust_paybys;
813
814     $payment_info = {
815
816       #list all counties/states/countries
817       'cust_main_county' => 
818         [ map { $_->hashref } qsearch('cust_main_county', {}) ],
819
820       #shortcut for one-country folks
821       'states' =>
822         [ sort { $a cmp $b } keys %states ],
823
824       'card_types' => card_types(),
825
826       'withcvv'            => $conf->exists('selfservice-require_cvv'), #or enable optional cvv?
827       'require_cvv'        => $conf->exists('selfservice-require_cvv'),
828       'onfile_require_cvv' => $conf->exists('selfservice-onfile_require_cvv'),
829
830       'paytypes' => [ FS::cust_payby::paytypes ],
831
832       'paybys' => [ $conf->config('signup_server-payby') ],
833       'cust_paybys' => \@cust_paybys,
834
835       'stateid_label' => FS::Msgcat::_gettext('stateid'),
836       'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'),
837
838       'show_ss'  => $conf->exists('show_ss'),
839       'show_stateid' => $conf->exists('show_stateid'),
840       'show_paystate' => $conf->exists('show_bankstate'),
841
842       'save_unchecked' => $conf->exists('selfservice-save_unchecked'),
843       'ach_read_only' => $conf->exists('selfservice-ACH_info_readonly'),
844
845     };
846
847   }
848
849   ##
850   #customer-specific
851   ##
852
853   my %return = %$payment_info;
854
855   delete $return{'cust_main_county'} if $p->{'omit_cust_main_county'};
856
857   my $custnum = $session->{'custnum'};
858
859   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
860     or return { 'error' => "unknown custnum $custnum" };
861
862   $return{'hide_payment_fields'} = [
863     map { 
864       my $pg = payment_gateway($cust_main, $_);
865       $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
866     } @{ $return{cust_paybys} }
867   ];
868
869   $return{balance} = $cust_main->balance; #XXX pkg-balances?
870
871   $return{$_} = $cust_main->bill_location->get($_) 
872     for qw(address1 address2 city state zip);
873
874   # look for stored cust_payby info
875   #   only if we've been given a clear payment_payby (to avoid payname conflicts)
876   if ($p->{'payment_payby'} =~ /^(CARD|CHEK)$/) {
877     my @search_payby = ($p->{'payment_payby'} eq 'CARD') ? ('CARD','DCRD') : ('CHEK','DCHK');
878     my ($cust_payby) = $cust_main->cust_payby(@search_payby);
879     if ($cust_payby) {
880       $return{payname} = $cust_payby->payname
881                          || ( $cust_main->first. ' '. $cust_main->get('last') );
882
883       if ( $cust_payby->payby =~ /^(CARD|DCRD)$/ ) {
884         $return{card_type} = cardtype($cust_payby->payinfo);
885         $return{payinfo} = $cust_payby->paymask;
886
887         @return{'month', 'year'} = $cust_payby->paydate_monthyear;
888
889       }
890
891       if ( $cust_payby->payby =~ /^(CHEK|DCHK)$/ ) {
892         my ($payinfo1, $payinfo2) = split '@', $cust_payby->paymask;
893         $return{payinfo1} = $payinfo1;
894         $return{payinfo2} = $payinfo2;
895         $return{paytype}  = $cust_payby->paytype;
896         $return{paystate} = $cust_payby->paystate;
897         $return{payname}  = $cust_payby->payname;       # override 'first/last name' default from above, if any.  Is instution-name here.  (#15819)
898       }
899     }
900   }
901
902   if ( $conf->config('prepayment_discounts-credit_type') ) {
903     #need to eval?
904     $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
905   }
906
907   #doubleclick protection
908   my $_date = time;
909   $return{payunique} = "webui-MyAccount-$_date-$$-". rand() * 2**32; #new
910   $return{paybatch} = $return{payunique};  #back compat
911
912   $return{credit_card_surcharge_percentage} = $conf->config('credit-card-surcharge-percentage', $cust_main->agentnum);
913
914   return { 'error' => '',
915            %return,
916          };
917
918 }
919
920 #some false laziness with httemplate/process/payment.cgi - look there for
921 #ACH and CVV support stuff
922
923 sub validate_payment {
924   my $p = shift;
925
926   my $session = _cache->get($p->{'session_id'})
927     or return { 'error' => "Can't resume session" }; #better error message
928
929   my $custnum = $session->{'custnum'};
930
931   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
932     or return { 'error' => "unknown custnum $custnum" };
933
934   $p->{'amount'} =~ /^\s*(\d+(\.\d{2})?)\s*$/
935     or return { 'error' => gettext('illegal_amount') };
936   my $amount = $1;
937   return { error => 'Amount must be greater than 0' } unless $amount > 0;
938
939   #false laziness w/tr-amount_fee.html, but we don't want selfservice users
940   #changing the hidden form values
941   my $conf = new FS::Conf;
942   my $fee_display = $conf->config('selfservice_process-display') || 'add';
943   my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
944   my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
945   if ( $fee_display eq 'add'
946          and $fee_pkgpart
947          and ! $fee_skip_first || scalar($cust_main->cust_pay)
948      )
949   {
950     my $fee_pkg = qsearchs('part_pkg', { pkgpart=>$fee_pkgpart } );
951     $amount = sprintf('%.2f', $amount + $fee_pkg->option('setup_fee') );
952   }
953
954   $p->{'discount_term'} =~ /^\s*(\d*)\s*$/
955     or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} };
956   my $discount_term = $1;
957
958   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
959     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
960   my $payname = $1;
961
962   $p->{'payunique'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
963     or return { 'error' => gettext('illegal_text'). " payunique: ". $p->{'payunique'} };
964   my $payunique = $1;
965
966   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
967     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
968   my $paybatch = $1;
969
970   $payunique = $paybatch if ! length($payunique) && length($paybatch);
971
972   $p->{'payby'} ||= 'CARD';
973   $p->{'payby'} =~ /^([A-Z]{4})$/
974     or return { 'error' => "illegal_payby " . $p->{'payby'} };
975   my $payby = $1;
976
977   #false laziness w/process/payment.cgi
978   my $payinfo;
979   my $paycvv = '';
980   if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
981   
982     $p->{'payinfo1'} =~ /^([\dx]+)$/
983       or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
984     my $payinfo1 = $1;
985      $p->{'payinfo2'} =~ /^([\dx]+)$/
986       or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
987     my $payinfo2 = $1;
988     $payinfo = $payinfo1. '@'. $payinfo2;
989
990     foreach my $cust_payby ($cust_main->cust_payby('CHEK','DCHK')) {
991       if ( $cust_payby->paymask eq $payinfo ) {
992         $payinfo = $cust_payby->payinfo;
993         last;
994       }
995     }
996    
997   } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) {
998    
999     $payinfo = $p->{'payinfo'};
1000
1001     my $onfile = 0;
1002
1003     #more intelligent matching will be needed here if you change
1004     #card_masking_method and don't remove existing paymasks
1005     foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
1006       if ( $cust_payby->paymask eq $payinfo ) {
1007         $payinfo = $cust_payby->payinfo;
1008         $onfile = 1;
1009         last;
1010       }
1011     }
1012
1013     $payinfo =~ s/\D//g;
1014     $payinfo =~ /^(\d{13,19}|\d{8,9})$/
1015       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
1016     $payinfo = $1;
1017
1018     validate($payinfo)
1019       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
1020     return { 'error' => gettext('unknown_card_type') }
1021       if !$cust_main->tokenized($payinfo) && cardtype($payinfo) eq "Unknown";
1022
1023     if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
1024       if ( cardtype($payinfo) eq 'American Express card' ) {
1025         $p->{'paycvv'} =~ /^\s*(\d{4})\s*$/
1026           or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
1027         $paycvv = $1;
1028       } else {
1029         $p->{'paycvv'} =~ /^\s*(\d{3})\s*$/
1030           or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
1031         $paycvv = $1;
1032       }
1033     } elsif ( $conf->exists('selfservice-onfile_require_cvv') ) {
1034       return { 'error' => 'CVV2 is required' };
1035     } elsif ( !$onfile && $conf->exists('selfservice-require_cvv') ) {
1036       return { 'error' => 'CVV2 is required' };
1037     }
1038   
1039   } else {
1040     die "unknown payby $payby";
1041   }
1042
1043   my %payby2fields = (
1044     'CARD' => [ qw( paystart_month paystart_year payissue payip
1045                     address1 address2 city state zip country    ) ],
1046     'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ],
1047   );
1048
1049   my $card_type = '';
1050   $card_type = cardtype($payinfo) if $payby eq 'CARD';
1051
1052   { 
1053     'cust_main'      => $cust_main, #XXX or just custnum??
1054     'amount'         => sprintf('%.2f', $amount),
1055     'payby'          => $payby,
1056     'payinfo'        => $payinfo,
1057     'paymask'        => $cust_main->mask_payinfo( $payby, $payinfo ),
1058     'card_type'      => $card_type,
1059     'paydate'        => $p->{'year'}. '-'. $p->{'month'}. '-01',
1060     'paydate_pretty' => $p->{'month'}. ' / '. $p->{'year'},
1061     'month'          => $p->{'month'},
1062     'year'           => $p->{'year'},
1063     'payname'        => $payname,
1064     'payunique'      => $payunique,
1065     'paybatch'       => $paybatch,
1066     'paycvv'         => $paycvv,
1067     'payname'        => $payname,
1068     'discount_term'  => $discount_term,
1069     'pkgnum'         => $session->{'pkgnum'},
1070     map { $_ => $p->{$_} } ( @{ $payby2fields{$payby} },
1071                              qw( save auto ),
1072                            )
1073   };
1074
1075 }
1076
1077 sub store_payment {
1078   my $p = shift;
1079
1080   my $validate = validate_payment($p);
1081   return $validate if $validate->{'error'};
1082
1083   my $conf = new FS::Conf;
1084   my $timeout = $conf->config('selfservice-session_timeout') || '1 hour'; #?
1085   _cache->set( 'payment_'.$p->{'session_id'}, $validate, $timeout );
1086
1087   +{ map { $_=>$validate->{$_} }
1088       qw( card_type paymask payname paydate_pretty month year amount
1089           address1 address2 city state zip country
1090         )
1091   };
1092
1093 }
1094
1095 sub process_stored_payment {
1096   my $p = shift;
1097
1098   my $session_id = $p->{'session_id'};
1099
1100   my $payment_info = _cache->get( "payment_$session_id" )
1101     or return { 'error' => "Can't resume session" }; #better error message
1102
1103   do_process_payment($payment_info);
1104
1105 }
1106
1107 sub process_payment {
1108   my $p = shift;
1109
1110   my $payment_info = validate_payment($p);
1111   return $payment_info if $payment_info->{'error'};
1112
1113   do_process_payment($payment_info);
1114
1115 }
1116
1117 sub do_process_payment {
1118   my $validate = shift;
1119
1120   my $cust_main = $validate->{'cust_main'};
1121
1122   my $amount = delete $validate->{'amount'};
1123   my $paynum = '';
1124
1125   my $payby = delete $validate->{'payby'};
1126
1127   if ( $validate->{'save'} ) {
1128
1129     my %saveopt;
1130     foreach my $field ( qw( auto payinfo paymask payname payip ) ) {
1131       $saveopt{$field} = $validate->{$field};
1132     }
1133
1134     if ( $payby eq 'CARD' ) {
1135       my $bill_location = FS::cust_location->new({
1136           map { $_ => $validate->{$_} } 
1137           qw(address1 address2 city state country zip)
1138       });
1139       $saveopt{'bill_location'} = $bill_location;
1140       foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
1141         $saveopt{$field} = $validate->{$field};
1142       }
1143     } else {
1144       # stateid/stateid_state won't be saved, might be broken as of 4.x
1145       foreach my $field ( qw( paytype paystate ) ) {
1146         $saveopt{$field} = $validate->{$field};
1147       }
1148     }
1149
1150     my $error = $cust_main->save_cust_payby(
1151       'payment_payby' => $payby,
1152       %saveopt
1153     );
1154
1155     if ( $error ) {
1156       #no, this causes customers to process their payments again
1157       #return { 'error' => $error };
1158       #XXX just warn verosely for now so i can figure out how these happen in
1159       # the first place, eventually should redirect them to the "change
1160       #address" page but indicate if the payment processed?
1161       delete($validate->{'payinfo'}); #don't want to log this!
1162       warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n".
1163            "PAYBY: $payby\n".
1164            "SAVEOPT: ".Dumper(\%saveopt)."\n".
1165            "CUST_MAIN: ". Dumper($cust_main)."\n".
1166            "PACKET: ". Dumper($validate)."\n";
1167     }
1168   }
1169
1170   my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
1171     'quiet'       => 1,
1172     'manual'      => 1,
1173     'selfservice' => 1,
1174     'paynum_ref'  => \$paynum,
1175     %$validate,
1176   );
1177   return { 'error' => $error } if $error;
1178
1179   #no error, so order the fee package if applicable...
1180   my $conf = new FS::Conf;
1181   my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
1182   my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
1183   
1184   if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) {
1185
1186     my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart };
1187
1188     $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg );
1189     return { 'error' => "payment processed successfully, but error ordering fee: $error" }
1190       if $error;
1191
1192     #and generate an invoice for it now too
1193     $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
1194     return { 'error' => "payment processed and fee ordered successfully, but error billing fee: $error" }
1195       if $error;
1196
1197   }
1198
1199   $cust_main->apply_payments;
1200
1201   my $cust_pay = '';
1202   my $receipt_html = '';
1203   if ($paynum) {
1204       # currently supported for realtime CC only; send receipt data to SS
1205       $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } );
1206       if($cust_pay) {
1207         $receipt_html = qq!
1208 <TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=2>
1209
1210 <TR>
1211   <TD ALIGN="right">Payment#</TD>
1212   <TD BGCOLOR="#FFFFFF"><B>! . $cust_pay->paynum . qq!</B></TD>
1213 </TR>
1214
1215 <TR>
1216   <TD ALIGN="right">Date</TD>
1217
1218   <TD BGCOLOR="#FFFFFF"><B>! . 
1219         time2str("%a&nbsp;%b&nbsp;%o,&nbsp;%Y&nbsp;%r", $cust_pay->_date)
1220                                                             . qq!</B></TD>
1221 </TR>
1222
1223
1224 <TR>
1225   <TD ALIGN="right">Amount</TD>
1226   <TD BGCOLOR="#FFFFFF"><B>! . sprintf('%.2f', $cust_pay->paid) . qq!</B></TD>
1227
1228 </TR>
1229
1230 <TR>
1231   <TD ALIGN="right">Payment method</TD>
1232   <TD BGCOLOR="#FFFFFF"><B>! . $cust_pay->payby_name .' #'. $cust_pay->paymask
1233                                                                 . qq!</B></TD>
1234 </TR>
1235
1236 </TABLE>
1237 !;
1238       }
1239   }
1240
1241   if ( $cust_pay ) {
1242
1243     return {
1244       'error'        => '',
1245       'amount'       => sprintf('%.2f', $cust_pay->paid),
1246       'date'         => $cust_pay->_date,
1247       'date_pretty'  => time2str('%Y-%m-%d', $cust_pay->_date),
1248       'time_pretty'  => time2str('%T', $cust_pay->_date),
1249       'auth_num'     => $cust_pay->auth,
1250       'order_num'    => $cust_pay->order_number,
1251       'receipt_html' => $receipt_html,
1252     };
1253
1254   } else {
1255
1256     return {
1257       'error'        => '',
1258       'receipt_html' => '',
1259     };
1260
1261   }
1262
1263 }
1264
1265 sub realtime_collect {
1266   my $p = shift;
1267
1268   my $session = _cache->get($p->{'session_id'})
1269     or return { 'error' => "Can't resume session" }; #better error message
1270
1271   my $custnum = $session->{'custnum'};
1272
1273   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1274     or return { 'error' => "unknown custnum $custnum" };
1275
1276   my $amount;
1277   if ( $p->{'amount'} ) {
1278     $amount = $p->{'amount'};
1279   }
1280   elsif ( $session->{'pkgnum'} ) {
1281     $amount = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
1282   }
1283   else {
1284     $amount = $cust_main->balance;
1285   }
1286
1287   my $error = $cust_main->realtime_collect(
1288     'method'     => $p->{'method'},
1289     'amount'     => $amount,
1290     'pkgnum'     => $session->{'pkgnum'},
1291     'session_id' => $p->{'session_id'},
1292     'apply'      => 1,
1293     'selfservice'=> 1,
1294   );
1295   return { 'error' => $error } unless ref( $error );
1296
1297   return { 'error' => '', amount => $amount, %$error };
1298 }
1299
1300 sub start_thirdparty {
1301   my $p = shift;
1302   my $session = _cache->get($p->{'session_id'})
1303     or return { 'error' => "Can't resume session" }; #better error message
1304   my $custnum = $session->{'custnum'};
1305   my $cust_main = FS::cust_main->by_key($custnum);
1306   
1307   my $amount = $p->{'amount'}
1308     or return { error => 'no amount' };
1309
1310   my $result = $cust_main->create_payment(
1311     'method'      => $p->{'method'},
1312     'amount'      => $p->{'amount'},
1313     'pkgnum'      => $session->{'pkgnum'},
1314     'session_id'  => $p->{'session_id'},
1315   );
1316   
1317   if ( ref($result) ) { # hashref or error
1318     return $result;
1319   } else {
1320     return { error => $result };
1321   }
1322 }
1323
1324 sub finish_thirdparty {
1325   my $p = shift;
1326   my $session_id = delete $p->{'session_id'};
1327   my $session = _cache->get($session_id)
1328     or return { 'error' => "Can't resume session" };
1329   my $custnum = $session->{'custnum'};
1330   my $cust_main = FS::cust_main->by_key($custnum);
1331
1332   if ( $p->{_cancel} ) {
1333     # customer backed out of making a payment
1334     return $cust_main->cancel_payment( $session_id );
1335   }
1336   my $result = $cust_main->execute_payment( $session_id, %$p );
1337   if ( ref($result) ) {
1338     return $result;
1339   } else {
1340     return { error => $result };
1341   }
1342 }
1343
1344 sub process_payment_order_pkg {
1345   my $p = shift;
1346
1347   my $hr = process_payment($p);
1348   return $hr if $hr->{'error'};
1349
1350   order_pkg($p);
1351 }
1352
1353 sub process_payment_order_renew {
1354   my $p = shift;
1355
1356   my $hr = process_payment($p);
1357   return $hr if $hr->{'error'};
1358
1359   order_renew($p);
1360 }
1361
1362 sub process_prepay {
1363
1364   my $p = shift;
1365
1366   my $session = _cache->get($p->{'session_id'})
1367     or return { 'error' => "Can't resume session" }; #better error message
1368
1369   my %return;
1370
1371   my $custnum = $session->{'custnum'};
1372
1373   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1374     or return { 'error' => "unknown custnum $custnum" };
1375
1376   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = ( 0, 0, 0, 0, 0 );
1377   my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
1378                                            \$amount,
1379                                            \$seconds,
1380                                            \$upbytes,
1381                                            \$downbytes,
1382                                            \$totalbytes,
1383                                          );
1384
1385   return { 'error' => $error } if $error;
1386
1387   return { 'error'     => '',
1388            'amount'    => $amount,
1389            'seconds'   => $seconds,
1390            'duration'  => duration_exact($seconds),
1391            'upbytes'   => $upbytes,
1392            'upload'    => FS::UI::bytecount::bytecount_unexact($upbytes),
1393            'downbytes' => $downbytes,
1394            'download'  => FS::UI::bytecount::bytecount_unexact($downbytes),
1395            'totalbytes'=> $totalbytes,
1396            'totalload' => FS::UI::bytecount::bytecount_unexact($totalbytes),
1397          };
1398
1399 }
1400
1401 sub invoice {
1402   my $p = shift;
1403   my $session = _cache->get($p->{'session_id'})
1404     or return { 'error' => "Can't resume session" }; #better error message
1405
1406   my $custnum = $session->{'custnum'};
1407
1408   my $invnum = $p->{'invnum'};
1409
1410   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
1411                                           'custnum' => $custnum } )
1412     or return { 'error' => "Can't find invnum" };
1413
1414   #my %return;
1415
1416   return { 'error'        => '',
1417            'invnum'       => $invnum,
1418            'invoice_text' => join('', $cust_bill->print_text ),
1419            'invoice_html' => $cust_bill->print_html( { unsquelch_cdr => 1 } ),
1420          };
1421
1422 }
1423
1424 sub invoice_pdf {
1425   my $p = shift;
1426   my $session = _cache->get($p->{'session_id'})
1427     or return { 'error' => "Can't resume session" }; #better error message
1428
1429   my $custnum = $session->{'custnum'};
1430
1431   my $invnum = $p->{'invnum'};
1432
1433   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
1434                                           'custnum' => $custnum } )
1435     or return { 'error' => "Can't find invnum" };
1436
1437   #my %return;
1438
1439   return { 'error'       => '',
1440            'invnum'      => $invnum,
1441            'invoice_pdf' => $cust_bill->print_pdf({
1442                               'unsquelch_cdr' => 1,
1443                               'locale'        => $p->{'locale'},
1444                             }),
1445          };
1446
1447 }
1448
1449 sub legacy_invoice {
1450   my $p = shift;
1451   my $session = _cache->get($p->{'session_id'})
1452     or return { 'error' => "Can't resume session" }; #better error message
1453
1454   my $custnum = $session->{'custnum'};
1455
1456   my $legacyinvnum = $p->{'legacyinvnum'};
1457
1458   my %hash = (
1459     'legacyinvnum' => $legacyinvnum,
1460     'custnum'      => $custnum,
1461   );
1462
1463   my $legacy_cust_bill =
1464          qsearchs('legacy_cust_bill', { %hash, 'locale' => $p->{'locale'} } )
1465       || qsearchs('legacy_cust_bill', \%hash )
1466     or return { 'error' => "Can't find legacyinvnum" };
1467
1468   #my %return;
1469
1470   return { 'error'        => '',
1471            'legacyinvnum' => $legacyinvnum,
1472            'legacyid'     => $legacy_cust_bill->legacyid,
1473            'invoice_html' => $legacy_cust_bill->content_html,
1474          };
1475
1476 }
1477
1478 sub legacy_invoice_pdf {
1479   my $p = shift;
1480   my $session = _cache->get($p->{'session_id'})
1481     or return { 'error' => "Can't resume session" }; #better error message
1482
1483   my $custnum = $session->{'custnum'};
1484
1485   my $legacyinvnum = $p->{'legacyinvnum'};
1486
1487   my $legacy_cust_bill = qsearchs('legacy_cust_bill', {
1488     'legacyinvnum' => $legacyinvnum,
1489     'custnum'      => $custnum,
1490   }) or return { 'error' => "Can't find legacyinvnum" };
1491
1492   #my %return;
1493
1494   return { 'error'        => '',
1495            'legacyinvnum' => $legacyinvnum,
1496            'legacyid'     => $legacy_cust_bill->legacyid,
1497            'invoice_pdf'  => $legacy_cust_bill->content_pdf,
1498          };
1499
1500 }
1501
1502 sub invoice_logo {
1503   my $p = shift;
1504
1505   #sessioning for this?  how do we get the session id to the backend invoice
1506   # template so it can add it to the link, blah
1507
1508   my $agentnum = '';
1509   if ( $p->{'invnum'} ) {
1510     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $p->{'invnum'} } )
1511       or return { 'error' => 'unknown invnum' };
1512     $agentnum = $cust_bill->cust_main->agentnum;
1513   }
1514
1515   my $templatename = $p->{'template'} || $p->{'templatename'};
1516
1517   #false laziness-ish w/view/cust_bill-logo.cgi
1518
1519   my $conf = new FS::Conf;
1520   if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
1521     $templatename = "_$1";
1522   } else {
1523     $templatename = '';
1524   }
1525
1526   my $filename = "logo$templatename.png";
1527
1528   return { 'error'        => '',
1529            'logo'         => $conf->config_binary($filename, $agentnum),
1530            'content_type' => 'image/png', #should allow gif, jpg too
1531          };
1532 }
1533
1534 sub list_invoices {
1535   my $p = shift;
1536   my $session = _cache->get($p->{'session_id'})
1537     or return { 'error' => "Can't resume session" }; #better error message
1538
1539   my $custnum = $session->{'custnum'};
1540
1541   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1542     or return { 'error' => "unknown custnum $custnum" };
1543
1544   my $conf = new FS::Conf;
1545
1546   my @legacy_cust_bill = $cust_main->legacy_cust_bill;
1547
1548   my @cust_bill = grep ! $_->hide, $cust_main->cust_bill;
1549
1550   my $balance = 0;
1551   my $invoices = [
1552     map {
1553       #not super efficient, we also run cust_bill_pay/cust_credited inside owed
1554       my @payments_and_credits = sort {$b->_date <=> $a->_date} ($_->cust_bill_pay,$_->cust_credited);
1555       my $owed = $_->owed;
1556       $balance += $owed;
1557       +{ 'invnum'       => $_->invnum,
1558          '_date'        => $_->_date,
1559          'date'         => time2str("%b %o, %Y", $_->_date),
1560          'date_short'   => time2str("%m-%d-%Y",  $_->_date),
1561          'previous'     => sprintf('%.2f', ($_->previous)[0]),
1562          'charged'      => sprintf('%.2f', $_->charged),
1563          'owed'         => sprintf('%.2f', $owed),
1564          'balance'      => sprintf('%.2f', $balance),
1565          'lastpay'      => @payments_and_credits 
1566                            ? time2str("%b %o, %Y", $payments_and_credits[0]->_date)
1567                            : '',
1568       }
1569     } @cust_bill
1570   ];
1571
1572   return  { 'error'       => '',
1573             'balance'     => $cust_main->balance,
1574             'money_char'  => $conf->config("money_char") || '$',
1575             'invoices'    => $invoices,
1576             'legacy_invoices' => [
1577               map {
1578                     +{ 'legacyinvnum' => $_->legacyinvnum,
1579                        'legacyid'     => $_->legacyid,
1580                        '_date'        => $_->_date,
1581                        'date'         => time2str("%b %o, %Y", $_->_date),
1582                        'date_short'   => time2str("%m-%d-%Y",  $_->_date),
1583                        'charged'      => sprintf('%.2f', $_->charged),
1584                        'has_content'  => (    length($_->content_pdf)
1585                                            || length($_->content_html) ),
1586                      }
1587                   }
1588                   @legacy_cust_bill
1589             ],
1590           };
1591 }
1592
1593 sub list_payments {
1594   my $p = shift;
1595   my $session = _cache->get($p->{'session_id'})
1596     or return { 'error' => "Can't resume session" }; #better error message
1597
1598   my $custnum = $session->{'custnum'};
1599
1600   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1601     or return { 'error' => "unknown custnum $custnum" };
1602
1603   return  { 'error'       => '',
1604             'balance'     => $cust_main->balance,
1605             'money_char'  => FS::Conf->new->config("money_char") || '$',
1606             'payments'    => [ map $_->SSAPI_getinfo, $cust_main->cust_pay ],
1607           };
1608 }
1609
1610 sub payment_receipt {
1611   my $p = shift;
1612   my $session = _cache->get($p->{'session_id'})
1613     or return { 'error' => "Can't resume session" }; #better error message
1614
1615   my $custnum = $session->{'custnum'};
1616
1617   my $cust_pay = qsearchs('cust_pay', { 'custnum' => $custnum,
1618                                         'paynum'  => $p->{'paynum'},
1619                                       }
1620                          )
1621     or return { 'error' => "unknown payment ". $p->{'paynum'} };
1622
1623   return { 
1624            'error' => '',
1625            %{ $cust_pay->SSAPI_getinfo },
1626          };
1627 }
1628
1629 sub list_payby {
1630   my $p = shift;
1631
1632   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1633   return { 'error' => $session } if $context eq 'error';
1634
1635   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1636     or return { 'error' => "unknown custnum $custnum" };
1637
1638   return {
1639     'payby' => [ map {
1640                        my $cust_payby = $_;
1641                        +{
1642                           map { $_ => $cust_payby->$_ }
1643                             qw( custpaybynum weight payby paymask paydate
1644                                 payname paystate paytype
1645                               )
1646                         };
1647                      }
1648                    $cust_main->cust_payby
1649                ],
1650   };
1651 }
1652
1653 sub insert_payby {
1654   my $p = shift;
1655
1656   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1657   return { 'error' => $session } if $context eq 'error';
1658
1659   #XXX payinfo1 + payinfo2 for CHEK?
1660   #or take the opportunity to use separate, more well- named fields?
1661   # my $payinfo;
1662   # $p->{'payinfo1'} =~ /^([\dx]+)$/
1663   #   or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
1664   # my $payinfo1 = $1;
1665   #  $p->{'payinfo2'} =~ /^([\dx\.]+)$/ # . turned on by echeck-country CA ?
1666   #   or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
1667   # my $payinfo2 = $1;
1668   # $payinfo = $payinfo1. '@'. $payinfo2;
1669
1670   my $cust_payby = new FS::cust_payby {
1671     'custnum' => $custnum,
1672     map { $_ => $p->{$_} } qw( weight payby payinfo paycvv paydate payname
1673                                paystate paytype payip 
1674                              ),
1675   };
1676
1677   my $error = $cust_payby->insert;
1678   if ( $error ) {
1679     return { 'error' => $error };
1680   } else {
1681     return { 'custpaybynum' => $cust_payby->custpaybynum };
1682   }
1683   
1684 }
1685
1686 sub update_payby {
1687   my $p = shift;
1688
1689   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1690   return { 'error' => $session } if $context eq 'error';
1691
1692   my $cust_payby = qsearchs('cust_payby', {
1693                               'custnum'      => $custnum,
1694                               'custpaybynum' => $p->{'custpaybynum'},
1695                            })
1696     or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} };
1697
1698   foreach my $field (
1699     qw( weight payby payinfo paycvv paydate payname paystate paytype payip )
1700   ) {
1701     next unless exists($p->{$field});
1702     $cust_payby->set($field,$p->{$field});
1703   }
1704
1705   my $error = $cust_payby->replace;
1706   if ( $error ) {
1707     return { 'error' => $error };
1708   } else {
1709     return { 'custpaybynum' => $cust_payby->custpaybynum };
1710   }
1711   
1712 }
1713
1714 sub verify_payby {
1715   my $p = shift;
1716
1717   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1718   return { 'error' => $session } if $context eq 'error';
1719
1720   my $cust_payby = qsearchs('cust_payby', {
1721                               'custnum'      => $custnum,
1722                               'custpaybynum' => $p->{'custpaybynum'},
1723                            })
1724     or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} };
1725
1726   return { 'error' => $cust_payby->verify };
1727   
1728 }
1729
1730 sub delete_payby {
1731   my $p = shift;
1732
1733   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1734   return { 'error' => $session } if $context eq 'error';
1735
1736   my $cust_payby = qsearchs('cust_payby', {
1737                               'custnum'      => $custnum,
1738                               'custpaybynum' => $p->{'custpaybynum'},
1739                            })
1740     or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} };
1741
1742   return { 'error' => $cust_payby->delete };
1743
1744 }
1745
1746 sub cancel {
1747   my $p = shift;
1748   my $session = _cache->get($p->{'session_id'})
1749     or return { 'error' => "Can't resume session" }; #better error message
1750
1751   my $custnum = $session->{'custnum'};
1752
1753   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1754     or return { 'error' => "unknown custnum $custnum" };
1755
1756   my @errors = $cust_main->cancel( 'quiet'=>1 );
1757
1758   my $error = scalar(@errors) ? join(' / ', @errors) : '';
1759
1760   return { 'error' => $error };
1761
1762 }
1763
1764 sub pkg_info {
1765   my $p = shift;
1766
1767   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1768   return { 'error' => $session } if $context eq 'error';
1769
1770   my $pkg = qsearchs({
1771     'table'     => 'cust_pkg',
1772     'addl_from' => 'LEFT JOIN part_pkg USING ( pkgpart )',
1773     'hashref'   => {
1774                       'custnum' => $custnum,
1775                       'pkgnum'  => $p->{'pkgnum'},
1776                    },
1777   })
1778     or return {'error' => 'unknown pkg num $pkgnum'};
1779
1780   return {
1781         pkg_label => $pkg->pkg,
1782         pkgpart   => $pkg->pkgpart,
1783         classnum  => $pkg->classnum,
1784   };
1785
1786 }
1787
1788 sub list_pkgs {
1789   my $p = shift;
1790
1791   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1792   return { 'error' => $session } if $context eq 'error';
1793
1794   my $search = { 'custnum' => $custnum };
1795   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1796   my $cust_main = qsearchs('cust_main', $search )
1797     or return { 'error' => "unknown custnum $custnum" };
1798
1799   my $conf = new FS::Conf;
1800   my $immutable = $conf->exists('selfservice_immutable-package');
1801   
1802 # the duplication below is necessary:
1803 # 1. to maintain the current buggy behaviour wrt the cust_pkg and part_pkg
1804 # hashes overwriting each other (setup and no_auto fields). Fixing that is a
1805 # non-backwards-compatible change breaking the software of anyone using the API
1806 # instead of the stock selfservice
1807 # 2. to return cancelled packages as well - for wholesale and non-wholesale
1808   if( $conf->exists('selfservice_server-view-wholesale') ) {
1809     return { 'svcnum'   => $session->{'svcnum'},
1810             'custnum'  => $custnum,
1811             'cust_pkg' => [ map {
1812                           { $_->hash,
1813                             immutable => $immutable,
1814                             part_pkg => [ map $_->hashref, $_->part_pkg ],
1815                             part_svc =>
1816                               [ map $_->hashref, $_->available_part_svc ],
1817                             cust_svc => 
1818                               [ map { my $ref = { $_->hash,
1819                                                   label => [ $_->label ],
1820                                                 };
1821                                       $ref->{_password} = $_->svc_x->_password
1822                                         if $context eq 'agent'
1823                                         && $conf->exists('agent-showpasswords')
1824                                         && $_->part_svc->svcdb eq 'svc_acct';
1825                                       $ref;
1826                                     } $_->cust_svc
1827                               ],
1828                           };
1829                         } $cust_main->cust_pkg
1830                   ],
1831     'small_custview' =>
1832       small_custview( $cust_main, $conf->config('countrydefault') ),
1833     'wholesale_view' => 1,
1834     'login_svcpart' => [ $conf->config('selfservice_server-login_svcpart') ],
1835     'date_format' => $conf->config('date_format') || '%m/%d/%Y',
1836     'lnp' => $conf->exists('svc_phone-lnp'),
1837       };
1838   }
1839
1840   { 'svcnum'   => $session->{'svcnum'},
1841     'custnum'  => $custnum,
1842     'cust_pkg' => [ map {
1843                           my $primary_cust_svc = $_->primary_cust_svc;
1844                           +{ $_->hash,
1845                             $_->part_pkg->hash,
1846                             immutable   => $immutable,
1847                             pkg_label   => $_->pkg_locale,
1848                             status      => $_->status,
1849                             statuscolor => $_->statuscolor,
1850                             part_svc =>
1851                               [ map { $_->hashref }
1852                                   grep { $_->selfservice_access ne 'hidden' }
1853                                     $_->available_part_svc
1854                               ],
1855                             cust_svc => 
1856                               [ map { my $ref = { $_->hash,
1857                                                   label => [ $_->label ],
1858                                                 };
1859                                       $ref->{_password} = $_->svc_x->_password
1860                                         if $context eq 'agent'
1861                                         && $conf->exists('agent-showpasswords')
1862                                         && $_->part_svc->svcdb eq 'svc_acct';
1863                                       $ref->{svchash} = { $_->svc_x->hash } if 
1864                                         $_->part_svc->svcdb eq 'svc_phone';
1865                                       $ref->{svchash}->{svcpart} =  $_->part_svc->svcpart
1866                                         if $_->part_svc->svcdb eq 'svc_phone'; # hack
1867                                       $ref;
1868                                     }
1869                                   grep { $_->part_svc->selfservice_access ne 'hidden' }
1870                                     $_->cust_svc
1871                               ],
1872                             primary_cust_svc =>
1873                               $primary_cust_svc
1874                                 ? { $primary_cust_svc->hash,
1875                                     label => [ $primary_cust_svc->label ],
1876                                     finger => $primary_cust_svc->svc_x->finger, #uuh
1877                                     $primary_cust_svc->part_svc->hash,
1878                                   }
1879                                 : {}, #'' ?
1880                           };
1881                         } $cust_main->ncancelled_pkgs
1882                   ],
1883     'small_custview' =>
1884       small_custview( $cust_main, $conf->config('countrydefault') ),
1885     'date_format' => $conf->config('date_format') || '%m/%d/%Y',
1886   };
1887
1888 }
1889
1890 sub list_svcs {
1891   my $p = shift;
1892
1893   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1894   return { 'error' => $session } if $context eq 'error';
1895
1896   my $conf = new FS::Conf;
1897
1898   my $hide_usage = $conf->exists('selfservice_hide-usage') ? 1 : 0;
1899   my $search = { 'custnum' => $custnum };
1900   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1901   my $cust_main = qsearchs('cust_main', $search )
1902     or return { 'error' => "unknown custnum $custnum" };
1903
1904   my $pkgnum = $session->{'pkgnum'} || $p->{'pkgnum'} || '';
1905   if ( ! $pkgnum && $p->{'svcnum'} ) {
1906     my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $p->{'svcnum'} } );
1907     $pkgnum = $cust_svc->pkgnum if $cust_svc;
1908   }
1909
1910   my @cust_svc = ();
1911   my @cust_pkg_usage = ();
1912   foreach my $cust_pkg ( $p->{'ncancelled'} 
1913                          ? $cust_main->ncancelled_pkgs
1914                          : $cust_main->unsuspended_pkgs ) {
1915     next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
1916     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
1917     push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
1918   }
1919
1920   @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
1921   my %usage_pools;
1922   if (!$hide_usage) {
1923     foreach (@cust_pkg_usage) {
1924       my $part = $_->part_pkg_usage;
1925       my $tag = $part->description . ($part->shared ? 1 : 0);
1926       my $row = $usage_pools{$tag} 
1927             ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
1928       $row->[1] += sprintf('%.1f', $_->minutes); # minutes remaining
1929       $row->[2] += $part->minutes; # minutes total
1930     }
1931   } # otherwise just leave them empty
1932
1933   if ( $p->{'svcdb'} ) {
1934     my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
1935                   ? $p->{'svcdb'}
1936                   : ref($p->{'svcdb'}) eq 'ARRAY'
1937                     ? { map { $_=>1 } @{ $p->{'svcdb'} } }
1938                     : { $p->{'svcdb'} => 1 };
1939     @cust_svc = grep $svcdb->{ $_->part_svc->svcdb }, @cust_svc
1940   }
1941
1942   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
1943   #              @svc_x;
1944
1945   my @svcs; # stuff to return to the client
1946   my %bytes_used_total; # for _used columns only
1947   foreach my $cust_svc (@cust_svc) {
1948     my $svc_x = $cust_svc->svc_x;
1949     my($label, $value) = $cust_svc->label;
1950     my $part_svc = $cust_svc->part_svc;
1951     my $svcdb = $part_svc->svcdb;
1952     my $cust_pkg = $cust_svc->cust_pkg;
1953     my $part_pkg = $cust_pkg->part_pkg;
1954
1955     my %hash = (
1956       'svcnum'         => $cust_svc->svcnum,
1957       'display_svcnum' => $cust_svc->display_svcnum,
1958       'svcdb'          => $svcdb,
1959       'label'          => $label,
1960       'value'          => $value,
1961       'pkg_label'      => $cust_pkg->pkg_locale,
1962       'pkg_status'     => $cust_pkg->status,
1963       'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
1964     );
1965
1966     # would it make sense to put this in a svc_* method?
1967
1968     if (!$hide_usage and grep(/^$svcdb$/, qw(svc_acct svc_broadband)) and $part_svc->part_export_usage) {
1969       my $last_bill = $cust_pkg->last_bill || 0;
1970       my $now = time;
1971       my $up_used = $cust_svc->attribute_since_sqlradacct($last_bill,$now,'AcctInputOctets');
1972       my $down_used = $cust_svc->attribute_since_sqlradacct($last_bill,$now,'AcctOutputOctets');
1973       %hash = (
1974         %hash,
1975         'seconds_used'    => $cust_svc->seconds_since_sqlradacct($last_bill,$now),
1976         'upbytes_used'    => display_bytecount($up_used),
1977         'downbytes_used'  => display_bytecount($down_used),
1978         'totalbytes_used' => display_bytecount($up_used + $down_used)
1979       );
1980       $bytes_used_total{'seconds_used'} += $hash{'seconds_used'};
1981       $bytes_used_total{'upbytes_used'} += $up_used;
1982       $bytes_used_total{'downbytes_used'} += $down_used;
1983       $bytes_used_total{'totalbytes_used'} += $up_used + $down_used;
1984     }
1985
1986     if ( $svcdb eq 'svc_acct' ) {
1987       foreach (qw(username email finger seconds)) {
1988         $hash{$_} = $svc_x->$_;
1989       }
1990
1991       if (!$hide_usage) {
1992         %hash = (
1993           %hash,
1994           'upbytes'    => display_bytecount($svc_x->upbytes),
1995           'downbytes'  => display_bytecount($svc_x->downbytes),
1996           'totalbytes' => display_bytecount($svc_x->totalbytes),
1997
1998           'recharge_amount'  => $part_pkg->option('recharge_amount',1),
1999           'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
2000           'recharge_upbytes'    =>
2001             display_bytecount($part_pkg->option('recharge_upbytes',1)),
2002           'recharge_downbytes'  =>
2003             display_bytecount($part_pkg->option('recharge_downbytes',1)),
2004           'recharge_totalbytes' =>
2005             display_bytecount($part_pkg->option('recharge_totalbytes',1)),
2006           # more...
2007         );
2008       }
2009
2010     } elsif ( $svcdb eq 'svc_dsl' ) {
2011
2012       $hash{'phonenum'} = $svc_x->phonenum;
2013       if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
2014         $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
2015         $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
2016           if $svc_x->company;
2017       } else {
2018         $hash{'name'} = $cust_main->name;
2019       }
2020       # no usage to hide here
2021
2022     } elsif ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
2023       if (!$hide_usage) {
2024         # could potentially show lots of things...
2025         $hash{'outbound'} = 1;
2026         $hash{'inbound'}  = 0;
2027         if ( $svcdb eq 'svc_phone' ) {
2028           if ( $part_pkg->plan eq 'voip_inbound' ) {
2029             $hash{'outbound'} = 0;
2030             $hash{'inbound'}  = 1;
2031           } elsif ( $part_pkg->option('selfservice_inbound_format')
2032                 or  $conf->config('selfservice-default_inbound_cdr_format')
2033           ) {
2034             $hash{'inbound'}  = 1;
2035           }
2036         }
2037         foreach (qw(inbound outbound)) {
2038           # hmm...we can't filter by status here, because there might
2039           # not be cdr_terminations at all.  have to go by date.
2040           # find all since the last bill date.
2041           # XXX cdr types?  we are going to need them.
2042           if ( $hash{$_} ) {
2043             my $sum_cdr = $svc_x->sum_cdrs(
2044               'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
2045               'begin'   => ($cust_pkg->last_bill || 0),
2046               'nonzero' => 1,
2047               'disable_charged_party' => 1,
2048             );
2049             $hash{$_} = $sum_cdr->hashref;
2050           }
2051         }
2052       } # not hiding usage
2053     } # svcdb
2054
2055     push @svcs, \%hash;
2056   } # foreach $cust_svc
2057
2058   foreach my $field (keys %bytes_used_total) {
2059     if ($field =~ /bytes/) {
2060       $bytes_used_total{$field} = display_bytecount($bytes_used_total{$field});
2061     }
2062   }
2063
2064   return { 
2065     'svcnum'   => $session->{'svcnum'},
2066     'custnum'  => $custnum,
2067     'date_format' => $conf->config('date_format') || '%m/%d/%Y',
2068     'view_usage_nodomain' => $conf->exists('selfservice-view_usage_nodomain'),
2069     'svcs'     => \@svcs,
2070     'bytes_used_total' => \%bytes_used_total,
2071     'usage_pools' => [
2072       map { $usage_pools{$_} }
2073       sort { $a cmp $b }
2074       keys %usage_pools
2075     ],
2076     'hide_usage' => $hide_usage,
2077   };
2078
2079 }
2080
2081 sub _customer_svc_x {
2082   my($custnum, $svcnum, $table) = (shift, shift, shift);
2083   my $hashref = ref($svcnum) ? $svcnum : { 'svcnum' => $svcnum };
2084
2085   $custnum =~ /^(\d+)$/ or die "illegal custnum";
2086   my $search = " AND custnum = $1";
2087   #$search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
2088
2089   qsearchs( {
2090     'table'     => ($table || 'svc_acct'),
2091     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
2092                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) ',#.
2093                    #'LEFT JOIN cust_main USING ( custnum ) ',
2094     'hashref'   => $hashref,
2095     'extra_sql' => $search, #important
2096   } );
2097
2098 }
2099
2100 sub svc_status_html {
2101   my $p = shift;
2102
2103   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2104   return { 'error' => $session } if $context eq 'error';
2105
2106   #XXX only svc_dsl for now
2107   my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl')
2108     or return { 'error' => "Service not found" };
2109
2110   my $html = $svc_x->getstatus_html;
2111
2112   return { 'html' => $html };
2113
2114 }
2115
2116 sub svc_status_hash {
2117   my $p = shift;
2118
2119   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2120   return { 'error' => $session } if $context eq 'error';
2121
2122   #XXX only svc_acct for now
2123   my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct')
2124     or return { 'error' => "Service not found" };
2125
2126   my ( $html, $hashref ) = $svc_x->export_getstatus;
2127   return $hashref;
2128
2129 }
2130
2131 sub set_svc_status_hash    { _svc_method_X(shift, 'export_setstatus') }
2132 sub set_svc_status_listadd { _svc_method_X(shift, 'export_setstatus_listadd') }
2133 sub set_svc_status_listdel { _svc_method_X(shift, 'export_setstatus_listdel') }
2134 sub set_svc_status_vacationadd { _svc_method_X(shift, 'export_setstatus_vacationadd') }
2135 sub set_svc_status_vacationdel { _svc_method_X(shift, 'export_setstatus_vacationdel') }
2136
2137 sub _svc_method_X {
2138   my( $p, $method ) = @_;
2139
2140   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2141   return { 'error' => $session } if $context eq 'error';
2142
2143   #XXX only svc_acct for now
2144   my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct')
2145     or return { 'error' => "Service not found" };
2146
2147   warn "$method ". join(' / ', map "$_=>".$p->{$_}, keys %$p )
2148     if $DEBUG;
2149   my $error = $svc_x->$method($p); #$p? returns error?
2150   return { 'error' => $error } if $error;
2151
2152   return {}; #? { 'error' => '' }
2153
2154 }
2155
2156 sub acct_forward_info {
2157   my $p = shift;
2158
2159   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2160   return { 'error' => $session } if $context eq 'error';
2161
2162   my $svc_forward = _customer_svc_x( $custnum,
2163                                      { 'srcsvc' => $p->{'svcnum'} },
2164                                      'svc_forward',
2165                                    )
2166     or return { 'error' => '',
2167                 'dst'   => '',
2168               };
2169
2170   return { 'error' => '',
2171            'dst'   => $svc_forward->dst || $svc_forward->dstsvc_acct->email,
2172          };
2173
2174 }
2175
2176 sub process_acct_forward {
2177   my $p = shift;
2178   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2179   return { 'error' => $session } if $context eq 'error';
2180
2181   my $old = _customer_svc_x( $custnum,
2182                              { 'srcsvc' => $p->{'svcnum'} },
2183                              'svc_forward',
2184                            );
2185
2186   if ( $p->{'dst'} eq '' ) {
2187     if ( $old ) {
2188       my $error = $old->delete;
2189       return { 'error' => $error };
2190     }
2191     return { 'error' => '' };
2192   }
2193
2194   my $new = new FS::svc_forward { 'srcsvc' => $p->{'svcnum'},
2195                                   'dst'    => $p->{'dst'},
2196                                 };
2197
2198   my $error;
2199   if ( $old ) {
2200     $new->svcnum($old->svcnum);
2201     my $cust_svc = $old->cust_svc;
2202     $new->svcpart($old->svcpart);
2203     $new->pkgnuym($old->pkgnum);
2204     $error = $new->replace($old);
2205   } else {
2206     my $conf = new FS::Conf;
2207     $new->svcpart($conf->config('selfservice-svc_forward_svcpart'));
2208
2209     my $svc_acct = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct' )
2210       or return { 'error' => 'No service' }; #how would we even get here?
2211
2212     $new->pkgnum( $svc_acct->cust_svc->pkgnum );
2213
2214     $error = $new->insert;
2215   }
2216
2217   return { 'error' => $error };
2218
2219 }
2220
2221 sub list_dsl_devices {
2222   my $p = shift;
2223
2224   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2225   return { 'error' => $session } if $context eq 'error';
2226
2227   my $svc_dsl = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl' )
2228     or return { 'error' => "Service not found" };
2229
2230   return {
2231     'devices' => [ map {
2232                          +{ 'mac_addr' => $_->mac_addr };
2233                        } $svc_dsl->dsl_device
2234                  ],
2235   };
2236
2237 }
2238
2239 sub add_dsl_device {
2240   my $p = shift;
2241
2242   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2243   return { 'error' => $session } if $context eq 'error';
2244
2245   my $svc_dsl = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl' )
2246     or return { 'error' => "Service not found" };
2247
2248   return { 'error' => 'No MAC address supplied' }
2249     unless length($p->{'mac_addr'});
2250
2251   my $dsl_device = new FS::dsl_device { 'svcnum'   => $svc_dsl->svcnum,
2252                                         'mac_addr' => scalar($p->{'mac_addr'}),
2253                                       };
2254   my $error = $dsl_device->insert;
2255   return { 'error' => $error };
2256
2257 }
2258
2259 sub delete_dsl_device {
2260   my $p = shift;
2261
2262   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2263   return { 'error' => $session } if $context eq 'error';
2264
2265   my $svc_dsl = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_dsl' )
2266     or return { 'error' => "Service not found" };
2267
2268   my $dsl_device = qsearchs('dsl_device', { 'svcnum'   => $svc_dsl->svcnum,
2269                                             'mac_addr' => scalar($p->{'mac_addr'}),
2270                                           }
2271                            )
2272     or return { 'error' => 'Unknown MAC address: '. $p->{'mac_addr'} };
2273
2274   my $error = $dsl_device->delete;
2275   return { 'error' => $error };
2276
2277 }
2278
2279 sub port_graph {
2280   my $p = shift;
2281   _usage_details( \&_port_graph, $p,
2282                   'svcdb' => 'svc_port',
2283                 );
2284 }
2285
2286 sub _port_graph {
2287   my($svc_port, $begin, $end) = @_;
2288   my @usage = ();
2289   my $pngOrError = $svc_port->graph_png( start=>$begin, end=> $end );
2290   push @usage, { 'png' => $pngOrError };
2291   (@usage);
2292 }
2293
2294 sub _list_svc_usage {
2295   my($svc_acct, $begin, $end) = @_;
2296   my @usage = ();
2297   foreach my $part_export ( 
2298     map { qsearch ( 'part_export', { 'exporttype' => $_ } ) }
2299     qw( sqlradius sqlradius_withdomain )
2300   ) {
2301     push @usage, @ { $part_export->usage_sessions($begin, $end, $svc_acct) };
2302   }
2303   (@usage);
2304 }
2305
2306 sub list_svc_usage {
2307   _usage_details(\&_list_svc_usage, @_);
2308 }
2309
2310 sub _list_support_usage {
2311   my($svc_acct, $begin, $end) = @_;
2312   my @usage = ();
2313   foreach ( grep { $begin <= $_->_date && $_->_date <= $end }
2314             qsearch('acct_rt_transaction', { 'svcnum' => $svc_acct->svcnum })
2315           ) {
2316     push @usage, { 'seconds'  => $_->seconds,
2317                    'support'  => $_->support,
2318                    '_date'    => $_->_date,
2319                    'id'       => $_->transaction_id,
2320                    'creator'  => $_->creator,
2321                    'subject'  => $_->subject,
2322                    'status'   => $_->status,
2323                    'ticketid' => $_->ticketid,
2324                  };
2325   }
2326   (@usage);
2327 }
2328
2329 sub list_support_usage {
2330   _usage_details(\&_list_support_usage, @_);
2331 }
2332
2333 sub _list_cdr_usage {
2334   # XXX CDR type support...
2335   # XXX any way to do a paged search on this?
2336   # we have to return the results all at once...
2337   my($svc_x, $begin, $end, %opt) = @_;
2338   map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
2339     $svc_x->get_cdrs(
2340       'begin' => $begin,
2341       'end'   => $end,
2342       'disable_charged_party' => 1,
2343       %opt
2344     );
2345 }
2346
2347 sub list_cdr_usage {
2348   my $p = shift;
2349   _usage_details( \&_list_cdr_usage, $p );
2350 }
2351
2352 sub _usage_details {
2353   my($callback, $p, %opt) = @_;
2354   my $conf = FS::Conf->new;
2355
2356   if ( $conf->exists('selfservice_hide-usage') ) {
2357     return { 'error' => 'Viewing usage is not allowed.' };
2358   }
2359
2360   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2361   return { 'error' => $session } if $context eq 'error';
2362
2363   my $search = { 'svcnum' => $p->{'svcnum'} };
2364   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
2365
2366   my $cust_svc = qsearchs( 'cust_svc', $search );
2367   return { 'error' => 'No service selected in list_svc_usage' } 
2368     unless $cust_svc;
2369
2370   my $svc_x = $cust_svc->svc_x;
2371   my $svcdb = $svc_x->table;
2372   my $cust_pkg = $cust_svc->cust_pkg;
2373   my $freq     = $cust_pkg->part_pkg->freq;
2374   my %callback_opt;
2375   my $header = [];
2376   if ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
2377     my $format = '';
2378     if ( $p->{inbound} ) {
2379       $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') 
2380                 || $conf->config('selfservice-default_inbound_cdr_format')
2381                 || 'source_default';
2382       $callback_opt{inbound} = 1;
2383     } else {
2384       $format = $cust_pkg->part_pkg->option('selfservice_format')
2385                 || $conf->config('selfservice-default_cdr_format')
2386                 || 'default';
2387     }
2388
2389     $callback_opt{format} = $format;
2390     $callback_opt{use_clid} = 1;
2391     $header = [ split(',', FS::cdr::invoice_header($format) ) ];
2392   }
2393
2394   my $start    = $cust_pkg->setup;
2395   #my $end      = $cust_pkg->bill; # or time?
2396   my $end      = time;
2397
2398   unless ( $p->{beginning} ) {
2399     $p->{beginning} = $cust_pkg->last_bill;
2400     $p->{ending}    = $end;
2401   }
2402
2403   die "illegal beginning" if $p->{beginning} !~ /^\d*$/;
2404   die "illegal ending"    if $p->{ending}    !~ /^\d*$/;
2405
2406   my (@usage) = &$callback($svc_x, $p->{beginning}, $p->{ending}, 
2407     %callback_opt
2408   );
2409
2410   if ( $conf->exists('selfservice-hide_cdr_price') ) {
2411     # ugly kludge, I know
2412     my ($delete_col) = grep { $header->[$_] eq 'Price' } (0..scalar(@$header));
2413     if (defined $delete_col) {
2414       delete($_->[$delete_col]) foreach ($header, @usage);
2415     }
2416   }
2417
2418   #kinda false laziness with FS::cust_main::bill, but perhaps
2419   #we should really change this bit to DateTime and DateTime::Duration
2420   #
2421   #change this bit to use Date::Manip? CAREFUL with timezones (see
2422   # mailing list archive)
2423   my ($nsec,$nmin,$nhour,$nmday,$nmon,$nyear) =
2424     (localtime($p->{ending}) )[0,1,2,3,4,5];
2425   my ($psec,$pmin,$phour,$pmday,$pmon,$pyear) =
2426     (localtime($p->{beginning}) )[0,1,2,3,4,5];
2427
2428   if ( $freq =~ /^\d+$/ ) {
2429     $nmon += $freq;
2430     until ( $nmon < 12 ) { $nmon -= 12; $nyear++; }
2431     $pmon -= $freq;
2432     until ( $pmon >= 0 ) { $pmon += 12; $pyear--; }
2433   } elsif ( $freq =~ /^(\d+)w$/ ) {
2434     my $weeks = $1;
2435     $nmday += $weeks * 7;
2436     $pmday -= $weeks * 7;
2437   } elsif ( $freq =~ /^(\d+)d$/ ) {
2438     my $days = $1;
2439     $nmday += $days;
2440     $pmday -= $days;
2441   } elsif ( $freq =~ /^(\d+)h$/ ) {
2442     my $hours = $1;
2443     $nhour += $hours;
2444     $phour -= $hours;
2445   } else {
2446     return { 'error' => "unparsable frequency: ". $freq };
2447   }
2448   
2449   my $previous  = timelocal_nocheck($psec,$pmin,$phour,$pmday,$pmon,$pyear);
2450   my $next      = timelocal_nocheck($nsec,$nmin,$nhour,$nmday,$nmon,$nyear);
2451
2452   { 
2453     'error'     => '',
2454     'svcnum'    => $p->{svcnum},
2455     'beginning' => $p->{beginning},
2456     'ending'    => $p->{ending},
2457     'inbound'   => $p->{inbound},
2458     'previous'  => ($previous > $start) ? $previous : $start,
2459     'next'      => ($next < $end) ? $next : $end,
2460     'header'    => $header,
2461     'usage'     => \@usage,
2462   };
2463 }
2464
2465 sub order_pkg {
2466   my $p = shift;
2467
2468   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2469   return { 'error' => $session } if $context eq 'error';
2470
2471   my $search = { 'custnum' => $custnum };
2472   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
2473   my $cust_main = qsearchs('cust_main', $search )
2474     or return { 'error' => "unknown custnum $custnum" };
2475
2476   my $status = $cust_main->status;
2477
2478   my %order_pkg_options = ();
2479   if ( $p->{locationnum} > 0 ) {
2480     $order_pkg_options{locationnum} = delete($p->{locationnum});
2481   } elsif ( $p->{address1} ) {
2482     $order_pkg_options{'cust_location'} = new FS::cust_location {
2483       map { $_ => $p->{$_} }
2484         qw( address1 address2 city county state zip country )
2485     };
2486   }
2487
2488   #false laziness w/ClientAPI/Signup.pm
2489
2490   my $cust_pkg = new FS::cust_pkg ( {
2491     'custnum'  => $custnum,
2492     'pkgpart'  => $p->{'pkgpart'},
2493     'quantity' => $p->{'quantity'} || 1,
2494   } );
2495   my $error = $cust_pkg->check;
2496   return { 'error' => $error } if $error;
2497
2498   my @svc = ();
2499   unless ( $p->{'svcpart'} eq 'none' ) {
2500
2501     my $svcdb;
2502     my $svcpart = '';
2503     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
2504       $svcpart = $1;
2505       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
2506       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
2507       $svcdb = $part_svc->svcdb;
2508     } else {
2509       $svcdb = 'svc_acct';
2510     }
2511     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
2512
2513     my %fields = (
2514       'svc_acct'     => [ qw( username domsvc _password sec_phrase popnum ) ],
2515       'svc_domain'   => [ qw( domain ) ],
2516       'svc_phone'    => [ qw( phonenum pin sip_password phone_name ) ],
2517       'svc_external' => [ qw( id title ) ],
2518       'svc_pbx'      => [ qw( id title ) ],
2519     );
2520   
2521     my $svc_x = "FS::$svcdb"->new( {
2522       'svcpart'   => $svcpart,
2523       map { $_ => $p->{$_} } @{$fields{$svcdb}}
2524     } );
2525     
2526     if ( $svcdb eq 'svc_acct' && exists($p->{"snarf_machine1"}) ) {
2527       my @acct_snarf;
2528       my $snarfnum = 1;
2529       while ( length($p->{"snarf_machine$snarfnum"}) ) {
2530         my $acct_snarf = new FS::acct_snarf ( {
2531           'machine'   => $p->{"snarf_machine$snarfnum"},
2532           'protocol'  => $p->{"snarf_protocol$snarfnum"},
2533           'username'  => $p->{"snarf_username$snarfnum"},
2534           '_password' => $p->{"snarf_password$snarfnum"},
2535         } );
2536         $snarfnum++;
2537         push @acct_snarf, $acct_snarf;
2538       }
2539       $svc_x->child_objects( \@acct_snarf );
2540     }
2541     
2542     my $y = $svc_x->setdefault; # arguably should be in new method
2543     return { 'error' => $y } if $y && !ref($y);
2544   
2545     $error = $svc_x->check;
2546     return { 'error' => $error } if $error;
2547
2548     push @svc, $svc_x;
2549
2550   }
2551
2552   $error = $cust_main->order_pkg(
2553     'cust_pkg' => $cust_pkg,
2554     'svcs'     => \@svc,
2555     'noexport' => 1,
2556     %order_pkg_options,
2557   );
2558   return { 'error' => $error } if $error;
2559
2560   my $conf = new FS::Conf;
2561   if ( $conf->exists('signup_server-realtime') ) {
2562
2563     my $bill_error = _do_bop_realtime( $cust_main, $status, 'collect'=>$p->{run_bill_events} );
2564
2565     if ($bill_error) {
2566       $cust_pkg->cancel('quiet'=>1);
2567       return $bill_error;
2568     } else {
2569       $cust_pkg->reexport;
2570     }
2571
2572   } else {
2573     $cust_pkg->reexport;
2574   }
2575
2576   my $svcnum = $svc[0] ? $svc[0]->svcnum : '';
2577
2578   return { error=>'', pkgnum=>$cust_pkg->pkgnum, svcnum=>$svcnum };
2579
2580 }
2581
2582 sub change_pkg {
2583   my $p = shift;
2584
2585   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2586   return { 'error' => $session } if $context eq 'error';
2587
2588   my $conf = new FS::Conf;
2589   my $immutable = $conf->exists('selfservice_immutable-package');
2590   return { 'error' => "Package modification disabled" } if $immutable;
2591
2592   my $search = { 'custnum' => $custnum };
2593   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
2594   my $cust_main = qsearchs('cust_main', $search )
2595     or return { 'error' => "unknown custnum $custnum" };
2596
2597   my $status = $cust_main->status;
2598   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
2599     or return { 'error' => "unknown package $p->{pkgnum}" };
2600
2601   #if someone does need self-service package change of suspended packages,
2602   # figure out how to be more discriminating
2603   return { error=>"Can't change a suspended package", pkgnum=>$cust_pkg->pkgnum}
2604     if $cust_pkg->status eq 'suspended';
2605
2606   my $err_or_cust_pkg = $cust_pkg->change( 'pkgpart'  => $p->{'pkgpart'},
2607                                            'quantity' => $p->{'quantity'} || 1,
2608                                          );
2609   
2610   my $new_pkg = qsearchs('part_pkg', { 'pkgpart' => $p->{pkgpart} } )
2611     or return { 'error' => "unknown package $p->{pkgpart}" };
2612
2613   return { error=>$err_or_cust_pkg, pkgnum=>$cust_pkg->pkgnum }
2614     unless ref($err_or_cust_pkg);
2615
2616
2617   if ( $conf->exists('signup_server-realtime') ) {
2618
2619     my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_invoice_void'=>1 );
2620
2621     if ($bill_error) {
2622       $err_or_cust_pkg->suspend;
2623       return $bill_error;
2624     } else {
2625       $err_or_cust_pkg->reexport;
2626     }
2627
2628   } else {  
2629     $err_or_cust_pkg->reexport;
2630   }
2631
2632   return { error => '', pkg => $new_pkg->pkg, pkgnum => $err_or_cust_pkg->pkgnum };
2633
2634 }
2635
2636 sub order_recharge {
2637   my $p = shift;
2638
2639   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2640   return { 'error' => $session } if $context eq 'error';
2641
2642   my $search = { 'custnum' => $custnum };
2643   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
2644   my $cust_main = qsearchs('cust_main', $search )
2645     or return { 'error' => "unknown custnum $custnum" };
2646
2647   my $status = $cust_main->status;
2648   my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } )
2649     or return { 'error' => "unknown service " . $p->{'svcnum'} };
2650
2651   my $svc_x = $cust_svc->svc_x;
2652   my $part_pkg = $cust_svc->cust_pkg->part_pkg;
2653
2654   my %vhash =
2655     map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) } 
2656     qw ( recharge_seconds recharge_upbytes recharge_downbytes
2657          recharge_totalbytes );
2658   my $amount = $part_pkg->option('recharge_amount', 1); 
2659   
2660   my ($l, $v, $d) = $cust_svc->label;  # blah
2661   my $pkg = "Recharge $v"; 
2662
2663   my $bill_error = $cust_main->charge($amount, $pkg,
2664      "time: $vhash{seconds}, up: $vhash{upbytes}," . 
2665      "down: $vhash{downbytes}, total: $vhash{totalbytes}",
2666      $part_pkg->taxclass); #meh
2667
2668   my $conf = new FS::Conf;
2669   if ( $conf->exists('signup_server-realtime') && !$bill_error ) {
2670
2671     $bill_error = _do_bop_realtime( $cust_main, $status );
2672
2673     if ($bill_error) {
2674       return $bill_error;
2675     } else {
2676       my $error = $svc_x->recharge (\%vhash);
2677       return { 'error' => $error } if $error;
2678     }
2679
2680   } else {  
2681     my $error = $bill_error;
2682     $error ||= $svc_x->recharge (\%vhash);
2683     return { 'error' => $error } if $error;
2684   }
2685
2686   return { error => '', svc => $cust_svc->part_svc->svc };
2687
2688 }
2689
2690 sub _do_bop_realtime {
2691   my ($cust_main, $status, %opt) = @_;
2692
2693   my $old_balance = $cust_main->balance;
2694
2695   my @cust_bill;
2696   my $bill_error = $cust_main->bill(
2697     'return_bill'   => \@cust_bill,
2698   );
2699
2700   $bill_error ||= $cust_main->apply_payments_and_credits;
2701
2702   $bill_error ||= $cust_main->realtime_collect('selfservice' => 1);
2703
2704   if (    $cust_main->balance > $old_balance
2705        && $cust_main->balance > 0
2706        && ( $cust_main->has_cust_payby_auto || $status eq 'suspended' )
2707      )
2708   {
2709     unless ( $opt{'no_invoice_void'} ) {
2710
2711       #this used to apply a credit, but now we can void invoices...
2712       foreach my $cust_bill (@cust_bill) {
2713         my $voiderror = $cust_bill->void('automatic payment failed');
2714         warn "Error voiding cust bill after decline: $voiderror" if $voiderror;
2715       }
2716
2717     }
2718
2719     return { 'error' => '_decline', 'bill_error' => $bill_error };
2720   }
2721
2722   if ( $opt{'collect'} ) {
2723     my $collect_error = $cust_main->collect();
2724     return { 'error' => '_decline', 'bill_error' => $collect_error }
2725      if $collect_error; #?
2726   }
2727
2728   '';
2729 }
2730
2731 sub renew_info {
2732   my $p = shift;
2733
2734   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2735   return { 'error' => $session } if $context eq 'error';
2736
2737   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
2738     or return { 'error' => "unknown custnum $custnum" };
2739
2740   my @cust_pkg = sort { $a->bill <=> $b->bill }
2741                  grep { $_->part_pkg->freq ne '0' }
2742                  $cust_main->ncancelled_pkgs;
2743
2744   #return { 'error' => 'No active packages to renew.' } unless @cust_pkg;
2745
2746   my $total = $cust_main->balance;
2747
2748   my @array = map {
2749                     my $bill = $_->bill;
2750                     $total += $_->part_pkg->base_recur($_, \$bill);
2751                     my $renew_date = $_->part_pkg->add_freq($_->bill);
2752                     {
2753                       'pkgnum'             => $_->pkgnum,
2754                       'amount'             => sprintf('%.2f', $total),
2755                       'bill_date'          => $_->bill,
2756                       'bill_date_pretty'   => time2str('%x', $_->bill),
2757                       'renew_date'         => $renew_date,
2758                       'renew_date_pretty'  => time2str('%x', $renew_date),
2759                       'expire_date'        => $_->expire,
2760                       'expire_date_pretty' => time2str('%x', $_->expire),
2761                     };
2762                   }
2763                   @cust_pkg;
2764
2765   return { 'dates' => \@array };
2766
2767 }
2768
2769 sub payment_info_renew_info {
2770   my $p = shift;
2771   my $renew_info   = renew_info($p);
2772   my $payment_info = payment_info($p);
2773   return { %$renew_info,
2774            %$payment_info,
2775          };
2776 }
2777
2778 sub order_renew {
2779   my $p = shift;
2780
2781   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2782   return { 'error' => $session } if $context eq 'error';
2783
2784   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
2785     or return { 'error' => "unknown custnum $custnum" };
2786
2787   my $date = $p->{'date'};
2788
2789   my $now = time;
2790
2791   #freeside-daily -n -d $date fs_daily $custnum
2792   $cust_main->bill_and_collect( 'time'         => $date,
2793                                 'invoice_time' => $now,
2794                                 'actual_time'  => $now,
2795                                 'check_freq'   => '1d',
2796                               );
2797
2798   return { 'error' => '' };
2799
2800 }
2801
2802 sub suspend_pkg {
2803   my $p = shift;
2804   my $session = _cache->get($p->{'session_id'})
2805     or return { 'error' => "Can't resume session" }; #better error message
2806
2807   my $custnum = $session->{'custnum'};
2808
2809   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
2810     or return { 'error' => "unknown custnum $custnum" };
2811
2812   my $conf = new FS::Conf;
2813   my $reasonnum = 
2814     $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum)
2815       or return { 'error' => 'Permission denied' };
2816
2817   my $pkgnum = $p->{'pkgnum'};
2818
2819   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
2820                                         'pkgnum'  => $pkgnum,   } )
2821     or return { 'error' => "unknown pkgnum $pkgnum" };
2822
2823   my $error = $cust_pkg->suspend(reason => $reasonnum);
2824   return { 'error' => $error };
2825
2826 }
2827
2828 sub cancel_pkg {
2829   my $p = shift;
2830   my $session = _cache->get($p->{'session_id'})
2831     or return { 'error' => "Can't resume session" }; #better error message
2832
2833   my $custnum = $session->{'custnum'};
2834   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
2835     or return { 'error' => "unknown custnum $custnum" };
2836
2837   my $pkgnum = $p->{'pkgnum'};
2838   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
2839                                         'pkgnum'  => $pkgnum,   } )
2840     or return { 'error' => "unknown pkgnum $pkgnum" };
2841
2842   my $error = $cust_pkg->cancel( 'quiet' => 1,
2843                                  'date'  => $p->{'date'},
2844                                );
2845   return { 'error' => $error };
2846 }
2847
2848 sub provision_phone {
2849   my $p = shift;
2850   my @bulkdid;
2851   @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
2852
2853   #editing an existing phone number
2854   if ( $p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/ ) {
2855       my($context, $session, $custnum) = _custoragent_session_custnum($p);
2856       return { 'error' => $session } if $context eq 'error';
2857     
2858       my $svc_phone = qsearchs('svc_phone', { svcnum => $p->{'svcnum'} });
2859       return { 'error' => 'service not found' } unless $svc_phone;
2860       return { 'error' => 'invalid svcnum' } 
2861         if $svc_phone && $svc_phone->cust_svc->cust_pkg->custnum != $custnum;
2862
2863       $svc_phone->email($p->{'email'}) 
2864         if $svc_phone->email ne $p->{'email'} && $p->{'email'} =~ /^([\w\.\d@]+|)$/;
2865       $svc_phone->forwarddst($p->{'forwarddst'}) 
2866         if $svc_phone->forwarddst ne $p->{'forwarddst'} 
2867             && $p->{'forwarddst'} =~ /^(\d+|)$/;
2868       return { 'error' => $svc_phone->replace };
2869  }
2870
2871   # single DID LNP
2872   unless ( $p->{'lnp'} ) {
2873     $p->{'lnp_desired_due_date'} = parse_datetime($p->{'lnp_desired_due_date'});
2874     $p->{'lnp_status'} = "portingin";
2875     return _provision( 'FS::svc_phone',
2876                   [qw(lnp_desired_due_date lnp_other_provider 
2877                     lnp_other_provider_account phonenum countrycode lnp_status)],
2878                   [qw(phonenum countrycode)],
2879                   $p,
2880                   @_
2881                 );
2882   }
2883
2884   # single DID order (the usual case)
2885   unless (scalar(@bulkdid)) {
2886     return _provision( 'FS::svc_phone',
2887                   [qw(phonenum countrycode)],
2888                   [qw(phonenum countrycode)],
2889                   $p,
2890                   @_
2891                 );
2892   }
2893
2894   # bulk DID order case
2895   my $error;
2896   foreach my $did ( @bulkdid ) {
2897     $did =~ s/[^0-9]//g;
2898     $error = _provision( 'FS::svc_phone',
2899               [qw(phonenum countrycode)],
2900               [qw(phonenum countrycode)],
2901               {
2902                 'pkgnum' => $p->{'pkgnum'},
2903                 'svcpart' => $p->{'svcpart'},
2904                 'phonenum' => $did,
2905                 'countrycode' => $p->{'countrycode'},
2906                 'session_id' => $p->{'session_id'},
2907               }
2908             );
2909     return $error if ($error->{'error'} && length($error->{'error'}) > 1);
2910   }
2911   { 'bulkdid' => [ @bulkdid ], 'svc' => $error->{'svc'} }
2912 }
2913
2914 sub provision_pbx {
2915   my $p = shift;
2916   warn "provision_pbx called\n"
2917     if $DEBUG;
2918
2919   warn "provision_pbx calling _provision\n"
2920     if $DEBUG;
2921   _provision( 'FS::svc_pbx',
2922               [qw(id title max_extensions max_simultaneous ip_addr)],
2923               [qw(id title max_extensions max_simultaneous ip_addr)],
2924               $p,
2925               @_
2926             );
2927 }
2928
2929 sub provision_acct {
2930   my $p = shift;
2931   warn "provision_acct called\n"
2932     if $DEBUG;
2933
2934   return { 'error' => gettext('passwords_dont_match') }
2935     if $p->{'_password'} ne $p->{'_password2'};
2936   return { 'error' => gettext('empty_password') }
2937     unless length($p->{'_password'});
2938  
2939   if ($p->{'domsvc'}) {
2940     my %domains = domain_select_hash FS::svc_acct(map { $_ => $p->{$_} }
2941                                                   qw ( svcpart pkgnum ) );
2942     return { 'error' => gettext('invalid_domain') }
2943       unless ($domains{$p->{'domsvc'}});
2944   }
2945
2946   warn "provision_acct calling _provision\n"
2947     if $DEBUG;
2948   _provision( 'FS::svc_acct',
2949               [qw(username _password domsvc)],
2950               [qw(username _password domsvc)],
2951               $p,
2952               @_
2953             );
2954 }
2955
2956 sub provision_external {
2957   my $p = shift;
2958   #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
2959   _provision( 'FS::svc_external',
2960               [],
2961               [qw(id title)],
2962               $p,
2963               @_
2964             );
2965 }
2966
2967 sub provision_forward {
2968   my $p = shift;
2969   _provision( 'FS::svc_forward',
2970               ['srcsvc','src','dstsvc','dst'],
2971               [],
2972               $p,
2973             );
2974 }
2975
2976 sub _provision {
2977   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
2978   warn "_provision called for $class\n"
2979     if $DEBUG;
2980
2981   my($context, $session, $custnum) = _custoragent_session_custnum($p);
2982   return { 'error' => $session } if $context eq 'error';
2983
2984   my $search = { 'custnum' => $custnum };
2985   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
2986   my $cust_main = qsearchs('cust_main', $search )
2987     or return { 'error' => "unknown custnum $custnum" };
2988
2989   my $pkgnum = $p->{'pkgnum'};
2990
2991   warn "searching for custnum $custnum pkgnum $pkgnum\n"
2992     if $DEBUG;
2993   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
2994                                         'pkgnum'  => $pkgnum,
2995                                                                } )
2996     or return { 'error' => "unknown pkgnum $pkgnum" };
2997
2998   warn "searching for svcpart ". $p->{'svcpart'}. "\n"
2999     if $DEBUG;
3000   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
3001     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
3002
3003   return { error=> 'svcpart '. $p->{'svcpart'}. " is not a $class definition" }
3004     if $class ne 'FS::'. $part_svc->svcdb;
3005
3006   warn "creating $class record\n"
3007     if $DEBUG;
3008   my $svc_x = $class->new( {
3009     'pkgnum'  => $p->{'pkgnum'},
3010     'svcpart' => $p->{'svcpart'},
3011     map { $_ => $p->{$_} } @$fields
3012   } );
3013
3014   my %insert_args = ();
3015   #i shouldn't be a special case here (pass an option or something)
3016   if ( $class eq 'FS::svc_phone'
3017          && grep length($p->{$_}), @location_editable_fields
3018      )
3019   {
3020     $insert_args{'cust_location'} = new FS::cust_location {
3021       map { $_ => $p->{$_} } @location_editable_fields
3022     };
3023   }
3024
3025   warn "inserting $class record\n"
3026     if $DEBUG;
3027   my $error = $svc_x->insert(%insert_args);
3028
3029   unless ( $error ) {
3030     warn "finding inserted record for svcnum ". $svc_x->svcnum. "\n"
3031       if $DEBUG;
3032     $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
3033   }
3034
3035   my $return = { 'svc'   => $part_svc->svc,
3036                  'error' => $error,
3037                  map { $_ => $svc_x->get($_) } @$return_fields
3038                };
3039   warn "_provision returning ". Dumper($return). "\n"
3040     if $DEBUG;
3041   return $return;
3042
3043 }
3044
3045 sub part_svc_info {
3046   my $p = shift;
3047
3048   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3049   return { 'error' => $session } if $context eq 'error';
3050
3051   my $search = { 'custnum' => $custnum };
3052   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
3053   my $cust_main = qsearchs('cust_main', $search )
3054     or return { 'error' => "unknown custnum $custnum" };
3055
3056   my $pkgnum = $p->{'pkgnum'};
3057
3058   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
3059                                         'pkgnum'  => $pkgnum,
3060                                                                } )
3061     or return { 'error' => "unknown pkgnum $pkgnum" };
3062
3063   my $svcpart = $p->{'svcpart'};
3064
3065   my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
3066                                       'svcpart' => $svcpart,           } )
3067     or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
3068   my $part_svc = $pkg_svc->part_svc;
3069
3070   my $conf = new FS::Conf;
3071
3072   my $ret = {
3073     'svc'     => $part_svc->svc,
3074     'svcdb'   => $part_svc->svcdb,
3075     'pkgnum'  => $pkgnum,
3076     'svcpart' => $svcpart,
3077     'custnum' => $custnum,
3078
3079     'security_phrase' => 0, #XXX !
3080     'svc_acct_pop'    => [], #XXX !
3081     'popnum'          => '',
3082     'init_popstate'   => '',
3083     'popac'           => '',
3084     'acstate'         => '',
3085
3086     'small_custview' =>
3087       small_custview( $cust_main, $conf->config('countrydefault') ),
3088
3089   };
3090
3091   if ($p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/ 
3092                      && $ret->{'svcdb'} eq 'svc_phone') {
3093         $ret->{'svcnum'} = $p->{'svcnum'};
3094         my $svc_phone = qsearchs('svc_phone', { svcnum => $p->{'svcnum'} });
3095         if ( $svc_phone && $svc_phone->cust_svc->cust_pkg->custnum == $custnum ) {
3096             $ret->{'email'} = $svc_phone->email;
3097             $ret->{'forwarddst'} = $svc_phone->forwarddst;
3098         }
3099   }
3100
3101   if ($ret->{'svcdb'} eq 'svc_forward') {
3102     $ret->{'forward_emails'} = {$cust_pkg->forward_emails()};
3103   }
3104
3105   $ret;
3106 }
3107
3108 sub unprovision_svc {
3109   my $p = shift;
3110
3111   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3112   return { 'error' => $session } if $context eq 'error';
3113
3114   my $search = { 'custnum' => $custnum };
3115   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
3116   my $cust_main = qsearchs('cust_main', $search )
3117     or return { 'error' => "unknown custnum $custnum" };
3118
3119   my $svcnum = $p->{'svcnum'};
3120
3121   my $cust_svc = qsearchs('cust_svc', { 'svcnum'  => $svcnum, } )
3122     or return { 'error' => "unknown svcnum $svcnum" };
3123
3124   return { 'error' => "Service $svcnum does not belong to customer $custnum" }
3125     unless $cust_svc->cust_pkg->custnum == $custnum;
3126
3127   my $conf = new FS::Conf;
3128
3129   return { 'svc'   => $cust_svc->part_svc->svc,
3130            'error' => $cust_svc->cancel,
3131            'small_custview' =>
3132              small_custview( $cust_main, $conf->config('countrydefault') ),
3133          };
3134
3135 }
3136
3137 sub myaccount_passwd {
3138   my $p = shift;
3139   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3140   return { 'error' => $session } if $context eq 'error';
3141
3142   return { 'error' => "New passwords don't match." }
3143     if $p->{'new_password'} ne $p->{'new_password2'};
3144
3145   return { 'error' => 'Enter new password' }
3146     unless length($p->{'new_password'});
3147
3148   #my $search = { 'custnum' => $custnum };
3149   #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
3150   $custnum =~ /^(\d+)$/ or die "illegal custnum";
3151   my $search = " AND custnum = $1";
3152   $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
3153
3154   my $svc_acct = qsearchs( {
3155     'table'     => 'svc_acct',
3156     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
3157                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
3158                    'LEFT JOIN cust_main USING ( custnum ) ',
3159     'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
3160     'extra_sql' => $search, #important
3161   } )
3162     or return { 'error' => "Service not found" };
3163
3164   my $error = '';
3165
3166   my $conf = new FS::Conf;
3167
3168   return { 'error' => 'Incorrect current password.' }
3169     if  ( exists($p->{'old_password'})
3170           || $conf->exists('selfservice-password_change_oldpass')
3171         )
3172     && ! $svc_acct->check_password($p->{'old_password'});
3173
3174   $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
3175          ||  $svc_acct->set_password($p->{'new_password'})
3176          ||  $svc_acct->replace();
3177
3178   #regular pw change in self-service should change contact pw too, otherwise its
3179   #way too confusing.  hell its confusing they're separate at all, but alas.
3180   #need to support the "ISP provides email that's used as a contact email" case
3181   #as well as we can.
3182   my $contact = FS::contact->by_selfservice_email($svc_acct->email);
3183   if ( $contact && qsearchs('cust_contact', { contactnum=> $contact->contactnum,
3184                                               custnum   => $custnum,
3185                                               selfservice_access => 'Y',
3186                                             }
3187                            )
3188   ) {
3189     #svc_acct was successful but this one returns an error?  "shouldn't happen"
3190     #don't recheck is_password_allowed here; if the svc_acct password was
3191     #legal, that's good enough
3192     $error ||= $contact->change_password($p->{'new_password'});
3193   }
3194
3195   my($label, $value) = $svc_acct->cust_svc->label;
3196
3197   return { 'error' => $error,
3198            'label' => $label,
3199            'value' => $value,
3200          };
3201
3202 }
3203
3204 sub reset_passwd {
3205   my $p = shift;
3206
3207   my $info = skin_info($p);
3208
3209   my $conf = new FS::Conf;
3210   my $verification = $conf->config('selfservice-password_reset_verification')
3211     or return { %$info, 'error' => 'Password resets disabled' };
3212
3213   my $contact = '';
3214   my $svc_acct = '';
3215   my $cust_main = '';
3216   if ( $p->{'email'} ) { #new-style, changes contact and svc_acct
3217   
3218     $contact = FS::contact->by_selfservice_email($p->{'email'});
3219
3220     if ( $contact ) {
3221       my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
3222       $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
3223     }
3224
3225     #also look for an svc_acct, otherwise it would be super confusing
3226
3227     my($username, $domain) = split('@', $p->{'email'});
3228     my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } );
3229     if ( $svc_domain ) {
3230       $svc_acct = qsearchs('svc_acct', { 'username' => $username,
3231                                          'domsvc'   => $svc_domain->svcnum  }
3232                           );
3233       if ( $svc_acct ) {
3234         my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
3235         $cust_main ||= $cust_pkg->cust_main if $cust_pkg;
3236
3237         #precaution: don't change svc_acct password not part of the same
3238         # customer as contact
3239         $svc_acct = '' if ! $cust_pkg
3240                        || $cust_pkg->custnum != $cust_main->custnum;
3241       }
3242       
3243     }
3244
3245     return { %$info, 'error' => 'Email address not found' }
3246       unless $contact || $svc_acct;
3247
3248   } elsif ( $p->{'username'} ) { #old style, looks in svc_acct only
3249
3250     my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
3251       or return { %$info, 'error' => 'Account not found' };
3252
3253     $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
3254                                        'domsvc'   => $svc_domain->svcnum  }
3255                         )
3256       or return { %$info, 'error' => 'Account not found' };
3257
3258     my $cust_pkg = $svc_acct->cust_svc->cust_pkg
3259       or return { %$info, 'error' => 'Account not found' };
3260
3261     $cust_main = $cust_pkg->cust_main;
3262
3263   }
3264
3265   return { %$info, 'error' => 'Multi-customer contacts incompatible with customer-based verification' }
3266     if ! $cust_main && $verification ne 'email';
3267
3268   my %verify = (
3269     'email'   => sub { 1; },
3270     'paymask' => sub { 
3271       my( $p, $cust_main ) = @_;
3272       $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/
3273         && $p->{'paymask'} eq substr($cust_main->paymask, -4)
3274     },
3275     'amount'  => sub {
3276       my( $p, $cust_main ) = @_;
3277       my $cust_pay = qsearchs({
3278         'table' => 'cust_pay',
3279         'hashref' => { 'custnum' => $cust_main->custnum },
3280         'order_by' => 'ORDER BY _date DESC LIMIT 1',
3281       })
3282         or return 0;
3283
3284       $p->{'amount'} == $cust_pay->paid;
3285     },
3286     'zip'     => sub {
3287       my( $p, $cust_main ) = @_;
3288       $p->{'zip'} eq $cust_main->zip
3289         || ( $cust_main->ship_zip && $p->{'zip'} eq $cust_main->ship_zip );
3290     },
3291   );
3292
3293   foreach my $verify ( split(',', $verification) ) {
3294
3295     &{ $verify{$verify} }( $p, $cust_main )
3296       or return { %$info, 'error' => 'Account not found' };
3297
3298   }
3299
3300   #okay, we're verified
3301
3302   if ( $contact ) {
3303
3304     my $error = $contact->send_reset_email(
3305                             'svcnum' => ($svc_acct ? $svc_acct->svcnum : ''),
3306                           );
3307
3308     if ( $error ) {
3309       return { %$info, 'error' => $error }; #????
3310     }
3311
3312   } elsif ( $svc_acct ) {
3313
3314     #create a unique session
3315
3316     my $reset_session = {
3317       'svcnum'   => $svc_acct->svcnum,
3318       'agentnum' => $svc_acct->cust_main->agentnum,
3319     };
3320
3321     my $timeout = '1 hour'; #?
3322
3323     my $reset_session_id;
3324     do {
3325       $reset_session_id = sha512_hex(time(). {}. rand(). $$)
3326     } until ( ! defined _cache->get("reset_passwd_$reset_session_id") );
3327       #just in case
3328
3329     _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
3330
3331     #email it
3332
3333     my $msgnum = $conf->config('selfservice-password_reset_msgnum',
3334                                $cust_main->agentnum);
3335     #die "selfservice-password_reset_msgnum unset" unless $msgnum;
3336     return { %$info, 'error' => "selfservice-password_reset_msgnum unset" }
3337       unless $msgnum;
3338     my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
3339     my $error = $msg_template->send( 'cust_main'     => $cust_main,
3340                                      'object'        => $svc_acct,
3341                                      'substitutions' => {
3342                                        'session_id' => $reset_session_id,
3343                                      }
3344                                    );
3345     if ( $error ) {
3346       return { %$info, 'error' => $error }; #????
3347     }
3348
3349   }
3350
3351   return { %$info, 'error' => '' };
3352 }
3353
3354 sub check_reset_passwd {
3355   my $p = shift;
3356
3357   my $conf = new FS::Conf;
3358   my $verification = $conf->config('selfservice-password_reset_verification')
3359     or return { 'error' => 'Password resets disabled' };
3360
3361   my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
3362     or return { 'error' => "Can't resume session" }; #better error message
3363
3364   if ( $reset_session->{'svcnum'} ) {
3365
3366     my $svcnum = $reset_session->{'svcnum'};
3367
3368     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
3369       or return { 'error' => "Service not found" };
3370
3371     $p->{'agentnum'} = $svc_acct->cust_svc->cust_pkg->cust_main->agentnum;
3372     my $info = skin_info($p);
3373
3374     return { %$info,
3375              'error'      => '',
3376              'session_id' => $p->{'session_id'},
3377              'username'   => $svc_acct->username,
3378            };
3379
3380   } elsif ( $reset_session->{'contactnum'} ) {
3381
3382     my $contactnum = $reset_session->{'contactnum'};
3383
3384     my $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
3385       or return { 'error' => "Contact not found" };
3386
3387     my @contact_email = $contact->contact_email;
3388     return { 'error' => 'No contact email' } unless @contact_email;
3389
3390     my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
3391     $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
3392       if scalar(@cust_contact) == 1;
3393     my $info = skin_info($p);
3394
3395     return { %$info,
3396              'error'      => '',
3397              'session_id' => $p->{'session_id'},
3398              'email'      => $contact_email[0]->email, #the first?
3399            };
3400
3401   } else {
3402
3403     return { 'error' => 'No svcnum or contactnum in session' }; #??
3404
3405   }
3406
3407 }
3408
3409 sub process_reset_passwd {
3410   my $p = shift;
3411
3412   my $conf = new FS::Conf;
3413   my $verification = $conf->config('selfservice-password_reset_verification')
3414     or return { 'error' => 'Password resets disabled' };
3415
3416   my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
3417     or return { 'error' => "Can't resume session" }; #better error message
3418
3419   my $info = '';
3420
3421   my $svc_acct = '';
3422   if ( $reset_session->{'svcnum'} ) {
3423
3424     my $svcnum = $reset_session->{'svcnum'};
3425
3426     $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
3427       or return { 'error' => "Service not found" };
3428
3429     $p->{'agentnum'} ||= $svc_acct->cust_svc->cust_pkg->cust_main->agentnum;
3430     $info ||= skin_info($p);
3431
3432   }
3433
3434   my $contact = '';
3435   if ( $reset_session->{'contactnum'} ) {
3436
3437     my $contactnum = $reset_session->{'contactnum'};
3438
3439     $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
3440       or return { 'error' => "Contact not found" };
3441
3442     my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
3443     $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
3444       if scalar(@cust_contact) == 1;
3445     $info ||= skin_info($p);
3446
3447   }
3448
3449   return { %$info, 'error' => "New passwords don't match." }
3450     if $p->{'new_password'} ne $p->{'new_password2'};
3451
3452   return { %$info, 'error' => 'Enter new password' }
3453     unless length($p->{'new_password'});
3454
3455   if ( $svc_acct ) {
3456
3457     my $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
3458               ||  $svc_acct->set_password($p->{'new_password'})
3459               ||  $svc_acct->replace();
3460
3461     return { %$info, 'error' => $error } if $error;
3462
3463     #my($label, $value) = $svc_acct->cust_svc->label;
3464     #return { 'error' => $error,
3465     #         #'label' => $label,
3466     #         #'value' => $value,
3467     #       };
3468
3469   }
3470
3471   if ( $contact ) {
3472
3473     my $error = $contact->is_password_allowed($p->{'new_password'})
3474             ||  $contact->change_password($p->{'new_password'});
3475
3476     return { %$info, 'error' => $error }; # if $error;
3477
3478   }
3479
3480   #password changed ,so remove session, don't want it reused
3481   _cache->remove($p->{'session_id'});
3482
3483   return { %$info, 'error' => '' };
3484
3485 }
3486
3487 sub validate_passwd {
3488   my $p = shift;
3489
3490   my %result;
3491   %result = ( 'fieldid' => $p->{'fieldid'} )
3492     if $p->{'fieldid'} =~ /^\w+$/;
3493
3494   return { %result, 'password_invalid' => 'Enter new password' }
3495     unless length($p->{'check_password'});
3496
3497   my $svc_acct;
3498   if ($p->{'svcnum'}) {
3499     # false laziness with myaccount_passwd
3500     my($context, $session, $custnum) = _custoragent_session_custnum($p);
3501     return { %result, 'error' => $session } if $context eq 'error';
3502
3503     $custnum =~ /^(\d+)$/ or die "illegal custnum";
3504     my $search = " AND custnum = $1";
3505     $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
3506
3507     $svc_acct = qsearchs( {
3508       'table'     => 'svc_acct',
3509       'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
3510                      'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
3511                      'LEFT JOIN cust_main USING ( custnum ) ',
3512       'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
3513       'extra_sql' => $search, #important
3514     } )
3515       or return { %result, 'error' => "Service not found" };
3516     # end false laziness
3517   }
3518
3519   $svc_acct ||= new FS::svc_acct {};
3520
3521   my $error = $svc_acct->is_password_allowed($p->{'check_password'});
3522   return { %result, 'password_invalid' => $error } if $error;
3523   return { %result, 'password_valid' => 1 };
3524 }
3525
3526 sub list_tickets {
3527   my $p = shift;
3528   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3529   return { 'error' => $session } if $context eq 'error';
3530
3531   my @tickets = ();
3532   if ( $session->{'pkgnum'} ) { 
3533
3534     #tickets for specific service with pkg-balances on
3535     my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
3536                                           'pkgnum'  => $session->{'pkgnum'} })
3537                      or return { 'error' => 'unknown package' };
3538     foreach my $cust_svc ( $cust_pkg->cust_svc ) {
3539       push @tickets, $cust_svc->tickets( $p->{status} );
3540     }
3541
3542   } else {
3543
3544     my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
3545       or return { 'error' => "unknown custnum $custnum" };
3546
3547     @tickets = $cust_main->tickets( $p->{status} );
3548   }
3549
3550   # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html
3551   if ( $FS::TicketSystem::system && FS::TicketSystem->selfservice_priority ) {
3552     my $conf = new FS::Conf;
3553     my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
3554     +{ tickets => [ 
3555          sort { 
3556            (
3557              ($a->{'_selfservice_priority'} eq '') <=>
3558              ($b->{'_selfservice_priority'} eq '')
3559            ) ||
3560            ( $dir * 
3561              ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'})
3562            )
3563          } @tickets
3564        ]
3565     };
3566   } else {
3567     +{ tickets => \@tickets };
3568   }
3569
3570 }
3571
3572 sub create_ticket {
3573   my $p = shift;
3574   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3575   return { 'error' => $session } if $context eq 'error';
3576
3577   warn "$me create_ticket: initializing ticket system\n" if $DEBUG;
3578   FS::TicketSystem->init();
3579
3580   my $conf = new FS::Conf;
3581   my $queue = $p->{'queue'}
3582               || $conf->config('ticket_system-selfservice_queueid')
3583               || $conf->config('ticket_system-default_queueid');
3584
3585   warn "$me create_ticket: creating ticket\n" if $DEBUG;
3586   my $err_or_ticket = FS::TicketSystem->create_ticket(
3587     '', #create RT session based on FS CurrentUser (fs_selfservice)
3588     'queue'   => $queue,
3589     'custnum' => $custnum,
3590     'svcnum'  => $session->{'svcnum'},
3591     map { $_ => $p->{$_} } qw( requestor cc subject message mime_type )
3592   );
3593
3594   if ( ref($err_or_ticket) ) {
3595     warn "$me create_ticket: successful: ". $err_or_ticket->id. "\n"
3596       if $DEBUG;
3597     return { 'error'     => '',
3598              'ticket_id' => $err_or_ticket->id,
3599            };
3600   } else {
3601     warn "$me create_ticket: unsuccessful: $err_or_ticket\n"
3602       if $DEBUG;
3603     return { 'error' => $err_or_ticket };
3604   }
3605
3606
3607 }
3608
3609 sub did_report {
3610   my $p = shift;
3611   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3612   return { 'error' => $session } if $context eq 'error';
3613  
3614   return { error => 'requested format not implemented' } 
3615     unless ($p->{'format'} eq 'csv' || $p->{'format'} eq 'xls');
3616
3617   my $conf = new FS::Conf;
3618   my $age_threshold = 0;
3619   $age_threshold = time() - $conf->config('selfservice-recent-did-age')
3620     if ($p->{'recentonly'} && $conf->exists('selfservice-recent-did-age'));
3621
3622   my $search = { 'custnum' => $custnum };
3623   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
3624   my $cust_main = qsearchs('cust_main', $search )
3625     or return { 'error' => "unknown custnum $custnum" };
3626
3627 # does it make more sense to just run one sql query for this instead of all the
3628 # insanity below? would increase performance greately for large data sets?
3629   my @svc_phone = ();
3630   foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
3631         my @part_svc = $cust_pkg->part_svc;
3632         foreach my $part_svc ( @part_svc ) {
3633             if($part_svc->svcdb eq 'svc_phone'){
3634                 my @cust_pkg_svc = @{$part_svc->cust_pkg_svc};
3635                 foreach my $cust_pkg_svc ( @cust_pkg_svc ) {
3636                     push @svc_phone, $cust_pkg_svc->svc_x
3637                         if $cust_pkg_svc->date_inserted >= $age_threshold;
3638                 }
3639             }
3640         }
3641   }
3642
3643   my $csv;
3644   my $xls;
3645   my($xls_r,$xls_c) = (0,0);
3646   my $xls_workbook;
3647   my $content = '';
3648   my @fields = qw( countrycode phonenum pin sip_password phone_name );
3649   if($p->{'format'} eq 'csv') {
3650     $csv = new Text::CSV_XS { 'always_quote' => 1,
3651                                  'eol'          => "\n",
3652                                 };
3653     return { 'error' => 'Unable to create CSV' } unless $csv->combine(@fields);
3654     $content .= $csv->string;
3655   }
3656   elsif($p->{'format'} eq 'xls') {
3657     my $XLS1 = new IO::Scalar \$content;
3658     $xls_workbook = Spreadsheet::WriteExcel->new($XLS1) 
3659         or return { 'error' => "Error opening .xls file: $!" };
3660     $xls = $xls_workbook->add_worksheet('DIDs');
3661     foreach ( @fields ) {
3662         $xls->write(0,$xls_c++,$_);
3663     }
3664     $xls_r++;
3665   }
3666
3667   foreach my $svc_phone ( @svc_phone ) {
3668     my @cols = map { $svc_phone->$_ } @fields;
3669     if($p->{'format'} eq 'csv') {
3670         return { 'error' => 'Unable to create CSV' } 
3671             unless $csv->combine(@cols);
3672         $content .= $csv->string;
3673     }
3674     elsif($p->{'format'} eq 'xls') {
3675         $xls_c = 0;
3676         foreach ( @cols ) {
3677             $xls->write($xls_r,$xls_c++,$_);
3678         }
3679         $xls_r++;
3680     }
3681   }
3682
3683   $xls_workbook->close() if $p->{'format'} eq 'xls';
3684   
3685   { content => $content, format => $p->{'format'}, };
3686 }
3687
3688 sub get_ticket {
3689   my $p = shift;
3690   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3691   return { 'error' => $session } if $context eq 'error';
3692
3693 #  warn "$me get_ticket: initializing ticket system\n" if $DEBUG;
3694 #  FS::TicketSystem->init();
3695 #  return { 'error' => 'get_ticket configuration error' }
3696 #    if $FS::TicketSystem::system ne 'RT_Internal';
3697
3698   # check existence and ownership as part of this
3699   warn "$me get_ticket: fetching ticket\n" if $DEBUG;
3700   my $rt_session = FS::TicketSystem->session('');
3701   my $Ticket = FS::TicketSystem->get_ticket_object(
3702     $rt_session, 
3703     ticket_id => $p->{'ticket_id'},
3704     custnum => $custnum
3705   );
3706   return { 'error' => 'ticket not found' } if !$Ticket;
3707
3708   if ( length( $p->{'subject'} || '' ) ) {
3709     # subject change
3710     if ( $p->{'subject'} ne $Ticket->Subject ) {
3711       my ($val, $msg) = $Ticket->SetSubject($p->{'subject'});
3712       return { 'error' => "unable to set subject: $msg" } if !$val;
3713     }
3714   }
3715
3716   if(length($p->{'reply'})) {
3717     my @err_or_res = FS::TicketSystem->correspond_ticket(
3718       $rt_session,
3719       'ticket_id' => $p->{'ticket_id'},
3720       'content' => $p->{'reply'},
3721     );
3722
3723     return { 'error' => 'unable to reply to ticket' } 
3724     unless ( $err_or_res[0] != 0 && defined $err_or_res[2] );
3725   }
3726
3727   warn "$me get_ticket: getting ticket history\n" if $DEBUG;
3728   my $err_or_ticket = FS::TicketSystem->get_ticket(
3729     $rt_session,
3730     'ticket_id' => $p->{'ticket_id'},
3731   );
3732
3733   if ( !ref($err_or_ticket) ) { # there is no way this should ever happen
3734     warn "$me get_ticket: unsuccessful: $err_or_ticket\n"
3735       if $DEBUG;
3736     return { 'error' => $err_or_ticket };
3737   }
3738
3739   my @custs = @{$err_or_ticket->{'custs'}};
3740   my @txns = @{$err_or_ticket->{'txns'}};
3741   my @filtered_txns;
3742
3743   # superseded by check in get_ticket_object
3744   #return { 'error' => 'invalid ticket requested' } 
3745   #unless grep($_ eq $custnum, @custs);
3746
3747   foreach my $txn ( @txns ) {
3748     push @filtered_txns, $txn 
3749     if ($txn->{'type'} eq 'EmailRecord' 
3750       || $txn->{'type'} eq 'Correspond'
3751       || $txn->{'type'} eq 'Create');
3752   }
3753
3754   warn "$me get_ticket: successful: \n"
3755   if $DEBUG;
3756   return { 'error'     => '',
3757     'transactions' => \@filtered_txns,
3758     'ticket_fields' => $err_or_ticket->{'fields'},
3759     'ticket_id' => $p->{'ticket_id'},
3760   };
3761 }
3762
3763 sub adjust_ticket_priority {
3764   my $p = shift;
3765   my($context, $session, $custnum) = _custoragent_session_custnum($p);
3766   return { 'error' => $session } if $context eq 'error';
3767
3768 #  warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG;
3769 #  FS::TicketSystem->init;
3770   my $ss_priority = FS::TicketSystem->selfservice_priority;
3771
3772   return { 'error' => 'adjust_ticket_priority configuration error' }
3773     if $FS::TicketSystem::system ne 'RT_Internal'
3774       or !$ss_priority;
3775
3776   my $values = $p->{'values'}; #hashref, id => priority value
3777   my %ticket_error;
3778
3779   foreach my $id (keys %$values) {
3780     warn "$me adjust_ticket_priority: fetching ticket $id\n" if $DEBUG;
3781     my $Ticket = FS::TicketSystem->get_ticket_object('',
3782       'ticket_id' => $id,
3783       'custnum'   => $custnum,
3784     );
3785     if ( !$Ticket ) {
3786       $ticket_error{$id} = 'ticket not found';
3787       next;
3788     }
3789     
3790   # RT API stuff--would we gain anything by wrapping this in FS::TicketSystem?
3791   # We're not going to implement it for RT_External.
3792     my $old_value = $Ticket->FirstCustomFieldValue($ss_priority);
3793     my $new_value = $values->{$id};
3794     next if $old_value eq $new_value;
3795
3796     warn "$me adjust_ticket_priority: updating ticket $id\n" if $DEBUG;
3797
3798     # AddCustomFieldValue works fine (replacing any existing value) if it's 
3799     # a single-valued custom field, which it should be.  If it's not, you're 
3800     # doing something wrong.
3801     my ($val, $msg);
3802     if ( length($new_value) ) {
3803       ($val, $msg) = $Ticket->AddCustomFieldValue( 
3804         Field => $ss_priority,
3805         Value => $new_value,
3806       );
3807     }
3808     else {
3809       ($val, $msg) = $Ticket->DeleteCustomFieldValue(
3810         Field => $ss_priority,
3811         Value => $old_value,
3812       );
3813     }
3814
3815     $ticket_error{$id} = $msg if !$val;
3816     warn "$me adjust_ticket_priority: $id: $msg\n" if $DEBUG and !$val;
3817   }
3818   return { 'error' => '',
3819            'ticket_error' => \%ticket_error,
3820            %{ customer_info($p) } # send updated customer info back
3821          }
3822 }
3823
3824 #--
3825
3826 sub _custoragent_session_custnum {
3827   my $p = shift;
3828
3829   my($context, $session, $custnum);
3830   if ( $p->{'session_id'} ) {
3831
3832     $context = 'customer';
3833     $session = _cache->get($p->{'session_id'})
3834       or return ( 'error' => "Can't resume session" ); #better error message
3835     $custnum = $session->{'custnum'};
3836
3837   } elsif ( $p->{'agent_session_id'} ) {
3838
3839     $context = 'agent';
3840     my $agent_cache = new FS::ClientAPI_SessionCache( {
3841       'namespace' => 'FS::ClientAPI::Agent',
3842     } );
3843     $session = $agent_cache->get($p->{'agent_session_id'})
3844       or return ( 'error' => "Can't resume session" ); #better error message
3845     $custnum = $p->{'custnum'};
3846
3847   } else {
3848     $context = 'error';
3849     return ( 'error' => "Can't resume session" ); #better error message
3850   }
3851
3852   ($context, $session, $custnum);
3853
3854 }
3855
3856 1;
3857