fix service add-ons RT#27974 / RT#28151, fallout from perf optimization #26097
[freeside.git] / FS / FS / cust_svc.pm
index 8cce7af..b01ed84 100644 (file)
@@ -1,29 +1,36 @@
 package FS::cust_svc;
+use base qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
 
 use strict;
-use vars qw( @ISA $DEBUG $me $ignore_quantity );
+use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system );
 use Carp;
 #use Scalar::Util qw( blessed );
+use List::Util qw( max );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh str2time_sql );
-use FS::cust_pkg;
 use FS::part_pkg;
 use FS::part_svc;
 use FS::pkg_svc;
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
+use FS::UI::Web;
 
 #most FS::svc_ classes are autoloaded in svc_x emthod
 use FS::svc_acct;  #this one is used in the cache stuff
 
-@ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
 
 $DEBUG = 0;
 $me = '[cust_svc]';
 
 $ignore_quantity = 0;
 
+#ask FS::UID to run this stuff for us later
+FS::UID->install_callback( sub { 
+  $conf = new FS::Conf;
+  $ticket_system = $conf->config('ticket_system')
+});
+
 sub _cache {
   my $self = shift;
   my ( $hashref, $cache ) = @_;
@@ -69,6 +76,8 @@ The following fields are currently supported:
 
 =item svcpart - Service definition (see L<FS::part_svc>)
 
+=item agent_svcid - Optional legacy service ID
+
 =item overlimit - date the service exceeded its usage limit
 
 =back
@@ -98,6 +107,44 @@ Deletes this service from the database.  If there is an error, returns the
 error, otherwise returns false.  Note that this only removes the cust_svc
 record - you should probably use the B<cancel> method instead.
 
+=cut
+
+my $rt_session;
+
+sub delete {
+  my $self = shift;
+
+  my $cust_pkg = $self->cust_pkg;
+  my $custnum = $cust_pkg->custnum if $cust_pkg;
+
+  my $error = $self->SUPER::delete;
+  return $error if $error;
+
+  if ( $ticket_system eq 'RT_Internal' ) {
+    unless ( $rt_session ) {
+      FS::TicketSystem->init;
+      $rt_session = FS::TicketSystem->session;
+    }
+    my $links = RT::Links->new($rt_session->{CurrentUser});
+    my $svcnum = $self->svcnum;
+    $links->Limit(FIELD => 'Target', 
+                  VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
+    while ( my $l = $links->Next ) {
+      my ($val, $msg);
+      if ( $custnum ) {
+        # re-link to point to the customer instead
+        ($val, $msg) =
+          $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
+      } else {
+        # unlinked service
+        ($val, $msg) = $l->Delete;
+      }
+      # can't do anything useful on error
+      warn "error unlinking ticket $svcnum: $msg\n" if !$val;
+    }
+  }
+}
+
 =item cancel
 
 Cancels the relevant service by calling the B<cancel> method of the associated
@@ -270,6 +317,17 @@ sub replace {
 #    }
 #  }
 
+  #trigger a pkg_change export on pkgnum changes
+  if ( $new->pkgnum != $old->pkgnum ) {
+    my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
+                                                  $old->cust_pkg,
+                                   );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error if $error;
+    }
+  }
+
   #my $error = $new->SUPER::replace($old, @_);
   my $error = $new->SUPER::replace($old);
   if ( $error ) {
@@ -297,6 +355,7 @@ sub check {
     $self->ut_numbern('svcnum')
     || $self->ut_numbern('pkgnum')
     || $self->ut_number('svcpart')
+    || $self->ut_numbern('agent_svcid')
     || $self->ut_numbern('overlimit')
   ;
   return $error if $error;
@@ -304,19 +363,62 @@ sub check {
   my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
   return "Unknown svcpart" unless $part_svc;
 
-  if ( $self->pkgnum ) {
-    my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-    return "Unknown pkgnum" unless $cust_pkg;
-    ($part_svc) = grep { $_->svcpart == $self->svcpart } $cust_pkg->part_svc;
+  if ( $self->pkgnum && ! $ignore_quantity ) {
+
+    #slightly inefficient since ->pkg_svc will also look it up, but fixing
+    # a much larger perf problem and have bigger fish to fry
+    my $cust_pkg = $self->cust_pkg;
+
+    my $pkg_svc = $self->pkg_svc
+                    || new FS::pkg_svc { 'svcpart'  => $self->svcpart,
+                                         'pkgpart'  => $cust_pkg->pkgpart,
+                                         'quantity' => 0,
+                                       };
+
+    #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
+    foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
+      my $addon_pkg_svc = qsearchs('pkg_svc', {
+                            pkgpart => $part_pkg_link->dst_pkgpart,
+                            svcpart => $self->svcpart,
+                          });
+      $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
+        if $addon_pkg_svc;
+    }
+
+   #better error message?  UI shouldn't get here
+   return "No svcpart ". $self->svcpart.
+          " services in pkgpart ". $cust_pkg->pkgpart
+     unless $pkg_svc->quantity > 0;
+
+    my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
+
+    #false laziness w/cust_pkg->part_svc
+    my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
+                            - $num_cust_svc
+                       );
 
-    return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc.
+   #better error message?  again, UI shouldn't get here
+    return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
            " services for pkgnum ". $self->pkgnum
-      if $part_svc->get('num_avail') == 0 and !$ignore_quantity;
+      if $num_avail <= 0;
+
   }
 
   $self->SUPER::check;
 }
 
+=item display_svcnum 
+
+Returns the displayed service number for this service: agent_svcid if it has a
+value, svcnum otherwise
+
+=cut
+
+sub display_svcnum {
+  my $self = shift;
+  $self->agent_svcid || $self->svcnum;
+}
+
 =item part_svc
 
 Returns the definition for this service, as a FS::part_svc object (see
@@ -336,13 +438,6 @@ sub part_svc {
 Returns the package this service belongs to, as a FS::cust_pkg object (see
 L<FS::cust_pkg>).
 
-=cut
-
-sub cust_pkg {
-  my $self = shift;
-  qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-}
-
 =item pkg_svc
 
 Returns the pkg_svc record for for this service, if applicable.
@@ -440,7 +535,7 @@ Returns a listref of html elements associated with this service's exports.
 sub export_links {
   my $self = shift;
   my $svc_x = $self->svc_x
-    or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+    or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
 
   $svc_x->export_links;
 }
@@ -754,6 +849,161 @@ sub get_session_history {
 
 }
 
+=item tickets  [ STATUS ]
+
+Returns an array of hashes representing the tickets linked to this service.
+
+An optional status (or arrayref or hashref of statuses) may be specified.
+
+=cut
+
+sub tickets {
+  my $self = shift;
+  my $status = ( @_ && $_[0] ) ? shift : '';
+
+  my $conf = FS::Conf->new;
+  my $num = $conf->config('cust_main-max_tickets') || 10;
+  my @tickets = ();
+
+  if ( $conf->config('ticket_system') ) {
+    unless ( $conf->config('ticket_system-custom_priority_field') ) {
+
+      @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
+                                                       $num,
+                                                       undef,
+                                                       $status,
+                                                     )
+                  };
+
+    } else {
+
+      foreach my $priority (
+        $conf->config('ticket_system-custom_priority_field-values'), ''
+      ) {
+        last if scalar(@tickets) >= $num;
+        push @tickets,
+        @{ FS::TicketSystem->service_tickets( $self->svcnum,
+                                              $num - scalar(@tickets),
+                                              $priority,
+                                              $status,
+                                            )
+         };
+      }
+    }
+  }
+  (@tickets);
+}
+
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item smart_search OPTION => VALUE ...
+
+Accepts the option I<search>, the string to search for.  The string will 
+be searched for as a username, email address, IP address, MAC address, 
+phone number, and hardware serial number.  Unlike the I<smart_search> on 
+customers, this always requires an exact match.
+
+=cut
+
+# though perhaps it should be fuzzy in some cases?
+
+sub smart_search {
+  my %param = __PACKAGE__->smart_search_param(@_);
+  qsearch(\%param);
+}
+
+sub smart_search_param {
+  my $class = shift;
+  my %opt = @_;
+
+  my $string = $opt{'search'};
+  $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
+
+  my @or = 
+      map { my $table = $_;
+            my $search_sql = "FS::$table"->search_sql($string);
+
+            "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
+            "FROM $table WHERE $search_sql";
+          }
+      FS::part_svc->svc_tables;
+
+  if ( $string =~ /^(\d+)$/ ) {
+    unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
+  }
+
+  my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
+                  " ON (svc_all.svcnum = cust_svc.svcnum) ";
+
+  my @extra_sql;
+
+  push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
+    'null_right' => 'View/link unlinked services'
+  );
+  my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
+  #for agentnum
+  $addl_from  .=  ' LEFT JOIN cust_pkg  USING ( pkgnum  )'.
+                  FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
+                  ' LEFT JOIN part_svc  USING ( svcpart )';
+
+  (
+    'table'     => 'cust_svc',
+    'select'    => 'svc_all.svcnum AS svcnum, '.
+                   'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
+                   'cust_svc.*',
+    'addl_from' => $addl_from,
+    'hashref'   => {},
+    'extra_sql' => $extra_sql,
+  );
+}
+
+sub _upgrade_data {
+  my $class = shift;
+
+  # fix missing (deleted by mistake) svc_x records
+  warn "searching for missing svc_x records...\n";
+  my %search = (
+    'table'     => 'cust_svc',
+    'select'    => 'cust_svc.*',
+    'addl_from' => ' LEFT JOIN ( ' .
+      join(' UNION ',
+        map { "SELECT svcnum FROM $_" } 
+        FS::part_svc->svc_tables
+      ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
+    'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
+  );
+  my @svcs = qsearch(\%search);
+  warn "found ".scalar(@svcs)."\n";
+
+  local $FS::Record::nowarn_classload = 1; # for h_svc_
+  local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
+
+  my %h_search = (
+    'hashref'  => { history_action => 'delete' },
+    'order_by' => ' ORDER BY history_date DESC LIMIT 1',
+  );
+  foreach my $cust_svc (@svcs) {
+    my $svcnum = $cust_svc->svcnum;
+    my $svcdb = $cust_svc->part_svc->svcdb;
+    $h_search{'hashref'}{'svcnum'} = $svcnum;
+    $h_search{'table'} = "h_$svcdb";
+    my $h_svc_x = qsearchs(\%h_search)
+      or next;
+    my $class = "FS::$svcdb";
+    my $new_svc_x = $class->new({ $h_svc_x->hash });
+    my $error = $new_svc_x->insert;
+    warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
+      if $error;
+  }
+
+  '';
+}
+
 =back
 
 =head1 BUGS