fix old form of order_pkgs in favor of seconds_ref as an option, RT#10122
[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 Data::Dumper;
8 use Digest::MD5 qw(md5_hex);
9 use Date::Format;
10 use Business::CreditCard;
11 use Time::Duration;
12 use Time::Local qw(timelocal_nocheck);
13 use FS::UI::Web::small_custview qw(small_custview); #less doh
14 use FS::UI::Web;
15 use FS::UI::bytecount qw( display_bytecount );
16 use FS::Conf;
17 #use FS::UID qw(dbh);
18 use FS::Record qw(qsearch qsearchs dbh);
19 use FS::Msgcat qw(gettext);
20 use FS::Misc qw(card_types);
21 use FS::Misc::DateTime qw(parse_datetime);
22 use FS::ClientAPI_SessionCache;
23 use FS::svc_acct;
24 use FS::svc_domain;
25 use FS::svc_phone;
26 use FS::svc_external;
27 use FS::part_svc;
28 use FS::cust_main;
29 use FS::cust_bill;
30 use FS::cust_main_county;
31 use FS::cust_pkg;
32 use FS::payby;
33 use FS::acct_rt_transaction;
34 use HTML::Entities;
35 use FS::TicketSystem;
36 use Text::CSV_XS;
37 use IO::Scalar;
38 use Spreadsheet::WriteExcel;
39
40 $DEBUG = 0;
41 $me = '[FS::ClientAPI::MyAccount]';
42
43 use vars qw( @cust_main_editable_fields );
44 @cust_main_editable_fields = qw(
45   first last company address1 address2 city
46     county state zip country daytime night fax
47   ship_first ship_last ship_company ship_address1 ship_address2 ship_city
48     ship_state ship_zip ship_country ship_daytime ship_night ship_fax
49   payby payinfo payname paystart_month paystart_year payissue payip
50   ss paytype paystate stateid stateid_state
51 );
52
53 sub _cache {
54   $cache ||= new FS::ClientAPI_SessionCache( {
55                'namespace' => 'FS::ClientAPI::MyAccount',
56              } );
57 }
58
59 sub skin_info {
60   my $p = shift;
61
62   my($context, $session, $custnum) = _custoragent_session_custnum($p);
63   #return { 'error' => $session } if $context eq 'error';
64
65   my $agentnum = '';
66   if ( $context eq 'customer' ) {
67
68     my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
69       or die dbh->errstr;
70
71     $sth->execute($custnum) or die $sth->errstr;
72
73     $agentnum = $sth->fetchrow_arrayref->[0]
74       or die "no agentnum for custnum $custnum";
75
76   #} elsif ( $context eq 'agent' ) {
77   } elsif ( $p->{'agentnum'} =~ /^(\d+)$/ ) {
78     $agentnum = $1;
79   }
80
81   my $conf = new FS::Conf;
82
83   #false laziness w/Signup.pm
84
85   my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum");
86
87   if ( $skin_info_cache_agent ) {
88
89     warn "$me loading cached skin info for agentnum $agentnum\n"
90       if $DEBUG > 1;
91
92   } else {
93
94     warn "$me populating skin info cache for agentnum $agentnum\n"
95       if $DEBUG > 1;
96
97     $skin_info_cache_agent = {
98       'agentnum' => $agentnum,
99       ( map { $_ => scalar( $conf->config($_, $agentnum) ) }
100         qw( company_name ) ),
101       ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) }
102         qw( body_bgcolor box_bgcolor
103             text_color link_color vlink_color hlink_color alink_color
104             font title_color title_align title_size menu_bgcolor menu_fontsize
105           )
106       ),
107       ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
108         qw( menu_skipblanks menu_skipheadings menu_nounderline )
109       ),
110       ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) }
111         qw( title_left_image title_right_image
112             menu_top_image menu_body_image menu_bottom_image
113           )
114       ),
115       'logo' => scalar($conf->config_binary('logo.png', $agentnum )),
116       ( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
117         qw( head body_header body_footer company_address ) ),
118     };
119
120     _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
121
122   }
123
124   #{ %$skin_info_cache_agent };
125   $skin_info_cache_agent;
126
127 }
128
129 sub login_info {
130   my $p = shift;
131
132   my $conf = new FS::Conf;
133
134   my %info = (
135     %{ skin_info($p) },
136     'phone_login'  => $conf->exists('selfservice_server-phone_login'),
137     'single_domain'=> scalar($conf->config('selfservice_server-single_domain')),
138   );
139
140   return \%info;
141
142 }
143
144 #false laziness w/FS::ClientAPI::passwd::passwd
145 sub login {
146   my $p = shift;
147
148   my $conf = new FS::Conf;
149
150   my $svc_x = '';
151   if ( $p->{'domain'} eq 'svc_phone'
152        && $conf->exists('selfservice_server-phone_login') ) { 
153
154     my $svc_phone = qsearchs( 'svc_phone', { 'phonenum' => $p->{'username'} } );
155     return { error => 'Number not found.' } unless $svc_phone;
156
157     #XXX?
158     #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
159     #return { error => 'Only primary user may log in.' } 
160     #  if $conf->exists('selfservice_server-primary_only')
161     #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
162
163     return { error => 'Incorrect PIN.' }
164       unless $svc_phone->check_pin($p->{'password'});
165
166     $svc_x = $svc_phone;
167
168   } else {
169
170     my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
171       or return { error => 'Domain '. $p->{'domain'}. ' not found' };
172
173     my $svc_acct = qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
174                                            'domsvc'    => $svc_domain->svcnum, }
175                            );
176     return { error => 'User not found.' } unless $svc_acct;
177
178     if($conf->exists('selfservice_server-login_svcpart')) {
179         my @svcpart = $conf->config('selfservice_server-login_svcpart');
180         my $svcpart = $svc_acct->cust_svc->svcpart;
181         return { error => 'Invalid user.' } 
182             unless grep($_ eq $svcpart, @svcpart);
183     }
184
185     return { error => 'Incorrect password.' }
186       unless $svc_acct->check_password($p->{'password'});
187
188     $svc_x = $svc_acct;
189
190   }
191
192   my $session = {
193     'svcnum' => $svc_x->svcnum,
194   };
195
196   my $cust_svc = $svc_x->cust_svc;
197   my $cust_pkg = $cust_svc->cust_pkg;
198   if ( $cust_pkg ) {
199     my $cust_main = $cust_pkg->cust_main;
200     $session->{'custnum'} = $cust_main->custnum;
201     if ( $conf->exists('pkg-balances') ) {
202       my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
203                           $cust_main->ncancelled_pkgs;
204       $session->{'pkgnum'} = $cust_pkg->pkgnum
205         if scalar(@cust_pkg) > 1;
206     }
207   }
208
209   #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
210   #return { error => 'Only primary user may log in.' } 
211   #  if $conf->exists('selfservice_server-primary_only')
212   #    && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
213   my $part_pkg = $cust_pkg->part_pkg;
214   return { error => 'Only primary user may log in.' }
215     if $conf->exists('selfservice_server-primary_only')
216        && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
217
218   my $session_id;
219   do {
220     $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
221   } until ( ! defined _cache->get($session_id) ); #just in case
222
223   my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
224   _cache->set( $session_id, $session, $timeout );
225
226   return { 'error'      => '',
227            'session_id' => $session_id,
228          };
229 }
230
231 sub logout {
232   my $p = shift;
233   if ( $p->{'session_id'} ) {
234     _cache->remove($p->{'session_id'});
235     return { %{ skin_info($p) }, 'error' => '' };
236   } else {
237     return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message
238   }
239 }
240
241 sub payment_gateway {
242   # internal use only
243   # takes a cust_main and a cust_payby entry, returns the payment_gateway
244   my $conf = new FS::Conf;
245   my $cust_main = shift;
246   my $cust_payby = shift;
247   my $gatewaynum = $conf->config('selfservice-payment_gateway');
248   if ( $gatewaynum ) {
249     my $pg = qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
250     die "configured gatewaynum $gatewaynum not found!" if !$pg;
251     return $pg;
252   }
253   else {
254     return '' if ! FS::payby->realtime($cust_payby);
255     my $pg = $cust_main->agent->payment_gateway(
256       'method'  => FS::payby->payby2bop($cust_payby),
257       'nofatal' => 1
258     );
259     return $pg;
260   }
261 }
262
263 sub access_info {
264   my $p = shift;
265
266   my $conf = new FS::Conf;
267
268   my $info = skin_info($p);
269
270   use vars qw( $cust_paybys ); #cache for performance
271   unless ( $cust_paybys ) {
272
273     my %cust_paybys = map { $_ => 1 }
274                       map { FS::payby->payby2payment($_) }
275                           $conf->config('signup_server-payby');
276
277     $cust_paybys = [ keys %cust_paybys ];
278
279   }
280   $info->{'cust_paybys'} = $cust_paybys;
281
282   my($context, $session, $custnum) = _custoragent_session_custnum($p);
283   return { 'error' => $session } if $context eq 'error';
284
285   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
286     or return { 'error' => "unknown custnum $custnum" };
287
288   $info->{'hide_payment_fields'} = [ 
289     map { 
290       my $pg = payment_gateway($cust_main, $_);
291       $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
292     } @{ $info->{cust_paybys} }
293   ];
294
295   $info->{'self_suspend_reason'} = 
296       $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum);
297
298   return { %$info,
299            'custnum'       => $custnum,
300            'access_pkgnum' => $session->{'pkgnum'},
301            'access_svcnum' => $session->{'svcnum'},
302          };
303 }
304
305 sub customer_info {
306   my $p = shift;
307
308   my($context, $session, $custnum) = _custoragent_session_custnum($p);
309   return { 'error' => $session } if $context eq 'error';
310
311   my %return;
312
313   my $conf = new FS::Conf;
314   if ($conf->exists('cust_main-require_address2')) {
315     $return{'require_address2'} = '1';
316   }else{
317     $return{'require_address2'} = '';
318   }
319   
320   if ( $custnum ) { #customer record
321
322     my $search = { 'custnum' => $custnum };
323     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
324     my $cust_main = qsearchs('cust_main', $search )
325       or return { 'error' => "unknown custnum $custnum" };
326
327     if ( $session->{'pkgnum'} ) { 
328       $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
329     } else {
330       $return{balance} = $cust_main->balance;
331     }
332
333     $return{tickets} = [ ($cust_main->tickets) ];
334
335     unless ( $session->{'pkgnum'} ) {
336       my @open = map {
337                        {
338                          invnum => $_->invnum,
339                          date   => time2str("%b %o, %Y", $_->_date),
340                          owed   => $_->owed,
341                        };
342                      } $cust_main->open_cust_bill;
343       $return{open_invoices} = \@open;
344     }
345
346     $return{small_custview} =
347       small_custview( $cust_main,
348                       scalar($conf->config('countrydefault')),
349                       ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance
350                     );
351
352     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
353
354     for (@cust_main_editable_fields) {
355       $return{$_} = $cust_main->get($_);
356     }
357
358     if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
359       $return{payinfo} = $cust_main->paymask;
360       @return{'month', 'year'} = $cust_main->paydate_monthyear;
361     }
362
363     $return{'invoicing_list'} =
364       join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
365     $return{'postal_invoicing'} =
366       0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
367
368     if (scalar($conf->config('support_packages'))) {
369       my @support_services = ();
370       foreach ($cust_main->support_services) {
371         my $seconds = $_->svc_x->seconds;
372         my $time_remaining = (($seconds < 0) ? '-' : '' ).
373                              int(abs($seconds)/3600)."h".
374                              sprintf("%02d",(abs($seconds)%3600)/60)."m";
375         my $cust_pkg = $_->cust_pkg;
376         my $pkgnum = '';
377         my $pkg = '';
378         $pkgnum = $cust_pkg->pkgnum if $cust_pkg;
379         $pkg = $cust_pkg->part_pkg->pkg if $cust_pkg;
380         push @support_services, { svcnum => $_->svcnum,
381                                   time => $time_remaining,
382                                   pkgnum => $pkgnum,
383                                   pkg => $pkg,
384                                 };
385       }
386       $return{support_services} = \@support_services;
387     }
388
389     if ( $conf->config('prepayment_discounts-credit_type') ) {
390       #need to eval?
391       $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
392     }
393
394   } elsif ( $session->{'svcnum'} ) { #no customer record
395
396     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
397       or die "unknown svcnum";
398     $return{name} = $svc_acct->email;
399
400   } else {
401
402     return { 'error' => 'Expired session' }; #XXX redirect to login w/this err!
403
404   }
405
406   return { 'error'          => '',
407            'custnum'        => $custnum,
408            %return,
409          };
410
411 }
412
413 sub edit_info {
414   my $p = shift;
415   my $session = _cache->get($p->{'session_id'})
416     or return { 'error' => "Can't resume session" }; #better error message
417
418   my $custnum = $session->{'custnum'}
419     or return { 'error' => "no customer record" };
420
421   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
422     or return { 'error' => "unknown custnum $custnum" };
423
424   my $new = new FS::cust_main { $cust_main->hash };
425   $new->set( $_ => $p->{$_} )
426     foreach grep { exists $p->{$_} } @cust_main_editable_fields;
427
428   my $payby = '';
429   if (exists($p->{'payby'})) {
430     $p->{'payby'} =~ /^([A-Z]{4})$/
431       or return { 'error' => "illegal_payby " . $p->{'payby'} };
432     $payby = $1;
433   }
434
435   if ( $payby =~ /^(CARD|DCRD)$/ ) {
436
437     $new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
438
439     if ( $new->payinfo eq $cust_main->paymask ) {
440       $new->payinfo($cust_main->payinfo);
441     } else {
442       $new->payinfo($p->{'payinfo'});
443     }
444
445     $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
446
447   } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) {
448
449     my $payinfo;
450     $p->{'payinfo1'} =~ /^([\dx]+)$/
451       or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
452     my $payinfo1 = $1;
453      $p->{'payinfo2'} =~ /^([\dx]+)$/
454       or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
455     my $payinfo2 = $1;
456     $payinfo = $payinfo1. '@'. $payinfo2;
457
458     $new->payinfo( ($payinfo eq $cust_main->paymask)
459                      ? $cust_main->payinfo
460                      : $payinfo
461                  );
462
463     $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' );
464
465   } elsif ( $payby =~ /^(BILL)$/ ) {
466     #no-op
467   } elsif ( $payby ) {  #notyet ready
468     return { 'error' => "unknown payby $payby" };
469   }
470
471   my @invoicing_list;
472   if ( exists $p->{'invoicing_list'} || exists $p->{'postal_invoicing'} ) {
473     #false laziness with httemplate/edit/process/cust_main.cgi
474     @invoicing_list = split( /\s*\,\s*/, $p->{'invoicing_list'} );
475     push @invoicing_list, 'POST' if $p->{'postal_invoicing'};
476   } else {
477     @invoicing_list = $cust_main->invoicing_list;
478   }
479
480   my $error = $new->replace($cust_main, \@invoicing_list);
481   return { 'error' => $error } if $error;
482   #$cust_main = $new;
483   
484   return { 'error' => '' };
485 }
486
487 sub payment_info {
488   my $p = shift;
489   my $session = _cache->get($p->{'session_id'})
490     or return { 'error' => "Can't resume session" }; #better error message
491
492   ##
493   #generic
494   ##
495
496   my $conf = new FS::Conf;
497   use vars qw($payment_info); #cache for performance
498   unless ( $payment_info ) {
499
500     my %states = map { $_->state => 1 }
501                    qsearch('cust_main_county', {
502                      'country' => $conf->config('countrydefault') || 'US'
503                    } );
504
505     my %cust_paybys = map { $_ => 1 }
506                       map { FS::payby->payby2payment($_) }
507                           $conf->config('signup_server-payby');
508
509     my @cust_paybys = keys %cust_paybys;
510
511     $payment_info = {
512
513       #list all counties/states/countries
514       'cust_main_county' => 
515         [ map { $_->hashref } qsearch('cust_main_county', {}) ],
516
517       #shortcut for one-country folks
518       'states' =>
519         [ sort { $a cmp $b } keys %states ],
520
521       'card_types' => card_types(),
522
523       'paytypes' => [ @FS::cust_main::paytypes ],
524
525       'paybys' => [ $conf->config('signup_server-payby') ],
526       'cust_paybys' => \@cust_paybys,
527
528       'stateid_label' => FS::Msgcat::_gettext('stateid'),
529       'stateid_state_label' => FS::Msgcat::_gettext('stateid_state'),
530
531       'show_ss'  => $conf->exists('show_ss'),
532       'show_stateid' => $conf->exists('show_stateid'),
533       'show_paystate' => $conf->exists('show_bankstate'),
534
535       'save_unchecked' => $conf->exists('selfservice-save_unchecked'),
536     };
537
538   }
539
540   ##
541   #customer-specific
542   ##
543
544   my %return = %$payment_info;
545
546   my $custnum = $session->{'custnum'};
547
548   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
549     or return { 'error' => "unknown custnum $custnum" };
550
551   $return{'hide_payment_fields'} = [
552     map { 
553       my $pg = payment_gateway($cust_main, $_);
554       $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
555     } @{ $return{cust_paybys} }
556   ];
557
558   $return{balance} = $cust_main->balance; #XXX pkg-balances?
559
560   $return{payname} = $cust_main->payname
561                      || ( $cust_main->first. ' '. $cust_main->get('last') );
562
563   $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
564
565   $return{payby} = $cust_main->payby;
566   $return{stateid_state} = $cust_main->stateid_state;
567
568   if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
569     $return{card_type} = cardtype($cust_main->payinfo);
570     $return{payinfo} = $cust_main->paymask;
571
572     @return{'month', 'year'} = $cust_main->paydate_monthyear;
573
574   }
575
576   if ( $cust_main->payby =~ /^(CHEK|DCHK)$/ ) {
577     my ($payinfo1, $payinfo2) = split '@', $cust_main->paymask;
578     $return{payinfo1} = $payinfo1;
579     $return{payinfo2} = $payinfo2;
580     $return{paytype}  = $cust_main->paytype;
581     $return{paystate} = $cust_main->paystate;
582
583   }
584
585   if ( $conf->config('prepayment_discounts-credit_type') ) {
586     #need to eval?
587     $return{discount_terms_hash} = { $cust_main->discount_terms_hash };
588   }
589
590   #doubleclick protection
591   my $_date = time;
592   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
593
594   return { 'error' => '',
595            %return,
596          };
597
598 };
599
600 #some false laziness with httemplate/process/payment.cgi - look there for
601 #ACH and CVV support stuff
602 sub process_payment {
603
604   my $p = shift;
605
606   my $session = _cache->get($p->{'session_id'})
607     or return { 'error' => "Can't resume session" }; #better error message
608
609   my %return;
610
611   my $custnum = $session->{'custnum'};
612
613   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
614     or return { 'error' => "unknown custnum $custnum" };
615
616   $p->{'amount'} =~ /^\s*(\d+(\.\d{2})?)\s*$/
617     or return { 'error' => gettext('illegal_amount') };
618   my $amount = $1;
619   return { error => 'Amount must be greater than 0' } unless $amount > 0;
620
621   $p->{'discount_term'} =~ /^\s*(\d*)\s*$/
622     or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} };
623   my $discount_term = $1;
624
625   $p->{'payname'} =~ /^([\w \,\.\-\']+)$/
626     or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
627   my $payname = $1;
628
629   $p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
630     or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
631   my $paybatch = $1;
632
633   $p->{'payby'} ||= 'CARD';
634   $p->{'payby'} =~ /^([A-Z]{4})$/
635     or return { 'error' => "illegal_payby " . $p->{'payby'} };
636   my $payby = $1;
637
638   #false laziness w/process/payment.cgi
639   my $payinfo;
640   my $paycvv = '';
641   if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
642   
643     $p->{'payinfo1'} =~ /^([\dx]+)$/
644       or return { 'error' => "illegal account number ". $p->{'payinfo1'} };
645     my $payinfo1 = $1;
646      $p->{'payinfo2'} =~ /^([\dx]+)$/
647       or return { 'error' => "illegal ABA/routing number ". $p->{'payinfo2'} };
648     my $payinfo2 = $1;
649     $payinfo = $payinfo1. '@'. $payinfo2;
650
651     $payinfo = $cust_main->payinfo
652       if $cust_main->paymask eq $payinfo;
653    
654   } elsif ( $payby eq 'CARD' || $payby eq 'DCRD' ) {
655    
656     $payinfo = $p->{'payinfo'};
657
658     #more intelligent mathing will be needed here if you change
659     #card_masking_method and don't remove existing paymasks
660     $payinfo = $cust_main->payinfo
661       if $cust_main->paymask eq $payinfo;
662
663     $payinfo =~ s/\D//g;
664     $payinfo =~ /^(\d{13,16})$/
665       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
666     $payinfo = $1;
667
668     validate($payinfo)
669       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
670     return { 'error' => gettext('unknown_card_type') }
671       if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown";
672
673     if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
674       if ( cardtype($payinfo) eq 'American Express card' ) {
675         $p->{'paycvv'} =~ /^\s*(\d{4})\s*$/
676           or return { 'error' => "CVV2 (CID) for American Express cards is four digits." };
677         $paycvv = $1;
678       } else {
679         $p->{'paycvv'} =~ /^\s*(\d{3})\s*$/
680           or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
681         $paycvv = $1;
682       }
683     }
684   
685   } else {
686     die "unknown payby $payby";
687   }
688
689   my %payby2fields = (
690     'CARD' => [ qw( paystart_month paystart_year payissue payip
691                     address1 address2 city state zip country    ) ],
692     'CHEK' => [ qw( ss paytype paystate stateid stateid_state payip ) ],
693   );
694
695   my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
696     'quiet'    => 1,
697     'payinfo'  => $payinfo,
698     'paydate'  => $p->{'year'}. '-'. $p->{'month'}. '-01',
699     'payname'  => $payname,
700     'paybatch' => $paybatch, #this doesn't actually do anything
701     'paycvv'   => $paycvv,
702     'pkgnum'   => $session->{'pkgnum'},
703     'discount_term' => $discount_term,
704     'selfservice' => 1,
705     map { $_ => $p->{$_} } @{ $payby2fields{$payby} }
706   );
707   return { 'error' => $error } if $error;
708
709   $cust_main->apply_payments;
710
711   if ( $p->{'save'} ) {
712     my $new = new FS::cust_main { $cust_main->hash };
713     if ($payby eq 'CARD' || $payby eq 'DCRD') {
714       $new->set( $_ => $p->{$_} )
715         foreach qw( payname paystart_month paystart_year payissue payip
716                     address1 address2 city state zip country );
717       $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
718     } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
719       $new->set( $_ => $p->{$_} )
720         foreach qw( payname payip paytype paystate
721                     stateid stateid_state );
722       $new->set( 'payby' => $p->{'auto'} ? 'CHEK' : 'DCHK' );
723     }
724     $new->set( 'payinfo' => $cust_main->card_token || $payinfo );
725     $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
726     my $error = $new->replace($cust_main);
727     if ( $error ) {
728       #no, this causes customers to process their payments again
729       #return { 'error' => $error };
730       #XXX just warn verosely for now so i can figure out how these happen in
731       # the first place, eventually should redirect them to the "change
732       #address" page but indicate the payment did process??
733       delete($p->{'payinfo'}); #don't want to log this!
734       warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n".
735            "NEW: ". Dumper($new)."\n".
736            "OLD: ". Dumper($cust_main)."\n".
737            "PACKET: ". Dumper($p)."\n";
738     #} else {
739       #not needed...
740       #$cust_main = $new;
741     }
742   }
743
744   return { 'error' => '' };
745
746 }
747
748 sub realtime_collect {
749   my $p = shift;
750
751   my $session = _cache->get($p->{'session_id'})
752     or return { 'error' => "Can't resume session" }; #better error message
753
754   my $custnum = $session->{'custnum'};
755
756   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
757     or return { 'error' => "unknown custnum $custnum" };
758
759   my $amount;
760   if ( $p->{'amount'} ) {
761     $amount = $p->{'amount'};
762   }
763   elsif ( $session->{'pkgnum'} ) {
764     $amount = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
765   }
766   else {
767     $amount = $cust_main->balance;
768   }
769
770   my $error = $cust_main->realtime_collect(
771     'method'     => $p->{'method'},
772     'amount'     => $amount,
773     'pkgnum'     => $session->{'pkgnum'},
774     'session_id' => $p->{'session_id'},
775     'apply'      => 1,
776     'selfservice'=> 1,
777   );
778   return { 'error' => $error } unless ref( $error );
779
780   return { 'error' => '', amount => $amount, %$error };
781 }
782
783 sub process_payment_order_pkg {
784   my $p = shift;
785
786   my $hr = process_payment($p);
787   return $hr if $hr->{'error'};
788
789   order_pkg($p);
790 }
791
792 sub process_payment_order_renew {
793   my $p = shift;
794
795   my $hr = process_payment($p);
796   return $hr if $hr->{'error'};
797
798   order_renew($p);
799 }
800
801 sub process_prepay {
802
803   my $p = shift;
804
805   my $session = _cache->get($p->{'session_id'})
806     or return { 'error' => "Can't resume session" }; #better error message
807
808   my %return;
809
810   my $custnum = $session->{'custnum'};
811
812   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
813     or return { 'error' => "unknown custnum $custnum" };
814
815   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = ( 0, 0, 0, 0, 0 );
816   my $error = $cust_main->recharge_prepay( $p->{'prepaid_cardnum'},
817                                            \$amount,
818                                            \$seconds,
819                                            \$upbytes,
820                                            \$downbytes,
821                                            \$totalbytes,
822                                          );
823
824   return { 'error' => $error } if $error;
825
826   return { 'error'     => '',
827            'amount'    => $amount,
828            'seconds'   => $seconds,
829            'duration'  => duration_exact($seconds),
830            'upbytes'   => $upbytes,
831            'upload'    => FS::UI::bytecount::bytecount_unexact($upbytes),
832            'downbytes' => $downbytes,
833            'download'  => FS::UI::bytecount::bytecount_unexact($downbytes),
834            'totalbytes'=> $totalbytes,
835            'totalload' => FS::UI::bytecount::bytecount_unexact($totalbytes),
836          };
837
838 }
839
840 sub invoice {
841   my $p = shift;
842   my $session = _cache->get($p->{'session_id'})
843     or return { 'error' => "Can't resume session" }; #better error message
844
845   my $custnum = $session->{'custnum'};
846
847   my $invnum = $p->{'invnum'};
848
849   my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
850                                           'custnum' => $custnum } )
851     or return { 'error' => "Can't find invnum" };
852
853   #my %return;
854
855   return { 'error'        => '',
856            'invnum'       => $invnum,
857            'invoice_text' => join('', $cust_bill->print_text ),
858            'invoice_html' => $cust_bill->print_html( { unsquelch_cdr => 1 } ),
859          };
860
861 }
862
863 sub invoice_logo {
864   my $p = shift;
865
866   #sessioning for this?  how do we get the session id to the backend invoice
867   # template so it can add it to the link, blah
868
869   my $agentnum = '';
870   if ( $p->{'invnum'} ) {
871     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $p->{'invnum'} } )
872       or return { 'error' => 'unknown invnum' };
873     $agentnum = $cust_bill->cust_main->agentnum;
874   }
875
876   my $templatename = $p->{'template'} || $p->{'templatename'};
877
878   #false laziness-ish w/view/cust_bill-logo.cgi
879
880   my $conf = new FS::Conf;
881   if ( $templatename =~ /^([^\.\/]*)$/ && $conf->exists("logo_$1.png") ) {
882     $templatename = "_$1";
883   } else {
884     $templatename = '';
885   }
886
887   my $filename = "logo$templatename.png";
888
889   return { 'error'        => '',
890            'logo'         => $conf->config_binary($filename, $agentnum),
891            'content_type' => 'image/png', #should allow gif, jpg too
892          };
893 }
894
895
896 sub list_invoices {
897   my $p = shift;
898   my $session = _cache->get($p->{'session_id'})
899     or return { 'error' => "Can't resume session" }; #better error message
900
901   my $custnum = $session->{'custnum'};
902
903   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
904     or return { 'error' => "unknown custnum $custnum" };
905
906   my @cust_bill = $cust_main->cust_bill;
907
908   return  { 'error'       => '',
909             'invoices'    =>  [ map { { 'invnum' => $_->invnum,
910                                         '_date'  => $_->_date,
911                                         'date'   => time2str("%b %o, %Y", $_->_date),
912                                       }
913                                     } @cust_bill
914                               ]
915           };
916 }
917
918 sub cancel {
919   my $p = shift;
920   my $session = _cache->get($p->{'session_id'})
921     or return { 'error' => "Can't resume session" }; #better error message
922
923   my $custnum = $session->{'custnum'};
924
925   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
926     or return { 'error' => "unknown custnum $custnum" };
927
928   my @errors = $cust_main->cancel( 'quiet'=>1 );
929
930   my $error = scalar(@errors) ? join(' / ', @errors) : '';
931
932   return { 'error' => $error };
933
934 }
935
936 sub list_pkgs {
937   my $p = shift;
938
939   my($context, $session, $custnum) = _custoragent_session_custnum($p);
940   return { 'error' => $session } if $context eq 'error';
941
942   my $search = { 'custnum' => $custnum };
943   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
944   my $cust_main = qsearchs('cust_main', $search )
945     or return { 'error' => "unknown custnum $custnum" };
946
947   my $conf = new FS::Conf;
948   
949 # the duplication below is necessary:
950 # 1. to maintain the current buggy behaviour wrt the cust_pkg and part_pkg
951 # hashes overwriting each other (setup and no_auto fields). Fixing that is a
952 # non-backwards-compatible change breaking the software of anyone using the API
953 # instead of the stock selfservice
954 # 2. to return cancelled packages as well - for wholesale and non-wholesale
955   if( $conf->exists('selfservice_server-view-wholesale') ) {
956     return { 'svcnum'   => $session->{'svcnum'},
957             'custnum'  => $custnum,
958             'cust_pkg' => [ map {
959                           { $_->hash,
960                             part_pkg => [ map $_->hashref, $_->part_pkg ],
961                             part_svc =>
962                               [ map $_->hashref, $_->available_part_svc ],
963                             cust_svc => 
964                               [ map { my $ref = { $_->hash,
965                                                   label => [ $_->label ],
966                                                 };
967                                       $ref->{_password} = $_->svc_x->_password
968                                         if $context eq 'agent'
969                                         && $conf->exists('agent-showpasswords')
970                                         && $_->part_svc->svcdb eq 'svc_acct';
971                                       $ref;
972                                     } $_->cust_svc
973                               ],
974                           };
975                         } $cust_main->cust_pkg
976                   ],
977     'small_custview' =>
978       small_custview( $cust_main, $conf->config('countrydefault') ),
979     'wholesale_view' => 1,
980     'login_svcpart' => [ $conf->config('selfservice_server-login_svcpart') ],
981     'date_format' => $conf->config('date_format') || '%m/%d/%Y',
982     'lnp' => $conf->exists('svc_phone-lnp'),
983       };
984   }
985
986   { 'svcnum'   => $session->{'svcnum'},
987     'custnum'  => $custnum,
988     'cust_pkg' => [ map {
989                           { $_->hash,
990                             $_->part_pkg->hash,
991                             part_svc =>
992                               [ map $_->hashref, $_->available_part_svc ],
993                             cust_svc => 
994                               [ map { my $ref = { $_->hash,
995                                                   label => [ $_->label ],
996                                                 };
997                                       $ref->{_password} = $_->svc_x->_password
998                                         if $context eq 'agent'
999                                         && $conf->exists('agent-showpasswords')
1000                                         && $_->part_svc->svcdb eq 'svc_acct';
1001                                       $ref;
1002                                     } $_->cust_svc
1003                               ],
1004                           };
1005                         } $cust_main->ncancelled_pkgs
1006                   ],
1007     'small_custview' =>
1008       small_custview( $cust_main, $conf->config('countrydefault') ),
1009   };
1010
1011 }
1012
1013 sub list_svcs {
1014   my $p = shift;
1015
1016   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1017   return { 'error' => $session } if $context eq 'error';
1018
1019   my $search = { 'custnum' => $custnum };
1020   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1021   my $cust_main = qsearchs('cust_main', $search )
1022     or return { 'error' => "unknown custnum $custnum" };
1023
1024   my @cust_svc = ();
1025   #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
1026   foreach my $cust_pkg ( $p->{'ncancelled'} 
1027                          ? $cust_main->ncancelled_pkgs
1028                          : $cust_main->unsuspended_pkgs ) {
1029     next if $session->{'pkgnum'} && $cust_pkg->pkgnum != $session->{'pkgnum'};
1030     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
1031   }
1032   if ( $p->{'svcdb'} ) {
1033     my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
1034                   ? $p->{'svcdb'}
1035                   : ref($p->{'svcdb'}) eq 'ARRAY'
1036                     ? { map { $_=>1 } @{ $p->{'svcdb'} } }
1037                     : { $p->{'svcdb'} => 1 };
1038     @cust_svc = grep $svcdb->{ $_->part_svc->svcdb }, @cust_svc
1039   }
1040
1041   #@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
1042   #              @svc_x;
1043
1044   { 
1045     'svcnum'   => $session->{'svcnum'},
1046     'custnum'  => $custnum,
1047     'svcs'     => [
1048       map { 
1049             my $svc_x = $_->svc_x;
1050             my($label, $value) = $_->label;
1051             my $svcdb = $_->part_svc->svcdb;
1052             my $part_pkg = $_->cust_pkg->part_pkg;
1053
1054             my %hash = (
1055               'svcnum' => $_->svcnum,
1056               'svcdb'  => $svcdb,
1057               'label'  => $label,
1058               'value'  => $value,
1059             );
1060
1061             if ( $svcdb eq 'svc_acct' ) {
1062               %hash = (
1063                 %hash,
1064                 'username'   => $svc_x->username,
1065                 'email'      => $svc_x->email,
1066                 'seconds'    => $svc_x->seconds,
1067                 'upbytes'    => display_bytecount($svc_x->upbytes),
1068                 'downbytes'  => display_bytecount($svc_x->downbytes),
1069                 'totalbytes' => display_bytecount($svc_x->totalbytes),
1070
1071                 'recharge_amount'  => $part_pkg->option('recharge_amount',1),
1072                 'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
1073                 'recharge_upbytes'    =>
1074                   display_bytecount($part_pkg->option('recharge_upbytes',1)),
1075                 'recharge_downbytes'  =>
1076                   display_bytecount($part_pkg->option('recharge_downbytes',1)),
1077                 'recharge_totalbytes' =>
1078                   display_bytecount($part_pkg->option('recharge_totalbytes',1)),
1079                 # more...
1080               );
1081
1082             } elsif ( $svcdb eq 'svc_phone' ) {
1083               %hash = (
1084                 %hash,
1085               );
1086             }
1087
1088             \%hash;
1089           }
1090           @cust_svc
1091     ],
1092   };
1093
1094 }
1095
1096 sub _list_svc_usage {
1097   my($svc_acct, $begin, $end) = @_;
1098   my @usage = ();
1099   foreach my $part_export ( 
1100     map { qsearch ( 'part_export', { 'exporttype' => $_ } ) }
1101     qw( sqlradius sqlradius_withdomain )
1102   ) {
1103     push @usage, @ { $part_export->usage_sessions($begin, $end, $svc_acct) };
1104   }
1105   (@usage);
1106 }
1107
1108 sub list_svc_usage {
1109   _usage_details(\&_list_svc_usage, @_);
1110 }
1111
1112 sub _list_support_usage {
1113   my($svc_acct, $begin, $end) = @_;
1114   my @usage = ();
1115   foreach ( grep { $begin <= $_->_date && $_->_date <= $end }
1116             qsearch('acct_rt_transaction', { 'svcnum' => $svc_acct->svcnum })
1117           ) {
1118     push @usage, { 'seconds'  => $_->seconds,
1119                    'support'  => $_->support,
1120                    '_date'    => $_->_date,
1121                    'id'       => $_->transaction_id,
1122                    'creator'  => $_->creator,
1123                    'subject'  => $_->subject,
1124                    'status'   => $_->status,
1125                    'ticketid' => $_->ticketid,
1126                  };
1127   }
1128   (@usage);
1129 }
1130
1131 sub list_support_usage {
1132   _usage_details(\&_list_support_usage, @_);
1133 }
1134
1135 sub _list_cdr_usage {
1136   my($svc_phone, $begin, $end) = @_;
1137   map [ $_->downstream_csv('format' => 'default') ], #XXX config for format
1138       $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
1139 }
1140
1141 sub list_cdr_usage {
1142   my $p = shift;
1143   _usage_details( \&_list_cdr_usage, $p,
1144                   'svcdb' => 'svc_phone',
1145                 );
1146 }
1147
1148 sub _usage_details {
1149   my($callback, $p, %opt) = @_;
1150
1151   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1152   return { 'error' => $session } if $context eq 'error';
1153
1154   my $search = { 'svcnum' => $p->{'svcnum'} };
1155   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1156
1157   my $svcdb = $opt{'svcdb'} || 'svc_acct';
1158
1159   my $svc_x = qsearchs( $svcdb, $search );
1160   return { 'error' => 'No service selected in list_svc_usage' } 
1161     unless $svc_x;
1162
1163   my $header = $svcdb eq 'svc_phone'
1164                  ? [ split(',', FS::cdr::invoice_header('default') ) ]  #XXX
1165                  : [];
1166
1167   my $cust_pkg = $svc_x->cust_svc->cust_pkg;
1168   my $freq     = $cust_pkg->part_pkg->freq;
1169   my $start    = $cust_pkg->setup;
1170   #my $end      = $cust_pkg->bill; # or time?
1171   my $end      = time;
1172
1173   unless ( $p->{beginning} ) {
1174     $p->{beginning} = $cust_pkg->last_bill;
1175     $p->{ending}    = $end;
1176   }
1177
1178   my (@usage) = &$callback($svc_x, $p->{beginning}, $p->{ending});
1179
1180   #kinda false laziness with FS::cust_main::bill, but perhaps
1181   #we should really change this bit to DateTime and DateTime::Duration
1182   #
1183   #change this bit to use Date::Manip? CAREFUL with timezones (see
1184   # mailing list archive)
1185   my ($nsec,$nmin,$nhour,$nmday,$nmon,$nyear) =
1186     (localtime($p->{ending}) )[0,1,2,3,4,5];
1187   my ($psec,$pmin,$phour,$pmday,$pmon,$pyear) =
1188     (localtime($p->{beginning}) )[0,1,2,3,4,5];
1189
1190   if ( $freq =~ /^\d+$/ ) {
1191     $nmon += $freq;
1192     until ( $nmon < 12 ) { $nmon -= 12; $nyear++; }
1193     $pmon -= $freq;
1194     until ( $pmon >= 0 ) { $pmon += 12; $pyear--; }
1195   } elsif ( $freq =~ /^(\d+)w$/ ) {
1196     my $weeks = $1;
1197     $nmday += $weeks * 7;
1198     $pmday -= $weeks * 7;
1199   } elsif ( $freq =~ /^(\d+)d$/ ) {
1200     my $days = $1;
1201     $nmday += $days;
1202     $pmday -= $days;
1203   } elsif ( $freq =~ /^(\d+)h$/ ) {
1204     my $hours = $1;
1205     $nhour += $hours;
1206     $phour -= $hours;
1207   } else {
1208     return { 'error' => "unparsable frequency: ". $freq };
1209   }
1210   
1211   my $previous  = timelocal_nocheck($psec,$pmin,$phour,$pmday,$pmon,$pyear);
1212   my $next      = timelocal_nocheck($nsec,$nmin,$nhour,$nmday,$nmon,$nyear);
1213
1214   { 
1215     'error'     => '',
1216     'svcnum'    => $p->{svcnum},
1217     'beginning' => $p->{beginning},
1218     'ending'    => $p->{ending},
1219     'previous'  => ($previous > $start) ? $previous : $start,
1220     'next'      => ($next < $end) ? $next : $end,
1221     'header'    => $header,
1222     'usage'     => \@usage,
1223   };
1224 }
1225
1226 sub order_pkg {
1227   my $p = shift;
1228
1229   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1230   return { 'error' => $session } if $context eq 'error';
1231
1232   my $search = { 'custnum' => $custnum };
1233   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1234   my $cust_main = qsearchs('cust_main', $search )
1235     or return { 'error' => "unknown custnum $custnum" };
1236
1237   my $status = $cust_main->status;
1238   #false laziness w/ClientAPI/Signup.pm
1239
1240   my $cust_pkg = new FS::cust_pkg ( {
1241     'custnum' => $custnum,
1242     'pkgpart' => $p->{'pkgpart'},
1243   } );
1244   my $error = $cust_pkg->check;
1245   return { 'error' => $error } if $error;
1246
1247   my @svc = ();
1248   unless ( $p->{'svcpart'} eq 'none' ) {
1249
1250     my $svcdb;
1251     my $svcpart = '';
1252     if ( $p->{'svcpart'} =~ /^(\d+)$/ ) {
1253       $svcpart = $1;
1254       my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
1255       return { 'error' => "Unknown svcpart $svcpart" } unless $part_svc;
1256       $svcdb = $part_svc->svcdb;
1257     } else {
1258       $svcdb = 'svc_acct';
1259     }
1260     $svcpart ||= $cust_pkg->part_pkg->svcpart($svcdb);
1261
1262     my %fields = (
1263       'svc_acct'     => [ qw( username domsvc _password sec_phrase popnum ) ],
1264       'svc_domain'   => [ qw( domain ) ],
1265       'svc_phone'    => [ qw( phonenum pin sip_password phone_name ) ],
1266       'svc_external' => [ qw( id title ) ],
1267       'svc_pbx'      => [ qw( id name ) ],
1268     );
1269   
1270     my $svc_x = "FS::$svcdb"->new( {
1271       'svcpart'   => $svcpart,
1272       map { $_ => $p->{$_} } @{$fields{$svcdb}}
1273     } );
1274     
1275     if ( $svcdb eq 'svc_acct' && exists($p->{"snarf_machine1"}) ) {
1276       my @acct_snarf;
1277       my $snarfnum = 1;
1278       while ( length($p->{"snarf_machine$snarfnum"}) ) {
1279         my $acct_snarf = new FS::acct_snarf ( {
1280           'machine'   => $p->{"snarf_machine$snarfnum"},
1281           'protocol'  => $p->{"snarf_protocol$snarfnum"},
1282           'username'  => $p->{"snarf_username$snarfnum"},
1283           '_password' => $p->{"snarf_password$snarfnum"},
1284         } );
1285         $snarfnum++;
1286         push @acct_snarf, $acct_snarf;
1287       }
1288       $svc_x->child_objects( \@acct_snarf );
1289     }
1290     
1291     my $y = $svc_x->setdefault; # arguably should be in new method
1292     return { 'error' => $y } if $y && !ref($y);
1293   
1294     $error = $svc_x->check;
1295     return { 'error' => $error } if $error;
1296
1297     push @svc, $svc_x;
1298
1299   }
1300
1301   use Tie::RefHash;
1302   tie my %hash, 'Tie::RefHash';
1303   %hash = ( $cust_pkg => \@svc );
1304   #msgcat
1305   $error = $cust_main->order_pkgs( \%hash, 'noexport' => 1 );
1306   return { 'error' => $error } if $error;
1307
1308   my $conf = new FS::Conf;
1309   if ( $conf->exists('signup_server-realtime') ) {
1310
1311     my $bill_error = _do_bop_realtime( $cust_main, $status );
1312
1313     if ($bill_error) {
1314       $cust_pkg->cancel('quiet'=>1);
1315       return $bill_error;
1316     } else {
1317       $cust_pkg->reexport;
1318     }
1319
1320   } else {
1321     $cust_pkg->reexport;
1322   }
1323
1324   my $svcnum = $svc[0] ? $svc[0]->svcnum : '';
1325
1326   return { error=>'', pkgnum=>$cust_pkg->pkgnum, svcnum=>$svcnum };
1327
1328 }
1329
1330 sub change_pkg {
1331   my $p = shift;
1332
1333   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1334   return { 'error' => $session } if $context eq 'error';
1335
1336   my $search = { 'custnum' => $custnum };
1337   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1338   my $cust_main = qsearchs('cust_main', $search )
1339     or return { 'error' => "unknown custnum $custnum" };
1340
1341   my $status = $cust_main->status;
1342   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
1343     or return { 'error' => "unknown package $p->{pkgnum}" };
1344
1345   my @newpkg;
1346   my $error = FS::cust_pkg::order( $custnum,
1347                                    [$p->{pkgpart}],
1348                                    [$p->{pkgnum}],
1349                                    \@newpkg,
1350                                  );
1351
1352   my $conf = new FS::Conf;
1353   if ( $conf->exists('signup_server-realtime') ) {
1354
1355     my $bill_error = _do_bop_realtime( $cust_main, $status );
1356
1357     if ($bill_error) {
1358       $newpkg[0]->suspend;
1359       return $bill_error;
1360     } else {
1361       $newpkg[0]->reexport;
1362     }
1363
1364   } else {  
1365     $newpkg[0]->reexport;
1366   }
1367
1368   return { error => '', pkgnum => $cust_pkg->pkgnum };
1369
1370 }
1371
1372 sub order_recharge {
1373   my $p = shift;
1374
1375   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1376   return { 'error' => $session } if $context eq 'error';
1377
1378   my $search = { 'custnum' => $custnum };
1379   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1380   my $cust_main = qsearchs('cust_main', $search )
1381     or return { 'error' => "unknown custnum $custnum" };
1382
1383   my $status = $cust_main->status;
1384   my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $p->{'svcnum'} } )
1385     or return { 'error' => "unknown service " . $p->{'svcnum'} };
1386
1387   my $svc_x = $cust_svc->svc_x;
1388   my $part_pkg = $cust_svc->cust_pkg->part_pkg;
1389
1390   my %vhash =
1391     map { $_ =~ /^recharge_(.*)$/; $1, $part_pkg->option($_, 1) } 
1392     qw ( recharge_seconds recharge_upbytes recharge_downbytes
1393          recharge_totalbytes );
1394   my $amount = $part_pkg->option('recharge_amount', 1); 
1395   
1396   my ($l, $v, $d) = $cust_svc->label;  # blah
1397   my $pkg = "Recharge $v"; 
1398
1399   my $bill_error = $cust_main->charge($amount, $pkg,
1400      "time: $vhash{seconds}, up: $vhash{upbytes}," . 
1401      "down: $vhash{downbytes}, total: $vhash{totalbytes}",
1402      $part_pkg->taxclass); #meh
1403
1404   my $conf = new FS::Conf;
1405   if ( $conf->exists('signup_server-realtime') && !$bill_error ) {
1406
1407     $bill_error = _do_bop_realtime( $cust_main, $status );
1408
1409     if ($bill_error) {
1410       return $bill_error;
1411     } else {
1412       my $error = $svc_x->recharge (\%vhash);
1413       return { 'error' => $error } if $error;
1414     }
1415
1416   } else {  
1417     my $error = $bill_error;
1418     $error ||= $svc_x->recharge (\%vhash);
1419     return { 'error' => $error } if $error;
1420   }
1421
1422   return { error => '', svc => $cust_svc->part_svc->svc };
1423
1424 }
1425
1426 sub _do_bop_realtime {
1427   my ($cust_main, $status) = (shift, shift);
1428
1429     my $old_balance = $cust_main->balance;
1430
1431     my $bill_error =    $cust_main->bill
1432                      || $cust_main->apply_payments_and_credits
1433                      || $cust_main->realtime_collect('selfservice' => 1);
1434
1435     if (    $cust_main->balance > $old_balance
1436          && $cust_main->balance > 0
1437          && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ?
1438               1 : $status eq 'suspended' ) ) {
1439       #this makes sense.  credit is "un-doing" the invoice
1440       my $conf = new FS::Conf;
1441       $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
1442                           'self-service decline',
1443                           'reason_type' => $conf->config('signup_credit_type'),
1444                         );
1445       $cust_main->apply_credits( 'order' => 'newest' );
1446
1447       return { 'error' => '_decline', 'bill_error' => $bill_error };
1448     }
1449
1450     '';
1451 }
1452
1453 sub renew_info {
1454   my $p = shift;
1455
1456   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1457   return { 'error' => $session } if $context eq 'error';
1458
1459   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1460     or return { 'error' => "unknown custnum $custnum" };
1461
1462   my @cust_pkg = sort { $a->bill <=> $b->bill }
1463                  grep { $_->part_pkg->freq ne '0' }
1464                  $cust_main->ncancelled_pkgs;
1465
1466   #return { 'error' => 'No active packages to renew.' } unless @cust_pkg;
1467
1468   my $total = $cust_main->balance;
1469
1470   my @array = map {
1471                     my $bill = $_->bill;
1472                     $total += $_->part_pkg->base_recur($_, \$bill);
1473                     my $renew_date = $_->part_pkg->add_freq($_->bill);
1474                     {
1475                       'pkgnum'             => $_->pkgnum,
1476                       'amount'             => sprintf('%.2f', $total),
1477                       'bill_date'          => $_->bill,
1478                       'bill_date_pretty'   => time2str('%x', $_->bill),
1479                       'renew_date'         => $renew_date,
1480                       'renew_date_pretty'  => time2str('%x', $renew_date),
1481                       'expire_date'        => $_->expire,
1482                       'expire_date_pretty' => time2str('%x', $_->expire),
1483                     };
1484                   }
1485                   @cust_pkg;
1486
1487   return { 'dates' => \@array };
1488
1489 }
1490
1491 sub payment_info_renew_info {
1492   my $p = shift;
1493   my $renew_info   = renew_info($p);
1494   my $payment_info = payment_info($p);
1495   return { %$renew_info,
1496            %$payment_info,
1497          };
1498 }
1499
1500 sub order_renew {
1501   my $p = shift;
1502
1503   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1504   return { 'error' => $session } if $context eq 'error';
1505
1506   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1507     or return { 'error' => "unknown custnum $custnum" };
1508
1509   my $date = $p->{'date'};
1510
1511   my $now = time;
1512
1513   #freeside-daily -n -d $date fs_daily $custnum
1514   $cust_main->bill_and_collect( 'time'         => $date,
1515                                 'invoice_time' => $now,
1516                                 'actual_time'  => $now,
1517                                 'check_freq'   => '1d',
1518                               );
1519
1520   return { 'error' => '' };
1521
1522 }
1523
1524 sub suspend_pkg {
1525   my $p = shift;
1526   my $session = _cache->get($p->{'session_id'})
1527     or return { 'error' => "Can't resume session" }; #better error message
1528
1529   my $custnum = $session->{'custnum'};
1530
1531   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1532     or return { 'error' => "unknown custnum $custnum" };
1533
1534   my $conf = new FS::Conf;
1535   my $reasonnum = 
1536     $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum)
1537       or return { 'error' => 'Permission denied' };
1538
1539   my $pkgnum = $p->{'pkgnum'};
1540
1541   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1542                                         'pkgnum'  => $pkgnum,   } )
1543     or return { 'error' => "unknown pkgnum $pkgnum" };
1544
1545   my $error = $cust_pkg->suspend(reason => $reasonnum);
1546   return { 'error' => $error };
1547
1548 }
1549
1550 sub cancel_pkg {
1551   my $p = shift;
1552   my $session = _cache->get($p->{'session_id'})
1553     or return { 'error' => "Can't resume session" }; #better error message
1554
1555   my $custnum = $session->{'custnum'};
1556
1557   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
1558     or return { 'error' => "unknown custnum $custnum" };
1559
1560   my $pkgnum = $p->{'pkgnum'};
1561
1562   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1563                                         'pkgnum'  => $pkgnum,   } )
1564     or return { 'error' => "unknown pkgnum $pkgnum" };
1565
1566   my $error = $cust_pkg->cancel('quiet' => 1);
1567   return { 'error' => $error };
1568
1569 }
1570
1571 sub provision_phone {
1572  my $p = shift;
1573  my @bulkdid;
1574  @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
1575
1576 # single DID LNP
1577  unless($p->{'lnp'}) {
1578     $p->{'lnp_desired_due_date'} = parse_datetime($p->{'lnp_desired_due_date'});
1579     $p->{'lnp_status'} = "portingin";
1580     return _provision( 'FS::svc_phone',
1581                   [qw(lnp_desired_due_date lnp_other_provider 
1582                     lnp_other_provider_account phonenum countrycode lnp_status)],
1583                   [qw(phonenum countrycode)],
1584                   $p,
1585                   @_
1586                 );
1587  }
1588
1589 # single DID order
1590  unless (scalar(@bulkdid)) {
1591     return _provision( 'FS::svc_phone',
1592                   [qw(phonenum countrycode)],
1593                   [qw(phonenum countrycode)],
1594                   $p,
1595                   @_
1596                 );
1597  }
1598
1599 # bulk DID order case
1600   my $error;
1601   foreach my $did ( @bulkdid ) {
1602     $did =~ s/[^0-9]//g;
1603     $error = _provision( 'FS::svc_phone',
1604               [qw(phonenum countrycode)],
1605               [qw(phonenum countrycode)],
1606               {
1607                 'pkgnum' => $p->{'pkgnum'},
1608                 'svcpart' => $p->{'svcpart'},
1609                 'phonenum' => $did,
1610                 'countrycode' => $p->{'countrycode'},
1611                 'session_id' => $p->{'session_id'},
1612               }
1613             );
1614     return $error if ($error->{'error'} && length($error->{'error'}) > 1);
1615   }
1616   { 'bulkdid' => [ @bulkdid ], 'svc' => $error->{'svc'} }
1617 }
1618
1619 sub provision_acct {
1620   my $p = shift;
1621   warn "provision_acct called\n"
1622     if $DEBUG;
1623
1624   return { 'error' => gettext('passwords_dont_match') }
1625     if $p->{'_password'} ne $p->{'_password2'};
1626   return { 'error' => gettext('empty_password') }
1627     unless length($p->{'_password'});
1628  
1629   if ($p->{'domsvc'}) {
1630     my %domains = domain_select_hash FS::svc_acct(map { $_ => $p->{$_} }
1631                                                   qw ( svcpart pkgnum ) );
1632     return { 'error' => gettext('invalid_domain') }
1633       unless ($domains{$p->{'domsvc'}});
1634   }
1635
1636   warn "provision_acct calling _provision\n"
1637     if $DEBUG;
1638   _provision( 'FS::svc_acct',
1639               [qw(username _password domsvc)],
1640               [qw(username _password domsvc)],
1641               $p,
1642               @_
1643             );
1644 }
1645
1646 sub provision_external {
1647   my $p = shift;
1648   #_provision( 'FS::svc_external', [qw(id title)], [qw(id title)], $p, @_ );
1649   _provision( 'FS::svc_external',
1650               [],
1651               [qw(id title)],
1652               $p,
1653               @_
1654             );
1655 }
1656
1657 sub _provision {
1658   my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
1659   warn "_provision called for $class\n"
1660     if $DEBUG;
1661
1662   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1663   return { 'error' => $session } if $context eq 'error';
1664
1665   my $search = { 'custnum' => $custnum };
1666   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1667   my $cust_main = qsearchs('cust_main', $search )
1668     or return { 'error' => "unknown custnum $custnum" };
1669
1670   my $pkgnum = $p->{'pkgnum'};
1671
1672   warn "searching for custnum $custnum pkgnum $pkgnum\n"
1673     if $DEBUG;
1674   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1675                                         'pkgnum'  => $pkgnum,
1676                                                                } )
1677     or return { 'error' => "unknown pkgnum $pkgnum" };
1678
1679   warn "searching for svcpart ". $p->{'svcpart'}. "\n"
1680     if $DEBUG;
1681   my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
1682     or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
1683
1684   warn "creating $class record\n"
1685     if $DEBUG;
1686   my $svc_x = $class->new( {
1687     'pkgnum'  => $p->{'pkgnum'},
1688     'svcpart' => $p->{'svcpart'},
1689     map { $_ => $p->{$_} } @$fields
1690   } );
1691   warn "inserting $class record\n"
1692     if $DEBUG;
1693   my $error = $svc_x->insert;
1694
1695   unless ( $error ) {
1696     warn "finding inserted record for svcnum ". $svc_x->svcnum. "\n"
1697       if $DEBUG;
1698     $svc_x = qsearchs($svc_x->table, { 'svcnum' => $svc_x->svcnum })
1699   }
1700
1701   my $return = { 'svc'   => $part_svc->svc,
1702                  'error' => $error,
1703                  map { $_ => $svc_x->get($_) } @$return_fields
1704                };
1705   warn "_provision returning ". Dumper($return). "\n"
1706     if $DEBUG;
1707   return $return;
1708
1709 }
1710
1711 sub part_svc_info {
1712   my $p = shift;
1713
1714   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1715   return { 'error' => $session } if $context eq 'error';
1716
1717   my $search = { 'custnum' => $custnum };
1718   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1719   my $cust_main = qsearchs('cust_main', $search )
1720     or return { 'error' => "unknown custnum $custnum" };
1721
1722   my $pkgnum = $p->{'pkgnum'};
1723
1724   my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
1725                                         'pkgnum'  => $pkgnum,
1726                                                                } )
1727     or return { 'error' => "unknown pkgnum $pkgnum" };
1728
1729   my $svcpart = $p->{'svcpart'};
1730
1731   my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart,
1732                                       'svcpart' => $svcpart,           } )
1733     or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" };
1734   my $part_svc = $pkg_svc->part_svc;
1735
1736   my $conf = new FS::Conf;
1737
1738   return {
1739     'svc'     => $part_svc->svc,
1740     'svcdb'   => $part_svc->svcdb,
1741     'pkgnum'  => $pkgnum,
1742     'svcpart' => $svcpart,
1743     'custnum' => $custnum,
1744
1745     'security_phrase' => 0, #XXX !
1746     'svc_acct_pop'    => [], #XXX !
1747     'popnum'          => '',
1748     'init_popstate'   => '',
1749     'popac'           => '',
1750     'acstate'         => '',
1751
1752     'small_custview' =>
1753       small_custview( $cust_main, $conf->config('countrydefault') ),
1754
1755   };
1756
1757 }
1758
1759 sub unprovision_svc {
1760   my $p = shift;
1761
1762   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1763   return { 'error' => $session } if $context eq 'error';
1764
1765   my $search = { 'custnum' => $custnum };
1766   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1767   my $cust_main = qsearchs('cust_main', $search )
1768     or return { 'error' => "unknown custnum $custnum" };
1769
1770   my $svcnum = $p->{'svcnum'};
1771
1772   my $cust_svc = qsearchs('cust_svc', { 'svcnum'  => $svcnum, } )
1773     or return { 'error' => "unknown svcnum $svcnum" };
1774
1775   return { 'error' => "Service $svcnum does not belong to customer $custnum" }
1776     unless $cust_svc->cust_pkg->custnum == $custnum;
1777
1778   my $conf = new FS::Conf;
1779
1780   return { 'svc'   => $cust_svc->part_svc->svc,
1781            'error' => $cust_svc->cancel,
1782            'small_custview' =>
1783              small_custview( $cust_main, $conf->config('countrydefault') ),
1784          };
1785
1786 }
1787
1788 sub myaccount_passwd {
1789   my $p = shift;
1790   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1791   return { 'error' => $session } if $context eq 'error';
1792
1793   return { 'error' => "New passwords don't match." }
1794     if $p->{'new_password'} ne $p->{'new_password2'};
1795
1796   return { 'error' => 'Enter new password' }
1797     unless length($p->{'new_password'});
1798
1799   #my $search = { 'custnum' => $custnum };
1800   #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1801   $custnum =~ /^(\d+)$/ or die "illegal custnum";
1802   my $search = " AND custnum = $1";
1803   $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
1804
1805   my $svc_acct = qsearchs( {
1806     'table'     => 'svc_acct',
1807     'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
1808                    'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
1809                    'LEFT JOIN cust_main USING ( custnum ) ',
1810     'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
1811     'extra_sql' => $search, #important
1812   } )
1813     or return { 'error' => "Service not found" };
1814
1815   $svc_acct->_password($p->{'new_password'});
1816   my $error = $svc_acct->replace();
1817
1818   my($label, $value) = $svc_acct->cust_svc->label;
1819
1820   return { 'error' => $error,
1821            'label' => $label,
1822            'value' => $value,
1823          };
1824
1825 }
1826
1827 sub create_ticket {
1828   my $p = shift;
1829   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1830   return { 'error' => $session } if $context eq 'error';
1831
1832   warn "$me create_ticket: initializing ticket system\n" if $DEBUG;
1833   FS::TicketSystem->init();
1834
1835   my $conf = new FS::Conf;
1836   my $queue = $p->{'queue'}
1837               || $conf->config('ticket_system-selfservice_queueid')
1838               || $conf->config('ticket_system-default_queueid');
1839
1840   warn "$me create_ticket: creating ticket\n" if $DEBUG;
1841   my $err_or_ticket = FS::TicketSystem->create_ticket(
1842     '', #create RT session based on FS CurrentUser (fs_selfservice)
1843     'queue'   => $queue,
1844     'custnum' => $custnum,
1845     'svcnum'  => $session->{'svcnum'},
1846     map { $_ => $p->{$_} } qw( requestor cc subject message mime_type )
1847   );
1848
1849   if ( ref($err_or_ticket) ) {
1850     warn "$me create_ticket: sucessful: ". $err_or_ticket->id. "\n"
1851       if $DEBUG;
1852     return { 'error'     => '',
1853              'ticket_id' => $err_or_ticket->id,
1854            };
1855   } else {
1856     warn "$me create_ticket: unsucessful: $err_or_ticket\n"
1857       if $DEBUG;
1858     return { 'error' => $err_or_ticket };
1859   }
1860
1861
1862 }
1863
1864 sub did_report {
1865   my $p = shift;
1866   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1867   return { 'error' => $session } if $context eq 'error';
1868  
1869   return { error => 'requested format not implemented' } 
1870     unless ($p->{'format'} eq 'csv' || $p->{'format'} eq 'xls');
1871
1872   my $conf = new FS::Conf;
1873   my $age_threshold = 0;
1874   $age_threshold = time() - $conf->config('selfservice-recent-did-age')
1875     if ($p->{'recentonly'} && $conf->exists('selfservice-recent-did-age'));
1876
1877   my $search = { 'custnum' => $custnum };
1878   $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
1879   my $cust_main = qsearchs('cust_main', $search )
1880     or return { 'error' => "unknown custnum $custnum" };
1881
1882 # does it make more sense to just run one sql query for this instead of all the
1883 # insanity below? would increase performance greately for large data sets?
1884   my @svc_phone = ();
1885   foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
1886         my @part_svc = $cust_pkg->part_svc;
1887         foreach my $part_svc ( @part_svc ) {
1888             if($part_svc->svcdb eq 'svc_phone'){
1889                 my @cust_pkg_svc = @{$part_svc->cust_pkg_svc};
1890                 foreach my $cust_pkg_svc ( @cust_pkg_svc ) {
1891                     push @svc_phone, $cust_pkg_svc->svc_x
1892                         if $cust_pkg_svc->date_inserted >= $age_threshold;
1893                 }
1894             }
1895         }
1896   }
1897
1898   my $csv;
1899   my $xls;
1900   my($xls_r,$xls_c) = (0,0);
1901   my $xls_workbook;
1902   my $content = '';
1903   my @fields = qw( countrycode phonenum pin sip_password phone_name );
1904   if($p->{'format'} eq 'csv') {
1905     $csv = new Text::CSV_XS { 'always_quote' => 1,
1906                                  'eol'          => "\n",
1907                                 };
1908     return { 'error' => 'Unable to create CSV' } unless $csv->combine(@fields);
1909     $content .= $csv->string;
1910   }
1911   elsif($p->{'format'} eq 'xls') {
1912     my $XLS1 = new IO::Scalar \$content;
1913     $xls_workbook = Spreadsheet::WriteExcel->new($XLS1) 
1914         or return { 'error' => "Error opening .xls file: $!" };
1915     $xls = $xls_workbook->add_worksheet('DIDs');
1916     foreach ( @fields ) {
1917         $xls->write(0,$xls_c++,$_);
1918     }
1919     $xls_r++;
1920   }
1921
1922   foreach my $svc_phone ( @svc_phone ) {
1923     my @cols = map { $svc_phone->$_ } @fields;
1924     if($p->{'format'} eq 'csv') {
1925         return { 'error' => 'Unable to create CSV' } 
1926             unless $csv->combine(@cols);
1927         $content .= $csv->string;
1928     }
1929     elsif($p->{'format'} eq 'xls') {
1930         $xls_c = 0;
1931         foreach ( @cols ) {
1932             $xls->write($xls_r,$xls_c++,$_);
1933         }
1934         $xls_r++;
1935     }
1936   }
1937
1938   $xls_workbook->close() if $p->{'format'} eq 'xls';
1939   
1940   { content => $content, format => $p->{'format'}, };
1941 }
1942
1943 sub get_ticket {
1944   my $p = shift;
1945   my($context, $session, $custnum) = _custoragent_session_custnum($p);
1946   return { 'error' => $session } if $context eq 'error';
1947
1948   warn "$me get_ticket: initializing ticket system\n" if $DEBUG;
1949   FS::TicketSystem->init();
1950
1951   if(length($p->{'reply'})) {
1952 # currently this allows anyone to correspond on any ticket as fs_selfservice
1953 # probably bad...
1954       my @err_or_res = FS::TicketSystem->correspond_ticket(
1955         '', #create RT session based on FS CurrentUser (fs_selfservice)
1956         'ticket_id' => $p->{'ticket_id'},
1957         'content' => $p->{'reply'},
1958       );
1959     
1960     return { 'error' => 'unable to reply to ticket' } 
1961         unless ( $err_or_res[0] != 0 && defined $err_or_res[2] );
1962   }
1963
1964   warn "$me get_ticket: getting ticket\n" if $DEBUG;
1965   my $err_or_ticket = FS::TicketSystem->get_ticket(
1966     '', #create RT session based on FS CurrentUser (fs_selfservice)
1967     'ticket_id' => $p->{'ticket_id'},
1968   );
1969
1970   if ( ref($err_or_ticket) ) {
1971
1972 # since we're bypassing the RT security/permissions model by always using
1973 # fs_selfservice as the RT user (as opposed to a requestor, which we
1974 # can't do since we want all tickets linked to a cust), we check below whether
1975 # the requested ticket was actually linked to this customer
1976     my @custs = @{$err_or_ticket->{'custs'}};
1977     my @txns = @{$err_or_ticket->{'txns'}};
1978     my @filtered_txns;
1979
1980     return { 'error' => 'no customer' } unless ( $custnum && scalar(@custs) );
1981
1982     return { 'error' => 'invalid ticket requested' } 
1983         unless grep($_ eq $custnum, @custs);
1984
1985     foreach my $txn ( @txns ) {
1986         push @filtered_txns, $txn 
1987             if ($txn->{'type'} eq 'EmailRecord' 
1988                 || $txn->{'type'} eq 'Correspond'
1989                 || $txn->{'type'} eq 'Create');
1990     }
1991
1992     warn "$me get_ticket: sucessful: \n"
1993       if $DEBUG;
1994     return { 'error'     => '',
1995              'transactions' => \@filtered_txns,
1996              'ticket_id' => $p->{'ticket_id'},
1997            };
1998   } else {
1999     warn "$me create_ticket: unsucessful: $err_or_ticket\n"
2000       if $DEBUG;
2001     return { 'error' => $err_or_ticket };
2002   }
2003 }
2004
2005
2006 #--
2007
2008 sub _custoragent_session_custnum {
2009   my $p = shift;
2010
2011   my($context, $session, $custnum);
2012   if ( $p->{'session_id'} ) {
2013
2014     $context = 'customer';
2015     $session = _cache->get($p->{'session_id'})
2016       or return ( 'error' => "Can't resume session" ); #better error message
2017     $custnum = $session->{'custnum'};
2018
2019   } elsif ( $p->{'agent_session_id'} ) {
2020
2021     $context = 'agent';
2022     my $agent_cache = new FS::ClientAPI_SessionCache( {
2023       'namespace' => 'FS::ClientAPI::Agent',
2024     } );
2025     $session = $agent_cache->get($p->{'agent_session_id'})
2026       or return ( 'error' => "Can't resume session" ); #better error message
2027     $custnum = $p->{'custnum'};
2028
2029   } else {
2030     $context = 'error';
2031     return ( 'error' => "Can't resume session" ); #better error message
2032   }
2033
2034   ($context, $session, $custnum);
2035
2036 }
2037
2038 1;
2039