RT# 83450 - fixed rateplan export
[freeside.git] / FS / FS / TicketSystem / RT_Internal.pm
1 package FS::TicketSystem::RT_Internal;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me );
5 use Data::Dumper;
6 use Date::Format qw( time2str );
7 use MIME::Entity;
8 use Encode;
9 use FS::UID qw(dbh);
10 use FS::CGI qw(popurl);
11 use FS::TicketSystem::RT_Libs;
12
13 @ISA = qw( FS::TicketSystem::RT_Libs );
14
15 $DEBUG = 0;
16 $me = '[FS::TicketSystem::RT_Internal]';
17
18 sub sql_num_customer_tickets {
19   "( select count(*) from Tickets
20                      join Links on ( Tickets.id = Links.LocalBase )
21      where ( Status = 'new' or Status = 'open' or Status = 'stalled' )
22        and Target = 'freeside://freeside/cust_main/' || custnum
23    )";
24 }
25
26 sub baseurl {
27   #my $self = shift;
28   if ( $RT::URI::freeside::URL ) {
29     $RT::URI::freeside::URL. '/rt/';
30   } else {
31     'http://you_need_to_set_RT_URI_freeside_URL_in_SiteConfig.pm/';
32   }
33 }
34
35 #mapping/genericize??
36 #ShowConfigTab ModifySelf
37 sub access_right {
38   my( $self, $session, $right ) = @_;
39
40   return '' unless FS::Conf->new->config('ticket_system');
41
42   $session = $self->session($session);
43
44   #warn "$me access_right: CurrentUser ". $session->{'CurrentUser'}. ":\n".
45   #     ( $DEBUG>1 ? Dumper($session->{'CurrentUser'}) : '' )
46   #  if $DEBUG > 1;
47
48   $session->{'CurrentUser'}->HasRight( Right  => $right,
49                                        Object => $RT::System );
50 }
51
52 sub session {
53   my( $self, $session ) = @_;
54
55   if ( $session && $session->{'CurrentUser'} ) { # does this even work?
56     warn "$me session: using existing session and CurrentUser: \n".
57          Dumper($session->{'CurrentUser'})
58       if $DEBUG;
59  } else {
60     warn "$me session: loading session and CurrentUser\n" if $DEBUG > 1;
61     $session = $self->_web_external_auth($session);
62   }
63
64   $session;
65 }
66
67 my $firsttime = 1;
68
69 sub init {
70   my $self = shift;
71   if ( $firsttime ) {
72
73     # this part only needs to be done once
74     warn "$me init: loading RT libraries\n" if $DEBUG;
75     eval '
76       use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
77       use RT;
78
79       #for web external auth...
80       use RT::Interface::Web;
81     ';
82     die $@ if $@;
83
84     warn "$me init: loading RT config\n" if $DEBUG;
85     {
86       local $SIG{__DIE__};
87       eval 'RT::LoadConfig();';
88     }
89     die $@ if $@;
90
91     $firsttime = 0;
92   }
93
94   # this needs to be done on each fork
95   warn "$me init: initializing RT\n" if $DEBUG;
96   {
97     local $SIG{__WARN__};
98     local $SIG{__DIE__};
99     eval 'RT::Init("NoSignalHandlers"=>1);';
100   }
101   die $@ if $@;
102
103   warn "$me init: complete" if $DEBUG;
104 }
105
106 =item customer_tickets CUSTNUM [ PARAMS ]
107
108 Replacement for the one in RT_External so that we can access custom fields 
109 properly.  Accepts a hashref with the following parameters:
110
111 number - custnum/svcnum
112
113 limit 
114
115 priority 
116
117 status
118
119 queueid
120
121 resolved - only return tickets resolved after this timestamp
122
123 =cut
124
125 # create an RT::Tickets object for a specified custnum or svcnum
126
127 sub _tickets_search {
128   my $self = shift;
129   my $type = shift;
130
131   my( $number, $limit, $priority, $status, $queueid, $opt );
132   if ( ref($_[0]) eq 'HASH' ) {
133     $opt = shift;
134     $number   = $$opt{'number'};
135     $limit    = $$opt{'limit'};
136     $priority = $$opt{'priority'};
137     $status   = $$opt{'status'};
138     $queueid  = $$opt{'queueid'};
139   } else {
140     ( $number, $limit, $priority, $status, $queueid ) = @_;
141     $opt = {};
142   }
143
144   $type =~ /^Customer|Service$/ or die "invalid type: $type";
145   $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number";
146   $limit =~ /^\d+$/ or die "invalid limit: $limit";
147
148   my $session = $self->session();
149   my $CurrentUser = $session->{CurrentUser} 
150     or die "unable to create an RT session";
151
152   my $Tickets = RT::Tickets->new($CurrentUser);
153
154   # "Customer.number" searches tickets linked via cust_svc also
155   my $rtql = "$type.number = $number";
156
157   if ( defined( $priority ) ) {
158     my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
159     if ( length( $priority ) ) {
160       $rtql .= " AND CF.{$custom_priority} = '$priority'";
161     }
162     else {
163       $rtql .= " AND CF.{$custom_priority} IS NULL";
164     }
165   }
166
167   my @statuses;
168   if ( defined($status) && $status ) {
169     if ( ref($status) ) {
170       if ( ref($status) eq 'HASH' ) {
171         @statuses = grep $status->{$_}, keys %$status;
172       } elsif ( ref($status) eq 'ARRAY' ) {
173         @statuses = @$status;
174       } else {
175         #what should be the failure mode here?  die?  return no tickets?
176         die 'unknown status ref '. ref($status);
177       }
178     } else {
179       @statuses = ( $status );
180     }
181     @statuses = grep /^\w+$/, @statuses; #injection prevention
182   } else {
183     @statuses = $self->statuses;
184   }
185
186   $rtql .= ' AND ( '.
187                       join(' OR ', map { "Status = '$_'" } @statuses).
188                ' ) ';
189
190   $rtql .= " AND Queue = $queueid " if $queueid;
191
192   if ($$opt{'resolved'}) {
193     $rtql .= " AND Resolved >= " . dbh->quote(time2str('%Y-%m-%d %H:%M:%S',$$opt{'resolved'}));
194   }
195
196   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
197   $Tickets->FromSQL($rtql);
198
199   $Tickets->RowsPerPage($limit);
200   warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
201
202   return $Tickets;
203 }
204
205 sub href_customer_tickets {
206   my ($self, $custnum) = (shift, shift);
207   if ($custnum =~ /^(\d+)$/) {
208     return $self->href_search_tickets("Customer.number = $custnum", @_);
209   }
210   warn "bad custnum $custnum"; '';
211 }
212
213 sub href_service_tickets {
214   my ($self, $svcnum) = (shift, shift);
215   if ($svcnum =~ /^(\d+)$/ ) {
216     return $self->href_search_tickets("Service.number = $svcnum", @_);
217   }
218   warn "bad svcnum $svcnum"; '';
219 }
220
221 sub customer_tickets {
222   my $self = shift;
223   my $Tickets = $self->_tickets_search('Customer', @_);
224
225   my $conf = FS::Conf->new;
226   my $priority_order =
227     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
228
229   my @order_by = (
230     { FIELD => 'Priority', ORDER => $priority_order },
231     { FIELD => 'Id',       ORDER => 'DESC' },
232   );
233
234   $Tickets->OrderByCols(@order_by);
235
236   my @tickets;
237   while ( my $t = $Tickets->Next ) {
238     push @tickets, _ticket_info($t);
239   }
240
241   return \@tickets;
242 }
243
244 sub num_customer_tickets {
245   my ( $self, $custnum, $priority ) = @_;
246   $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
247 }
248
249 sub service_tickets  {
250   my $self = shift;
251   my $Tickets = $self->_tickets_search('Service', @_);
252
253   my $conf = FS::Conf->new;
254   my $priority_order =
255     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
256
257   my @order_by = (
258     { FIELD => 'Priority', ORDER => $priority_order },
259     { FIELD => 'Id',       ORDER => 'DESC' },
260   );
261
262   $Tickets->OrderByCols(@order_by);
263
264   my @tickets;
265   while ( my $t = $Tickets->Next ) {
266     push @tickets, _ticket_info($t);
267   }
268
269   return \@tickets;
270 }
271
272 sub _ticket_info {
273   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
274   # custom fields.  Also returns custom and selfservice priority values as 
275   # _custom_priority and _selfservice_priority, and the IsUnreplied property
276   # as is_unreplied.
277   my $t = shift;
278
279   my $custom_priority = 
280     FS::Conf->new->config('ticket_system-custom_priority_field') || '';
281   my $ss_priority = selfservice_priority();
282
283   my %ticket_info;
284   foreach my $name ( $t->ReadableAttributes ) {
285     # lowercase names, and skip attributes with non-scalar values
286     $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
287   }
288   $ticket_info{'owner'} = $t->OwnerObj->Name;
289   $ticket_info{'queue'} = $t->QueueObj->Name;
290   $ticket_info{'_cf_sort_order'} = {};
291   my $cf_sort = 0;
292   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
293     $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
294     my $name = 'CF.{'.$CF->Name.'}';
295     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
296   }
297   # make this easy to find
298   if ( $custom_priority ) {
299     $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
300   }
301   if ( $ss_priority ) {
302     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
303   }
304   $ticket_info{'is_unreplied'} = $t->IsUnreplied;
305   my $svcnums = [ 
306     map { $_->Target =~ /cust_svc\/(\d+)/; $1 } 
307         @{ $t->Services->ItemsArrayRef }
308   ];
309   $ticket_info{'svcnums'} = $svcnums;
310
311   return \%ticket_info;
312 }
313
314 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
315
316 Class method.  Creates a ticket.  If there is an error, returns the scalar
317 error, otherwise returns the newly created RT::Ticket object.
318
319 Accepts the following options:
320
321 =over 4
322
323 =item queue
324
325 Queue name or Id
326
327 =item subject
328
329 Ticket subject
330
331 =item requestor
332
333 Requestor email address or arrayref of addresses
334
335 =item cc
336
337 Cc: email address or arrayref of addresses
338
339 =item message
340
341 Ticket message
342
343 =item mime_type
344
345 MIME type to use for message.  Defaults to text/plain.  Specifying text/html
346 can be useful to use HTML markup in message.
347
348 =item custnum
349
350 Customer number (see L<FS::cust_main>) to associate with ticket.
351
352 =item svcnum
353
354 Service number (see L<FS::cust_svc>) to associate with ticket.  Will also
355 associate the customer who has this service (unless the service is unlinked).
356
357 =back
358
359 =cut
360
361 sub create_ticket {
362   my($self, $session, %param) = @_;
363
364   $session = $self->session($session);
365
366   my $Queue = RT::Queue->new($session->{'CurrentUser'});
367   $Queue->Load( $param{'queue'} );
368
369   my $req = ref($param{'requestor'})
370               ? $param{'requestor'}
371               : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
372
373   my $cc = ref($param{'cc'})
374              ? $param{'cc'}
375              : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
376
377   my $mimeobj = MIME::Entity->build(
378     'Data' => Encode::encode_utf8( $param{'message'} ),
379     'Type' => ( $param{'mime_type'} || 'text/plain' ),
380   );
381
382   my %ticket = (
383     'Queue'     => $Queue->Id,
384     'Subject'   => $param{'subject'},
385     'Requestor' => $req,
386     'Cc'        => $cc,
387     'MIMEObj'   => $mimeobj,
388   );
389   warn Dumper(\%ticket) if $DEBUG > 1;
390
391   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
392   my( $id, $Transaction, $ErrStr );
393   {
394     local $SIG{__DIE__};
395     ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
396   }
397   return $ErrStr if $id == 0;
398
399   warn "ticket got id $id\n" if $DEBUG;
400
401   #XXX check errors adding custnum/svcnum links (put it in a transaction)...
402   # but we do already know they're good
403
404   if ( $param{'custnum'} ) {
405     my( $val, $msg ) = $Ticket->_AddLink(
406      'Type'   => 'MemberOf',
407      'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
408     );
409   }
410
411   if ( $param{'svcnum'} ) {
412     my( $val, $msg ) = $Ticket->_AddLink(
413      'Type'   => 'MemberOf',
414      'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
415     );
416   }
417
418   $Ticket;
419 }
420
421 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
422
423 Class method. Retrieves a ticket. If there is an error, returns the scalar
424 error. Otherwise, currently returns a slightly tricky data structure containing
425 the ticket's attributes, a list of the linked customers, each transaction's 
426 content, description, and create time.
427
428 Accepts the following options:
429
430 =over 4
431
432 =item ticket_id 
433
434 The ticket id
435
436 =back
437
438 =cut
439
440 sub get_ticket {
441   my($self, $session, %param) = @_;
442
443   $session = $self->session($session);
444
445   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
446   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
447   return 'Could not load ticket' unless $ticketid;
448
449   my @custs = ();
450   foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
451     my $cust = $link->Target;
452     push @custs, $1 if $cust =~ /\/(\d+)$/;
453   }
454
455   my @txns = ();
456   my $transactions = $Ticket->Transactions;
457   while ( my $transaction = $transactions->Next ) {
458     my $t = { created => $transaction->Created,
459         content => $transaction->Content,
460         description => $transaction->Description,
461         type => $transaction->Type,
462     };
463     push @txns, $t;
464   }
465
466   { txns => [ @txns ],
467     custs => [ @custs ],
468     fields => _ticket_info($Ticket),
469   };
470 }
471
472 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
473
474 Class method.  Retrieve the RT::Ticket object with the specified 
475 ticket_id.  If custnum is supplied, will also check that the object 
476 is a member of that customer.  If there is no ticket or the custnum 
477 check fails, returns nothing.  The meaning of that case is 
478 "to this customer, the ticket does not exist".
479
480 Options:
481
482 =over 4
483
484 =item ticket_id
485
486 =item custnum
487
488 =back
489
490 =cut
491
492 sub get_ticket_object {
493   my $self = shift;
494   my ($session, %opt) = @_;
495   $session = $self->session(shift);
496   # use a small search here so we can check ticket ownership
497   my $query;
498   if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
499     $query = "id = $1";
500   } else {
501     return;
502   }
503   if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
504     $query .= " AND Customer.number = $1"; # also checks ownership via services
505   }
506   my $Tickets = RT::Tickets->new($session->{CurrentUser});
507   $Tickets->FromSQL($query);
508   return $Tickets->First;
509 }
510
511 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
512
513 Class method. Correspond on a ticket. If there is an error, returns the scalar
514 error. Otherwise, returns the transaction id, error message, and
515 RT::Transaction object.
516
517 Accepts the following options:
518
519 =over 4
520
521 =item ticket_id 
522
523 The ticket id
524
525 =item content
526
527 Correspondence content
528
529 =back
530
531 =cut
532
533 sub correspond_ticket {
534   my($self, $session, %param) = @_;
535
536   $session = $self->session($session);
537
538   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
539   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
540   return 'Could not load ticket' unless $ticketid;
541   return 'No content' unless $param{'content'};
542
543   $Ticket->Correspond( Content => $param{'content'} );
544 }
545
546 =item queues SESSION_HASHREF [, ACL ]
547
548 Retrieve a list of queues.  Pass the name of an RT access control right, 
549 such as 'CreateTicket', to return only queues on which the current user 
550 has that right.  Otherwise this will return all queues with the 'SeeQueue' 
551 right.
552
553 =cut
554
555 sub queues {
556   my( $self, $session, $acl ) = @_;
557   $session = $self->session($session);
558
559   my $showall = $acl ? 0 : 1;
560   my @result = ();
561   my $q = new RT::Queues($session->{'CurrentUser'});
562   $q->UnLimit;
563   while (my $queue = $q->Next) {
564     if ($showall || $queue->CurrentUserHasRight($acl)) {
565       push @result, {
566         Id          => $queue->Id,
567         Name        => $queue->Name,
568         Description => $queue->Description,
569       };
570     }
571   }
572   return map { $_->{Id} => $_->{Name} } @result;
573 }
574
575 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
576 # to get logged into RT from afar
577 sub _web_external_auth {
578   my( $self, $session ) = @_;
579
580   my $user = $FS::CurrentUser::CurrentUser->username;
581
582   eval 'use RT::CurrentUser;';
583   die $@ if $@;
584
585   $session ||= {};
586   $session->{'CurrentUser'} = RT::CurrentUser->new();
587
588   warn "$me _web_external_auth loading RT user for $user\n"
589     if $DEBUG > 1;
590
591   $session->{'CurrentUser'}->Load($user);
592
593   if ( ! $session->{'CurrentUser'}->Id() ) {
594
595       # Create users on-the-fly
596
597       warn "can't load RT user for $user; auto-creating\n"
598         if $DEBUG;
599
600       my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
601
602       my ( $val, $msg ) = $UserObj->Create(
603           %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
604           Name  => $user,
605           Gecos => $user,
606       );
607
608       if ($val) {
609
610           # now get user specific information, to better create our user.
611           my $new_user_info
612               = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
613
614           # set the attributes that have been defined.
615           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
616           foreach my $attribute (
617               'Name',                  'Comments',
618               'Signature',             'EmailAddress',
619               'PagerEmailAddress',     'FreeformContactInfo',
620               'Organization',          'Disabled',
621               'Privileged',            'RealName',
622               'NickName',              'Lang',
623               'EmailEncoding',         'WebEncoding',
624               'ExternalContactInfoId', 'ContactInfoSystem',
625               'ExternalAuthId',        'Gecos',
626               'HomePhone',             'WorkPhone',
627               'MobilePhone',           'PagerPhone',
628               'Address1',              'Address2',
629               'City',                  'State',
630               'Zip',                   'Country'
631               )
632           {
633               #uhh, wrong root
634               #$m->comp( '/Elements/Callback', %ARGS,
635               #    _CallbackName => 'NewUser' );
636
637               my $method = "Set$attribute";
638               $UserObj->$method( $new_user_info->{$attribute} )
639                   if ( defined $new_user_info->{$attribute} );
640           }
641           $session->{'CurrentUser'}->Load($user);
642       }
643       else {
644
645          # we failed to successfully create the user. abort abort abort.
646           delete $session->{'CurrentUser'};
647
648           die "can't auto-create RT user: $msg"; #an error message would be nice :/
649           #$m->abort() unless $RT::WebFallbackToInternalAuth;
650           #$m->comp( '/Elements/Login', %ARGS,
651           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
652       }
653   }
654
655   unless ( $session->{'CurrentUser'}->Id() ) {
656       delete $session->{'CurrentUser'};
657
658       die "can't auto-create RT user";
659       #$user = $orig_user;
660       # 
661       #if ($RT::WebExternalOnly) {
662       #    $m->comp( '/Elements/Login', %ARGS,
663       #        Error => loc('You are not an authorized user') );
664       #    $m->abort();
665       #}
666   }
667
668   $session;
669
670 }
671
672 =item selfservice_priority
673
674 Returns the configured self-service priority field.
675
676 =cut
677
678 my $selfservice_priority;
679
680 sub selfservice_priority {
681   return $selfservice_priority ||= do {
682     my $conf = FS::Conf->new;
683     $conf->config('ticket_system-selfservice_priority_field') || '';
684   }
685 }
686
687 =item custom_fields
688
689 Returns a hash of custom field names and descriptions.
690
691 Accepts the following options:
692
693 lookuptype - limit results to this lookuptype
694
695 valuetype - limit results to this valuetype
696
697 Fields must be visible to CurrentUser.
698
699 =cut
700
701 sub custom_fields {
702   my $self = shift;
703   my %opt = @_;
704   my $lookuptype = $opt{lookuptype};
705   my $valuetype = $opt{valuetype};
706
707   my $CurrentUser = RT::CurrentUser->new();
708   $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
709   die "RT not configured" unless $CurrentUser->id;
710   my $CFs = RT::CustomFields->new($CurrentUser);
711
712   $CFs->UnLimit;
713
714   $CFs->Limit(FIELD => 'LookupType',
715               OPERATOR => 'ENDSWITH',
716               VALUE => $lookuptype)
717       if $lookuptype;
718
719   $CFs->Limit(FIELD => 'Type',
720               VALUE => $valuetype)
721       if $valuetype;
722
723   my @fields;
724   while (my $CF = $CFs->Next) {
725     push @fields, $CF->Name, ($CF->Description || $CF->Name);
726   }
727
728   return @fields;
729 }
730
731 1;
732