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