add conditions for customer cancelled packages, RT#42043
[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, $status, $queueid ) = @_;
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   my @statuses;
140   if ( defined($status) && $status ) {
141     if ( ref($status) ) {
142       if ( ref($status) eq 'HASH' ) {
143         @statuses = grep $status->{$_}, keys %$status;
144       } elsif ( ref($status) eq 'ARRAY' ) {
145         @statuses = @$status;
146       } else {
147         #what should be the failure mode here?  die?  return no tickets?
148         die 'unknown status ref '. ref($status);
149       }
150     } else {
151       @statuses = ( $status );
152     }
153     @statuses = grep /^\w+$/, @statuses; #injection prevention
154   } else {
155     @statuses = $self->statuses;
156   }
157
158   $rtql .= ' AND ( '.
159                       join(' OR ', map { "Status = '$_'" } @statuses).
160                ' ) ';
161
162   $rtql .= " AND Queue = $queueid " if $queueid;
163
164   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
165   $Tickets->FromSQL($rtql);
166
167   $Tickets->RowsPerPage($limit);
168   warn "\n\n" . $Tickets->BuildSelectQuery . "\n\n" if $DEBUG > 1;
169
170   return $Tickets;
171 }
172
173 sub href_customer_tickets {
174   my ($self, $custnum) = (shift, shift);
175   if ($custnum =~ /^(\d+)$/) {
176     return $self->href_search_tickets("Customer.number = $custnum", @_);
177   }
178   warn "bad custnum $custnum"; '';
179 }
180
181 sub href_service_tickets {
182   my ($self, $svcnum) = (shift, shift);
183   if ($svcnum =~ /^(\d+)$/ ) {
184     return $self->href_search_tickets("Service.number = $svcnum", @_);
185   }
186   warn "bad svcnum $svcnum"; '';
187 }
188
189 sub customer_tickets {
190   my $self = shift;
191   my $Tickets = $self->_tickets_search('Customer', @_);
192
193   my $conf = FS::Conf->new;
194   my $priority_order =
195     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
196
197   my @order_by = (
198     { FIELD => 'Priority', ORDER => $priority_order },
199     { FIELD => 'Id',       ORDER => 'DESC' },
200   );
201
202   $Tickets->OrderByCols(@order_by);
203
204   my @tickets;
205   while ( my $t = $Tickets->Next ) {
206     push @tickets, _ticket_info($t);
207   }
208
209   return \@tickets;
210 }
211
212 sub num_customer_tickets {
213   my ( $self, $custnum, $priority ) = @_;
214   $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
215 }
216
217 sub service_tickets  {
218   my $self = shift;
219   my $Tickets = $self->_tickets_search('Service', @_);
220
221   my $conf = FS::Conf->new;
222   my $priority_order =
223     $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
224
225   my @order_by = (
226     { FIELD => 'Priority', ORDER => $priority_order },
227     { FIELD => 'Id',       ORDER => 'DESC' },
228   );
229
230   $Tickets->OrderByCols(@order_by);
231
232   my @tickets;
233   while ( my $t = $Tickets->Next ) {
234     push @tickets, _ticket_info($t);
235   }
236
237   return \@tickets;
238 }
239
240 sub _ticket_info {
241   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
242   # custom fields.  Also returns custom and selfservice priority values as 
243   # _custom_priority and _selfservice_priority.
244   my $t = shift;
245
246   my $custom_priority = 
247     FS::Conf->new->config('ticket_system-custom_priority_field') || '';
248   my $ss_priority = selfservice_priority();
249
250   my %ticket_info;
251   foreach my $name ( $t->ReadableAttributes ) {
252     # lowercase names, and skip attributes with non-scalar values
253     $ticket_info{lc($name)} = $t->$name if !ref($t->$name);
254   }
255   $ticket_info{'owner'} = $t->OwnerObj->Name;
256   $ticket_info{'queue'} = $t->QueueObj->Name;
257   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
258     my $name = 'CF.{'.$CF->Name.'}';
259     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
260   }
261   # make this easy to find
262   if ( $custom_priority ) {
263     $ticket_info{'content'} = $ticket_info{"CF.{$custom_priority}"};
264   }
265   if ( $ss_priority ) {
266     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
267   }
268   my $svcnums = [ 
269     map { $_->Target =~ /cust_svc\/(\d+)/; $1 } 
270         @{ $t->Services->ItemsArrayRef }
271   ];
272   $ticket_info{'svcnums'} = $svcnums;
273
274   return \%ticket_info;
275 }
276
277 =item create_ticket SESSION_HASHREF, OPTION => VALUE ...
278
279 Class method.  Creates a ticket.  If there is an error, returns the scalar
280 error, otherwise returns the newly created RT::Ticket object.
281
282 Accepts the following options:
283
284 =over 4
285
286 =item queue
287
288 Queue name or Id
289
290 =item subject
291
292 Ticket subject
293
294 =item requestor
295
296 Requestor email address or arrayref of addresses
297
298 =item cc
299
300 Cc: email address or arrayref of addresses
301
302 =item message
303
304 Ticket message
305
306 =item mime_type
307
308 MIME type to use for message.  Defaults to text/plain.  Specifying text/html
309 can be useful to use HTML markup in message.
310
311 =item custnum
312
313 Customer number (see L<FS::cust_main>) to associate with ticket.
314
315 =item svcnum
316
317 Service number (see L<FS::cust_svc>) to associate with ticket.  Will also
318 associate the customer who has this service (unless the service is unlinked).
319
320 =back
321
322 =cut
323
324 sub create_ticket {
325   my($self, $session, %param) = @_;
326
327   $session = $self->session($session);
328
329   my $Queue = RT::Queue->new($session->{'CurrentUser'});
330   $Queue->Load( $param{'queue'} );
331
332   my $req = ref($param{'requestor'})
333               ? $param{'requestor'}
334               : ( $param{'requestor'} ? [ $param{'requestor'} ] : [] );
335
336   my $cc = ref($param{'cc'})
337              ? $param{'cc'}
338              : ( $param{'cc'} ? [ $param{'cc'} ] : [] );
339
340   my $mimeobj = MIME::Entity->build(
341     'Data' => $param{'message'},
342     'Type' => ( $param{'mime_type'} || 'text/plain' ),
343   );
344
345   my %ticket = (
346     'Queue'     => $Queue->Id,
347     'Subject'   => $param{'subject'},
348     'Requestor' => $req,
349     'Cc'        => $cc,
350     'MIMEObj'   => $mimeobj,
351   );
352   warn Dumper(\%ticket) if $DEBUG > 1;
353
354   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
355   my( $id, $Transaction, $ErrStr );
356   {
357     local $SIG{__DIE__};
358     ( $id, $Transaction, $ErrStr ) = $Ticket->Create( %ticket );
359   }
360   return $ErrStr if $id == 0;
361
362   warn "ticket got id $id\n" if $DEBUG;
363
364   #XXX check errors adding custnum/svcnum links (put it in a transaction)...
365   # but we do already know they're good
366
367   if ( $param{'custnum'} ) {
368     my( $val, $msg ) = $Ticket->_AddLink(
369      'Type'   => 'MemberOf',
370      'Target' => 'freeside://freeside/cust_main/'. $param{'custnum'},
371     );
372   }
373
374   if ( $param{'svcnum'} ) {
375     my( $val, $msg ) = $Ticket->_AddLink(
376      'Type'   => 'MemberOf',
377      'Target' => 'freeside://freeside/cust_svc/'. $param{'svcnum'},
378     );
379   }
380
381   $Ticket;
382 }
383
384 =item get_ticket SESSION_HASHREF, OPTION => VALUE ...
385
386 Class method. Retrieves a ticket. If there is an error, returns the scalar
387 error. Otherwise, currently returns a slightly tricky data structure containing
388 the ticket's attributes, a list of the linked customers, each transaction's 
389 content, description, and create time.
390
391 Accepts the following options:
392
393 =over 4
394
395 =item ticket_id 
396
397 The ticket id
398
399 =back
400
401 =cut
402
403 sub get_ticket {
404   my($self, $session, %param) = @_;
405
406   $session = $self->session($session);
407
408   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
409   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
410   return 'Could not load ticket' unless $ticketid;
411
412   my @custs = ();
413   foreach my $link ( @{ $Ticket->Customers->ItemsArrayRef } ) {
414     my $cust = $link->Target;
415     push @custs, $1 if $cust =~ /\/(\d+)$/;
416   }
417
418   my @txns = ();
419   my $transactions = $Ticket->Transactions;
420   while ( my $transaction = $transactions->Next ) {
421     my $t = { created => $transaction->Created,
422         content => $transaction->Content,
423         description => $transaction->Description,
424         type => $transaction->Type,
425     };
426     push @txns, $t;
427   }
428
429   { txns => [ @txns ],
430     custs => [ @custs ],
431     fields => _ticket_info($Ticket),
432   };
433 }
434
435 =item get_ticket_object SESSION_HASHREF, OPTION => VALUE...
436
437 Class method.  Retrieve the RT::Ticket object with the specified 
438 ticket_id.  If custnum is supplied, will also check that the object 
439 is a member of that customer.  If there is no ticket or the custnum 
440 check fails, returns nothing.  The meaning of that case is 
441 "to this customer, the ticket does not exist".
442
443 Options:
444
445 =over 4
446
447 =item ticket_id
448
449 =item custnum
450
451 =back
452
453 =cut
454
455 sub get_ticket_object {
456   my $self = shift;
457   my ($session, %opt) = @_;
458   $session = $self->session(shift);
459   # use a small search here so we can check ticket ownership
460   my $query;
461   if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
462     $query = "id = $1";
463   } else {
464     return;
465   }
466   if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
467     $query .= " AND Customer.number = $1"; # also checks ownership via services
468   }
469   my $Tickets = RT::Tickets->new($session->{CurrentUser});
470   $Tickets->FromSQL($query);
471   if ( $DEBUG ) { # temporary for RT#39536
472     warn "[get_ticket_object] " . $Tickets->BuildSelectQuery . "\n\n";
473   }
474   return $Tickets->First;
475 }
476
477 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
478
479 Class method. Correspond on a ticket. If there is an error, returns the scalar
480 error. Otherwise, returns the transaction id, error message, and
481 RT::Transaction object.
482
483 Accepts the following options:
484
485 =over 4
486
487 =item ticket_id 
488
489 The ticket id
490
491 =item content
492
493 Correspondence content
494
495 =back
496
497 =cut
498
499 sub correspond_ticket {
500   my($self, $session, %param) = @_;
501
502   $session = $self->session($session);
503
504   my $Ticket = RT::Ticket->new($session->{'CurrentUser'});
505   my $ticketid = $Ticket->Load( $param{'ticket_id'} );
506   return 'Could not load ticket' unless $ticketid;
507   return 'No content' unless $param{'content'};
508
509   $Ticket->Correspond( Content => $param{'content'} );
510 }
511
512 =item queues SESSION_HASHREF [, ACL ]
513
514 Retrieve a list of queues.  Pass the name of an RT access control right, 
515 such as 'CreateTicket', to return only queues on which the current user 
516 has that right.  Otherwise this will return all queues with the 'SeeQueue' 
517 right.
518
519 =cut
520
521 sub queues {
522   my( $self, $session, $acl ) = @_;
523   $session = $self->session($session);
524
525   my $showall = $acl ? 0 : 1;
526   my @result = ();
527   my $q = new RT::Queues($session->{'CurrentUser'});
528   $q->UnLimit;
529   while (my $queue = $q->Next) {
530     if ($showall || $queue->CurrentUserHasRight($acl)) {
531       push @result, {
532         Id          => $queue->Id,
533         Name        => $queue->Name,
534         Description => $queue->Description,
535       };
536     }
537   }
538   return map { $_->{Id} => $_->{Name} } @result;
539 }
540
541 #shameless false laziness w/RT::Interface::Web::AttemptExternalAuth
542 # to get logged into RT from afar
543 sub _web_external_auth {
544   my( $self, $session ) = @_;
545
546   my $user = $FS::CurrentUser::CurrentUser->username;
547
548   eval 'use RT::CurrentUser;';
549   die $@ if $@;
550
551   $session ||= {};
552   $session->{'CurrentUser'} = RT::CurrentUser->new();
553
554   warn "$me _web_external_auth loading RT user for $user\n"
555     if $DEBUG > 1;
556
557   $session->{'CurrentUser'}->Load($user);
558
559   if ( ! $session->{'CurrentUser'}->Id() ) {
560
561       # Create users on-the-fly
562
563       warn "can't load RT user for $user; auto-creating\n"
564         if $DEBUG;
565
566       my $UserObj = RT::User->new( RT::CurrentUser->new('RT_System') );
567
568       my ( $val, $msg ) = $UserObj->Create(
569           %{ ref($RT::AutoCreate) ? $RT::AutoCreate : {} },
570           Name  => $user,
571           Gecos => $user,
572       );
573
574       if ($val) {
575
576           # now get user specific information, to better create our user.
577           my $new_user_info
578               = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
579
580           # set the attributes that have been defined.
581           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
582           foreach my $attribute (
583               'Name',                  'Comments',
584               'Signature',             'EmailAddress',
585               'PagerEmailAddress',     'FreeformContactInfo',
586               'Organization',          'Disabled',
587               'Privileged',            'RealName',
588               'NickName',              'Lang',
589               'EmailEncoding',         'WebEncoding',
590               'ExternalContactInfoId', 'ContactInfoSystem',
591               'ExternalAuthId',        'Gecos',
592               'HomePhone',             'WorkPhone',
593               'MobilePhone',           'PagerPhone',
594               'Address1',              'Address2',
595               'City',                  'State',
596               'Zip',                   'Country'
597               )
598           {
599               #uhh, wrong root
600               #$m->comp( '/Elements/Callback', %ARGS,
601               #    _CallbackName => 'NewUser' );
602
603               my $method = "Set$attribute";
604               $UserObj->$method( $new_user_info->{$attribute} )
605                   if ( defined $new_user_info->{$attribute} );
606           }
607           $session->{'CurrentUser'}->Load($user);
608       }
609       else {
610
611          # we failed to successfully create the user. abort abort abort.
612           delete $session->{'CurrentUser'};
613
614           die "can't auto-create RT user: $msg"; #an error message would be nice :/
615           #$m->abort() unless $RT::WebFallbackToInternalAuth;
616           #$m->comp( '/Elements/Login', %ARGS,
617           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
618       }
619   }
620
621   unless ( $session->{'CurrentUser'}->Id() ) {
622       delete $session->{'CurrentUser'};
623
624       die "can't auto-create RT user";
625       #$user = $orig_user;
626       # 
627       #if ($RT::WebExternalOnly) {
628       #    $m->comp( '/Elements/Login', %ARGS,
629       #        Error => loc('You are not an authorized user') );
630       #    $m->abort();
631       #}
632   }
633
634   $session;
635
636 }
637
638 =item selfservice_priority
639
640 Returns the configured self-service priority field.
641
642 =cut
643
644 my $selfservice_priority;
645
646 sub selfservice_priority {
647   return $selfservice_priority ||= do {
648     my $conf = FS::Conf->new;
649     $conf->config('ticket_system-selfservice_priority_field') || '';
650   }
651 }
652
653 1;
654