fix perf edge case with multiple large packages, on svc insert, RT#26097
[freeside.git] / FS / FS / cust_svc.pm
index 0a58d55..6dee274 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use vars qw( @ISA $DEBUG $me $ignore_quantity );
 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;
@@ -69,6 +70,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
@@ -270,6 +273,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 +311,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,30 +319,44 @@ 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;
-    my $pkg_svc = qsearchs( 'pkg_svc', {
-      'pkgpart' => $cust_pkg->pkgpart,
-      'svcpart' => $self->svcpart,
-    });
-    # or new FS::pkg_svc ( { 'pkgpart'  => $cust_pkg->pkgpart,
-    #                        'svcpart'  => $self->svcpart,
-    #                        'quantity' => 0                   } );
-    my $quantity = $pkg_svc ? $pkg_svc->quantity : 0;
-
-    my @cust_svc = qsearch('cust_svc', {
-      'pkgnum'  => $self->pkgnum,
-      'svcpart' => $self->svcpart,
-    });
-    return "Already ". scalar(@cust_svc). " ". $part_svc->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
+      or return "No svcpart ". $self->svcpart.
+                " services in pkgpart ". $cust_pkg->pkgpart;
+
+    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 $num_cust_svc ". $pkg_svc->part_svc->svc.
            " services for pkgnum ". $self->pkgnum
-      if scalar(@cust_svc) >= $quantity && !$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
@@ -382,6 +411,20 @@ sub date_inserted {
   $self->h_date('insert');
 }
 
+=item pkg_cancel_date
+
+Returns the date this service's package was canceled.  This normally only 
+exists for a service that's been preserved through cancellation with the 
+part_pkg.preserve flag.
+
+=cut
+
+sub pkg_cancel_date {
+  my $self = shift;
+  my $cust_pkg = $self->cust_pkg or return;
+  return $cust_pkg->getfield('cancel') || '';
+}
+
 =item label
 
 Returns a list consisting of:
@@ -486,18 +529,20 @@ where B<svcdb> is not "svc_acct".
 
 =cut
 
-#note: implementation here, POD in FS::svc_acct
-sub seconds_since {
-  my($self, $since) = @_;
-  my $dbh = dbh;
-  my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
-                              WHERE svcnum = ?
-                                AND login >= ?
-                                AND logout IS NOT NULL'
-  ) or die $dbh->errstr;
-  $sth->execute($self->svcnum, $since) or die $sth->errstr;
-  $sth->fetchrow_arrayref->[0];
-}
+#internal session db deprecated (or at least on hold)
+sub seconds_since { 'internal session db deprecated'; };
+##note: implementation here, POD in FS::svc_acct
+#sub seconds_since {
+#  my($self, $since) = @_;
+#  my $dbh = dbh;
+#  my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
+#                              WHERE svcnum = ?
+#                                AND login >= ?
+#                                AND logout IS NOT NULL'
+#  ) or die $dbh->errstr;
+#  $sth->execute($self->svcnum, $since) or die $sth->errstr;
+#  $sth->fetchrow_arrayref->[0];
+#}
 
 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
 
@@ -751,6 +796,70 @@ sub get_session_history {
 
 =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);
+            " ( svcdb = '$table'
+               AND 0 < ( SELECT COUNT(*) FROM $table
+                           WHERE $table.svcnum = cust_svc.svcnum
+                             AND $search_sql
+                       )
+             ) ";
+          }
+      FS::part_svc->svc_tables;
+
+  if ( $string =~ /^(\d+)$/ ) {
+    unshift @or, " ( agent_svcid IS NOT NULL AND agent_svcid = $1 ) ";
+  }
+
+  my @extra_sql = ' ( '. join(' OR ', @or). ' ) ';
+
+  push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
+    'null_right' => 'View/link unlinked services'
+  );
+  my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
+  #for agentnum
+  my $addl_from = ' LEFT JOIN cust_pkg  USING ( pkgnum  )'.
+                  ' LEFT JOIN cust_main USING ( custnum )'.
+                  ' LEFT JOIN part_svc  USING ( svcpart )';
+
+  (
+    'table'     => 'cust_svc',
+    'addl_from' => $addl_from,
+    'hashref'   => {},
+    'extra_sql' => $extra_sql,
+  );
+}
+
+=back
+
 =head1 BUGS
 
 Behaviour of changing the svcpart of cust_svc records is undefined and should