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