71513: Card tokenization [token_check]
[freeside.git] / FS / FS / agent.pm
index 0a0e4f7..8aa78c2 100644 (file)
@@ -1,15 +1,16 @@
 package FS::agent;
+use base qw( FS::Commission_Mixin FS::m2m_Common FS::m2name_Common FS::Record );
 
 use strict;
 use vars qw( @ISA );
+use Business::CreditCard 0.35;
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::cust_main;
 use FS::cust_pkg;
-use FS::agent_type;
 use FS::reg_code;
-#use Crypt::YAPassGen;
-
-@ISA = qw( FS::Record );
+use FS::agent_payment_gateway;
+use FS::TicketSystem;
+use FS::Conf;
 
 =head1 NAME
 
@@ -43,21 +44,49 @@ from FS::Record.  The following fields are currently supported:
 
 =over 4
 
-=item agentnum - primary key (assigned automatically for new agents)
+=item agentnum
+
+primary key (assigned automatically for new agents)
+
+=item agent
+
+Text name of this agent
+
+=item typenum
+
+Agent type (see L<FS::agent_type>)
+
+=item ticketing_queueid
+
+Ticketing Queue
+
+=item invoice_template
 
-=item agent - Text name of this agent
+Invoice template name
 
-=item typenum - Agent type.  See L<FS::agent_type>
+=item agent_custnum
 
-=item prog - For future use.
+Optional agent customer (see L<FS::cust_main>)
 
-=item freq - For future use.
+=item disabled
 
-=item disabled - Disabled flag, empty or 'Y'
+Disabled flag, empty or 'Y'
 
-=item username - Username for the Agent interface
+=item prog
 
-=item _password - Password for the Agent interface
+Deprecated (never used)
+
+=item freq
+
+Deprecated (never used)
+
+=item username
+
+(Deprecated) Username for the Agent interface
+
+=item _password
+
+(Deprecated) Password for the Agent interface
 
 =back
 
@@ -116,6 +145,9 @@ sub check {
       || $self->ut_number('typenum')
       || $self->ut_numbern('freq')
       || $self->ut_textn('prog')
+      || $self->ut_textn('invoice_template')
+      || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
+      || $self->ut_numbern('ticketing_queueid')
   ;
   return $error if $error;
 
@@ -147,11 +179,34 @@ sub check {
 
 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
 
+=item agent_cust_main
+
+Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
+agent.
+
 =cut
 
-sub agent_type {
+sub agent_cust_main {
   my $self = shift;
-  qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
+  qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
+}
+
+=item agent_currency
+
+Returns the FS::agent_currency objects (see L<FS::agent_currency>), if any, for
+this agent.
+
+=item agent_currency_hashref
+
+Returns a hash references of supported additional currencies for this agent.
+
+=cut
+
+sub agent_currency_hashref {
+  my $self = shift;
+  +{ map { $_->currency => 1 }
+       $self->agent_currency
+   };
 }
 
 =item pkgpart_hashref
@@ -167,6 +222,170 @@ sub pkgpart_hashref {
   $self->agent_type->pkgpart_hashref;
 }
 
+=item ticketing_queue
+
+Returns the queue name corresponding with the id from the I<ticketing_queueid>
+field, or the empty string.
+
+=cut
+
+sub ticketing_queue {
+  my $self = shift;
+  FS::TicketSystem->queue($self->ticketing_queueid);
+}
+
+=item payment_gateway [ OPTION => VALUE, ... ]
+
+Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
+
+Currently available options are I<nofatal>, I<method>, I<thirdparty>,
+<conf> and I<load_gatewaynum>.
+
+If I<nofatal> is set, and no gateway is available, then the empty string
+will be returned instead of throwing a fatal exception.
+
+The I<method> option can be used to influence the choice
+as well.  Presently only CHEK/ECHECK and PAYPAL methods are meaningful.
+
+If I<method> is CHEK/ECHECK and the default gateway is being returned,
+the business-onlinepayment-ach gateway will be returned if available.
+
+If I<thirdparty> is set and the I<method> is PAYPAL, the defined paypal
+gateway will be returned.
+
+If I<load_gatewaynum> exists, then either the specified gateway or the
+default gateway will be returned.  Agent overrides are ignored, and this can
+safely be called as a class method if this option is specified.  Not
+compatible with I<thirdparty>.
+
+Exsisting I<$conf> may be passed for efficiency.
+
+=cut
+
+# opts invnum/payinfo for cardtype/taxclass overrides no longer supported
+# any future overrides added here need to be reconciled with the tokenization process
+
+sub payment_gateway {
+  my ( $self, %options ) = @_;
+  
+  my $conf = $options{'conf'};
+  $conf ||= new FS::Conf;
+
+  if ( $options{thirdparty} ) {
+
+    # allows PayPal to coexist with credit card gateways
+    my $is_paypal = { op => '!=', value => 'PayPal' };
+    if ( uc($options{method}) eq 'PAYPAL' ) {
+      $is_paypal = 'PayPal';
+    }
+
+    my $gateway = qsearchs({
+        table     => 'payment_gateway',
+        addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
+        hashref   => {
+          gateway_namespace => 'Business::OnlineThirdPartyPayment',
+          gateway_module    => $is_paypal,
+          disabled          => '',
+        },
+        extra_sql => ' AND agentnum = '.$self->agentnum,
+    });
+
+    if ( $gateway ) {
+      return $gateway;
+    } elsif ( $options{'nofatal'} ) {
+      return '';
+    } else {
+      die "no third-party gateway configured\n";
+    }
+  }
+
+  my ($override, $payment_gateway);
+  if (exists $options{'load_gatewaynum'}) { # no agent overrides if this opt is in use
+    if ($options{'load_gatewaynum'}) {
+      $payment_gateway = qsearchs('payment_gateway', { gatewaynumnum => $options{'load_gatewaynum'} } );
+      # always fatal
+      die "Could not load payment gateway ".$options{'load_gatewaynum'} unless $payment_gateway;
+    } # else use default, loaded below
+  } else {
+    $override = qsearchs('agent_payment_gateway', { agentnum => $self->agentnum } );
+  }
+
+  if ( $override ) { #use a payment gateway override
+
+    $payment_gateway = $override->payment_gateway;
+
+    $payment_gateway->gateway_namespace('Business::OnlinePayment')
+      unless $payment_gateway->gateway_namespace;
+
+  } elsif (!$payment_gateway) { #use the standard settings from the config
+
+    # the standard settings from the config could be moved to a null agent
+    # agent_payment_gateway referenced payment_gateway
+
+    # remember, this block might be run as a class method if false load_gatewaynum exists
+
+    unless ( $conf->exists('business-onlinepayment') ) {
+      if ( $options{'nofatal'} ) {
+        return '';
+      } else {
+        die "Real-time processing not enabled\n";
+      }
+    }
+
+    #load up config
+    my $bop_config = 'business-onlinepayment';
+    $bop_config .= '-ach'
+      if ( $options{method}
+           && $options{method} =~ /^(ECHECK|CHEK)$/
+           && $conf->exists($bop_config. '-ach')
+         );
+    my ( $processor, $login, $password, $action, @bop_options ) =
+      $conf->config($bop_config);
+    $action ||= 'normal authorization';
+    pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+    die "No real-time processor is enabled - ".
+        "did you set the business-onlinepayment configuration value?\n"
+      unless $processor;
+
+    $payment_gateway = new FS::payment_gateway;
+
+    $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
+                                 'Business::OnlinePayment');
+    $payment_gateway->gateway_module($processor);
+    $payment_gateway->gateway_username($login);
+    $payment_gateway->gateway_password($password);
+    $payment_gateway->gateway_action($action);
+    $payment_gateway->set('options', [ @bop_options ]);
+
+  }
+
+  unless ( $payment_gateway->gateway_namespace ) {
+    $payment_gateway->gateway_namespace(
+      scalar($conf->config('business-onlinepayment-namespace'))
+      || 'Business::OnlinePayment'
+    );
+  }
+
+  $payment_gateway;
+}
+
+=item invoice_modes
+
+Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
+those with this agentnum or null agentnum).
+
+=cut
+
+sub invoice_modes {
+  my $self = shift;
+  qsearch( {
+      table     => 'invoice_mode',
+      hashref   => { agentnum => $self->agentnum },
+      extra_sql => ' OR agentnum IS NULL',
+      order_by  => ' ORDER BY modename',
+  } );
+}
+
 =item num_prospect_cust_main
 
 Returns the number of prospects (customers with no packages ever ordered) for
@@ -182,7 +401,7 @@ sub num_sql {
   my( $self, $sql ) = @_;
   my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
-  $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
+  $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
   $sth->fetchrow_arrayref->[0];
 }
 
@@ -206,9 +425,33 @@ sub cust_main_sql {
   );
 }
 
+=item num_ordered_cust_main
+
+Returns the number of ordered customers for this agent (customers with packages
+ordered, but not yet billed).
+
+=cut
+
+sub num_ordered_cust_main {
+  shift->num_sql(FS::cust_main->ordered_sql);
+}
+
+=item ordered_cust_main
+
+Returns the ordered customers for this agent (customers with packages ordered,
+but not yet billed), as cust_main objects.
+
+=cut
+
+sub ordered_cust_main {
+  shift->cust_main_sql(FS::cust_main->ordered_sql);
+}
+
+
 =item num_active_cust_main
 
-Returns the number of active customers for this agent.
+Returns the number of active customers for this agent (customers with active
+recurring packages).
 
 =cut
 
@@ -226,6 +469,28 @@ sub active_cust_main {
   shift->cust_main_sql(FS::cust_main->active_sql);
 }
 
+=item num_inactive_cust_main
+
+Returns the number of inactive customers for this agent (customers with no
+active recurring packages, but otherwise unsuspended/uncancelled).
+
+=cut
+
+sub num_inactive_cust_main {
+  shift->num_sql(FS::cust_main->inactive_sql);
+}
+
+=item inactive_cust_main
+
+Returns the inactive customers for this agent, as cust_main objects.
+
+=cut
+
+sub inactive_cust_main {
+  shift->cust_main_sql(FS::cust_main->inactive_sql);
+}
+
+
 =item num_susp_cust_main
 
 Returns the number of suspended customers for this agent.
@@ -286,6 +551,17 @@ sub num_pkg_sql {
   $sth->fetchrow_arrayref->[0];
 }
 
+=item num_inactive_cust_pkg
+
+Returns the number of inactive customer packages (one-time packages otherwise
+unsuspended/uncancelled) for this agent.
+
+=cut
+
+sub num_inactive_cust_pkg {
+  shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
+}
+
 =item num_susp_cust_pkg
 
 Returns the number of suspended customer packages for this agent.
@@ -306,6 +582,28 @@ sub num_cancel_cust_pkg {
   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
 }
 
+=item num_on_hold_cust_pkg
+
+Returns the number of inactive customer packages (one-time packages otherwise
+unsuspended/uncancelled) for this agent.
+
+=cut
+
+sub num_on_hold_cust_pkg {
+  shift->num_pkg_sql(FS::cust_pkg->on_hold_sql);
+}
+
+=item num_not_yet_billed_cust_pkg
+
+Returns the number of inactive customer packages (one-time packages otherwise
+unsuspended/uncancelled) for this agent.
+
+=cut
+
+sub num_not_yet_billed_cust_pkg {
+  shift->num_pkg_sql(FS::cust_pkg->not_yet_billed_sql);
+}
+
 =item generate_reg_codes NUM PKGPART_ARRAYREF
 
 Generates the specified number of registration codes, allowing purchase of the
@@ -381,6 +679,31 @@ sub num_prepay_credit {
   $sth->fetchrow_arrayref->[0];
 }
 
+=item num_sales
+
+Returns the number of non-disabled sales people for this agent.
+
+=cut
+
+sub num_sales {
+  my $self = shift;
+  my $sth = dbh->prepare(
+    "SELECT COUNT(*) FROM sales WHERE agentnum = ?
+                                  AND ( disabled = '' OR disabled IS NULL )"
+  ) or die dbh->errstr;
+  $sth->execute($self->agentnum) or die $sth->errstr;
+  $sth->fetchrow_arrayref->[0];
+}
+
+sub commission_where {
+  my $self = shift;
+  'cust_credit.commission_agentnum = ' . $self->agentnum;
+}
+
+sub sales_where {
+  my $self = shift;
+  'cust_main.agentnum = ' . $self->agentnum;
+}
 
 =back