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