4 use base qw( FS::Commission_Mixin FS::m2m_Common FS::Record );
6 use Business::CreditCard 0.28;
7 use FS::Record qw( dbh qsearch qsearchs );
17 FS::agent - Object methods for agent records
23 $record = new FS::agent \%hash;
24 $record = new FS::agent { 'column' => 'value' };
26 $error = $record->insert;
28 $error = $new_record->replace($old_record);
30 $error = $record->delete;
32 $error = $record->check;
34 $agent_type = $record->agent_type;
36 $hashref = $record->pkgpart_hashref;
37 #may purchase $pkgpart if $hashref->{$pkgpart};
41 An FS::agent object represents an agent. Every customer has an agent. Agents
42 can be used to track things like resellers or salespeople. FS::agent inherits
43 from FS::Record. The following fields are currently supported:
49 primary key (assigned automatically for new agents)
53 Text name of this agent
57 Agent type (see L<FS::agent_type>)
59 =item ticketing_queueid
63 =item invoice_template
69 Optional agent customer (see L<FS::cust_main>)
73 Disabled flag, empty or 'Y'
77 Deprecated (never used)
81 Deprecated (never used)
85 (Deprecated) Username for the Agent interface
89 (Deprecated) Password for the Agent interface
99 Creates a new agent. To add the agent to the database, see L<"insert">.
103 sub table { 'agent'; }
107 Adds this agent to the database. If there is an error, returns the error,
108 otherwise returns false.
112 Deletes this agent from the database. Only agents with no customers can be
113 deleted. If there is an error, returns the error, otherwise returns false.
120 return "Can't delete an agent with customers!"
121 if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
123 $self->SUPER::delete;
126 =item replace OLD_RECORD
128 Replaces OLD_RECORD with this one in the database. If there is an error,
129 returns the error, otherwise returns false.
133 Checks all fields to make sure this is a valid agent. If there is an error,
134 returns the error, otherwise returns false. Called by the insert and replace
143 $self->ut_numbern('agentnum')
144 || $self->ut_text('agent')
145 || $self->ut_number('typenum')
146 || $self->ut_numbern('freq')
147 || $self->ut_textn('prog')
148 || $self->ut_textn('invoice_template')
149 || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
150 || $self->ut_numbern('ticketing_queueid')
152 return $error if $error;
154 if ( $self->dbdef_table->column('disabled') ) {
155 $error = $self->ut_enum('disabled', [ '', 'Y' ] );
156 return $error if $error;
159 if ( $self->dbdef_table->column('username') ) {
160 $error = $self->ut_alphan('username');
161 return $error if $error;
162 if ( length($self->username) ) {
163 my $conflict = qsearchs('agent', { 'username' => $self->username } );
164 return 'duplicate agent username (with '. $conflict->agent. ')'
165 if $conflict && $conflict->agentnum != $self->agentnum;
166 $error = $self->ut_text('password'); # ut_text... arbitrary choice
168 $self->_password('');
172 return "Unknown typenum!"
173 unless $self->agent_type;
180 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
186 qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
189 =item agent_cust_main
191 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
196 sub agent_cust_main {
198 qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
201 =item pkgpart_hashref
203 Returns a hash reference. The keys of the hash are pkgparts. The value is
204 true if this agent may purchase the specified package definition. See
209 sub pkgpart_hashref {
211 $self->agent_type->pkgpart_hashref;
214 =item ticketing_queue
216 Returns the queue name corresponding with the id from the I<ticketing_queueid>
217 field, or the empty string.
221 sub ticketing_queue {
223 FS::TicketSystem->queue($self->ticketing_queueid);
226 =item payment_gateway [ OPTION => VALUE, ... ]
228 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
230 Currently available options are I<nofatal>, I<invnum>, I<method>,
231 I<payinfo>, and I<thirdparty>.
233 If I<nofatal> is set, and no gateway is available, then the empty string
234 will be returned instead of throwing a fatal exception.
236 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
237 an attempt will be made to select a gateway suited for the taxes paid on
240 The I<method> and I<payinfo> options can be used to influence the choice
241 as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
243 When the I<method> is 'CC' then the card number in I<payinfo> can direct
244 this routine to route to a gateway suited for that type of card.
246 If I<thirdparty> is set, the defined self-service payment gateway will
251 sub payment_gateway {
252 my ( $self, %options ) = @_;
254 my $conf = new FS::Conf;
256 if ( $options{thirdparty} ) {
257 # still a kludge, but it gets the job done
258 # and the 'cardtype' semantics don't really apply to thirdparty
259 # gateways because we have to choose a gateway without ever
260 # seeing the card number
262 $conf->config('selfservice-payment_gateway', $self->agentnum);
264 $gateway = FS::payment_gateway->by_key($gatewaynum) if $gatewaynum;
265 return $gateway if $gateway;
267 # a little less kludgey than the above, and allows PayPal to coexist
268 # with credit card gateways
269 my $is_paypal = { op => '!=', value => 'PayPal' };
270 if ( uc($options{method}) eq 'PAYPAL' ) {
271 $is_paypal = 'PayPal';
274 $gateway = qsearchs({
275 table => 'payment_gateway',
276 addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
278 gateway_namespace => 'Business::OnlineThirdPartyPayment',
279 gateway_module => $is_paypal,
282 extra_sql => ' AND agentnum = '.$self->agentnum,
287 } elsif ( $options{'nofatal'} ) {
290 die "no third-party gateway configured\n";
295 if ( $options{invnum} ) {
297 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
298 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
304 $cust_bill->cust_bill_pkg;
306 my @taxclasses = map $_->taxclass, @part_pkg;
308 $taxclass = $taxclasses[0]
309 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
310 #different taxclasses
313 #look for an agent gateway override first
315 if ( $options{method} ) {
316 if ( $options{method} eq 'CC' && $options{payinfo} ) {
317 $cardtype = cardtype($options{payinfo});
318 } elsif ( $options{method} eq 'ECHECK' ) {
321 $cardtype = $options{method}
326 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
327 cardtype => $cardtype,
328 taxclass => $taxclass, } )
329 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
331 taxclass => $taxclass, } )
332 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
333 cardtype => $cardtype,
335 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
340 if ( $override ) { #use a payment gateway override
342 $payment_gateway = $override->payment_gateway;
344 $payment_gateway->gateway_namespace('Business::OnlinePayment')
345 unless $payment_gateway->gateway_namespace;
347 } else { #use the standard settings from the config
349 # the standard settings from the config could be moved to a null agent
350 # agent_payment_gateway referenced payment_gateway
352 unless ( $conf->exists('business-onlinepayment') ) {
353 if ( $options{'nofatal'} ) {
356 die "Real-time processing not enabled\n";
361 my $bop_config = 'business-onlinepayment';
362 $bop_config .= '-ach'
363 if ( $options{method}
364 && $options{method} =~ /^(ECHECK|CHEK)$/
365 && $conf->exists($bop_config. '-ach')
367 my ( $processor, $login, $password, $action, @bop_options ) =
368 $conf->config($bop_config);
369 $action ||= 'normal authorization';
370 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
371 die "No real-time processor is enabled - ".
372 "did you set the business-onlinepayment configuration value?\n"
375 $payment_gateway = new FS::payment_gateway;
377 $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
378 'Business::OnlinePayment');
379 $payment_gateway->gateway_module($processor);
380 $payment_gateway->gateway_username($login);
381 $payment_gateway->gateway_password($password);
382 $payment_gateway->gateway_action($action);
383 $payment_gateway->set('options', [ @bop_options ]);
387 unless ( $payment_gateway->gateway_namespace ) {
388 $payment_gateway->gateway_namespace(
389 scalar($conf->config('business-onlinepayment-namespace'))
390 || 'Business::OnlinePayment'
399 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
400 those with this agentnum or null agentnum).
407 table => 'invoice_mode',
408 hashref => { agentnum => $self->agentnum },
409 extra_sql => ' OR agentnum IS NULL',
410 order_by => ' ORDER BY modename',
414 =item num_prospect_cust_main
416 Returns the number of prospects (customers with no packages ever ordered) for
421 sub num_prospect_cust_main {
422 shift->num_sql(FS::cust_main->prospect_sql);
426 my( $self, $sql ) = @_;
427 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
428 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
429 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
430 $sth->fetchrow_arrayref->[0];
433 =item prospect_cust_main
435 Returns the prospects (customers with no packages ever ordered) for this agent,
436 as cust_main objects.
440 sub prospect_cust_main {
441 shift->cust_main_sql(FS::cust_main->prospect_sql);
445 my( $self, $sql ) = @_;
446 qsearch( 'cust_main',
447 { 'agentnum' => $self->agentnum },
453 =item num_ordered_cust_main
455 Returns the number of ordered customers for this agent (customers with packages
456 ordered, but not yet billed).
460 sub num_ordered_cust_main {
461 shift->num_sql(FS::cust_main->ordered_sql);
464 =item ordered_cust_main
466 Returns the ordered customers for this agent (customers with packages ordered,
467 but not yet billed), as cust_main objects.
471 sub ordered_cust_main {
472 shift->cust_main_sql(FS::cust_main->ordered_sql);
476 =item num_active_cust_main
478 Returns the number of active customers for this agent (customers with active
483 sub num_active_cust_main {
484 shift->num_sql(FS::cust_main->active_sql);
487 =item active_cust_main
489 Returns the active customers for this agent, as cust_main objects.
493 sub active_cust_main {
494 shift->cust_main_sql(FS::cust_main->active_sql);
497 =item num_inactive_cust_main
499 Returns the number of inactive customers for this agent (customers with no
500 active recurring packages, but otherwise unsuspended/uncancelled).
504 sub num_inactive_cust_main {
505 shift->num_sql(FS::cust_main->inactive_sql);
508 =item inactive_cust_main
510 Returns the inactive customers for this agent, as cust_main objects.
514 sub inactive_cust_main {
515 shift->cust_main_sql(FS::cust_main->inactive_sql);
519 =item num_susp_cust_main
521 Returns the number of suspended customers for this agent.
525 sub num_susp_cust_main {
526 shift->num_sql(FS::cust_main->susp_sql);
531 Returns the suspended customers for this agent, as cust_main objects.
536 shift->cust_main_sql(FS::cust_main->susp_sql);
539 =item num_cancel_cust_main
541 Returns the number of cancelled customer for this agent.
545 sub num_cancel_cust_main {
546 shift->num_sql(FS::cust_main->cancel_sql);
549 =item cancel_cust_main
551 Returns the cancelled customers for this agent, as cust_main objects.
555 sub cancel_cust_main {
556 shift->cust_main_sql(FS::cust_main->cancel_sql);
559 =item num_active_cust_pkg
561 Returns the number of active customer packages for this agent.
565 sub num_active_cust_pkg {
566 shift->num_pkg_sql(FS::cust_pkg->active_sql);
570 my( $self, $sql ) = @_;
572 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
573 " WHERE agentnum = ? AND $sql";
574 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
575 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
576 $sth->fetchrow_arrayref->[0];
579 =item num_inactive_cust_pkg
581 Returns the number of inactive customer packages (one-time packages otherwise
582 unsuspended/uncancelled) for this agent.
586 sub num_inactive_cust_pkg {
587 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
590 =item num_susp_cust_pkg
592 Returns the number of suspended customer packages for this agent.
596 sub num_susp_cust_pkg {
597 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
600 =item num_cancel_cust_pkg
602 Returns the number of cancelled customer packages for this agent.
606 sub num_cancel_cust_pkg {
607 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
610 =item num_on_hold_cust_pkg
612 Returns the number of inactive customer packages (one-time packages otherwise
613 unsuspended/uncancelled) for this agent.
617 sub num_on_hold_cust_pkg {
618 shift->num_pkg_sql(FS::cust_pkg->on_hold_sql);
621 =item num_not_yet_billed_cust_pkg
623 Returns the number of inactive customer packages (one-time packages otherwise
624 unsuspended/uncancelled) for this agent.
628 sub num_not_yet_billed_cust_pkg {
629 shift->num_pkg_sql(FS::cust_pkg->not_yet_billed_sql);
632 =item generate_reg_codes NUM PKGPART_ARRAYREF
634 Generates the specified number of registration codes, allowing purchase of the
635 specified package definitions. Returns an array reference of the newly
636 generated codes, or a scalar error message.
640 #false laziness w/prepay_credit::generate
641 sub generate_reg_codes {
642 my( $self, $num, $pkgparts ) = @_;
644 my @codeset = ( 'A'..'Z' );
646 local $SIG{HUP} = 'IGNORE';
647 local $SIG{INT} = 'IGNORE';
648 local $SIG{QUIT} = 'IGNORE';
649 local $SIG{TERM} = 'IGNORE';
650 local $SIG{TSTP} = 'IGNORE';
651 local $SIG{PIPE} = 'IGNORE';
653 my $oldAutoCommit = $FS::UID::AutoCommit;
654 local $FS::UID::AutoCommit = 0;
659 my $reg_code = new FS::reg_code {
660 'agentnum' => $self->agentnum,
661 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
663 my $error = $reg_code->insert($pkgparts);
665 $dbh->rollback if $oldAutoCommit;
668 push @codes, $reg_code->code;
671 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
679 Returns the number of unused registration codes for this agent.
685 my $sth = dbh->prepare(
686 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
687 ) or die dbh->errstr;
688 $sth->execute($self->agentnum) or die $sth->errstr;
689 $sth->fetchrow_arrayref->[0];
692 =item num_prepay_credit
694 Returns the number of unused prepaid cards for this agent.
698 sub num_prepay_credit {
700 my $sth = dbh->prepare(
701 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
702 ) or die dbh->errstr;
703 $sth->execute($self->agentnum) or die $sth->errstr;
704 $sth->fetchrow_arrayref->[0];
709 Returns the number of non-disabled sales people for this agent.
715 my $sth = dbh->prepare(
716 "SELECT COUNT(*) FROM sales WHERE agentnum = ?
717 AND ( disabled = '' OR disabled IS NULL )"
718 ) or die dbh->errstr;
719 $sth->execute($self->agentnum) or die $sth->errstr;
720 $sth->fetchrow_arrayref->[0];
723 sub commission_where {
725 'cust_credit.commission_agentnum = ' . $self->agentnum;
730 'cust_main.agentnum = ' . $self->agentnum;
739 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
740 schema.html from the base documentation.