2 use base qw( FS::m2m_Common FS::m2name_Common FS::Record );
6 use Business::CreditCard 0.28;
7 use FS::Record qw( dbh qsearch qsearchs );
11 use FS::agent_currency;
18 FS::agent - Object methods for agent records
24 $record = new FS::agent \%hash;
25 $record = new FS::agent { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
35 $agent_type = $record->agent_type;
37 $hashref = $record->pkgpart_hashref;
38 #may purchase $pkgpart if $hashref->{$pkgpart};
42 An FS::agent object represents an agent. Every customer has an agent. Agents
43 can be used to track things like resellers or salespeople. FS::agent inherits
44 from FS::Record. The following fields are currently supported:
48 =item agentnum - primary key (assigned automatically for new agents)
50 =item agent - Text name of this agent
52 =item typenum - Agent type (see L<FS::agent_type>)
54 =item ticketing_queueid - Ticketing Queue
56 =item invoice_template - Invoice template name
58 =item agent_custnum - Optional agent customer (see L<FS::cust_main>)
60 =item disabled - Disabled flag, empty or 'Y'
62 =item prog - Deprecated (never used)
64 =item freq - Deprecated (never used)
66 =item username - (Deprecated) Username for the Agent interface
68 =item _password - (Deprecated) Password for the Agent interface
78 Creates a new agent. To add the agent to the database, see L<"insert">.
82 sub table { 'agent'; }
86 Adds this agent to the database. If there is an error, returns the error,
87 otherwise returns false.
91 Deletes this agent from the database. Only agents with no customers can be
92 deleted. If there is an error, returns the error, otherwise returns false.
99 return "Can't delete an agent with customers!"
100 if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
102 $self->SUPER::delete;
105 =item replace OLD_RECORD
107 Replaces OLD_RECORD with this one in the database. If there is an error,
108 returns the error, otherwise returns false.
112 Checks all fields to make sure this is a valid agent. If there is an error,
113 returns the error, otherwise returns false. Called by the insert and replace
122 $self->ut_numbern('agentnum')
123 || $self->ut_text('agent')
124 || $self->ut_number('typenum')
125 || $self->ut_numbern('freq')
126 || $self->ut_textn('prog')
127 || $self->ut_textn('invoice_template')
128 || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
130 return $error if $error;
132 if ( $self->dbdef_table->column('disabled') ) {
133 $error = $self->ut_enum('disabled', [ '', 'Y' ] );
134 return $error if $error;
137 if ( $self->dbdef_table->column('username') ) {
138 $error = $self->ut_alphan('username');
139 return $error if $error;
140 if ( length($self->username) ) {
141 my $conflict = qsearchs('agent', { 'username' => $self->username } );
142 return 'duplicate agent username (with '. $conflict->agent. ')'
143 if $conflict && $conflict->agentnum != $self->agentnum;
144 $error = $self->ut_text('password'); # ut_text... arbitrary choice
146 $self->_password('');
150 return "Unknown typenum!"
151 unless $self->agent_type;
158 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
164 qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
167 =item agent_cust_main
169 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
174 sub agent_cust_main {
176 qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
181 Returns the FS::agent_currency objects (see L<FS::agent_currency>), if any, for
188 qsearch('agent_currency', { 'agentnum' => $self->agentnum } );
191 =item agent_currency_hashref
193 Returns a hash references of supported additional currencies for this agent.
197 sub agent_currency_hashref {
199 +{ map { $_->currency => 1 }
200 $self->agent_currency
204 =item pkgpart_hashref
206 Returns a hash reference. The keys of the hash are pkgparts. The value is
207 true if this agent may purchase the specified package definition. See
212 sub pkgpart_hashref {
214 $self->agent_type->pkgpart_hashref;
217 =item ticketing_queue
219 Returns the queue name corresponding with the id from the I<ticketing_queueid>
220 field, or the empty string.
224 sub ticketing_queue {
226 FS::TicketSystem->queue($self->ticketing_queueid);
229 =item payment_gateway [ OPTION => VALUE, ... ]
231 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
233 Currently available options are I<nofatal>, I<invnum>, I<method>,
234 I<payinfo>, and I<thirdparty>.
236 If I<nofatal> is set, and no gateway is available, then the empty string
237 will be returned instead of throwing a fatal exception.
239 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
240 an attempt will be made to select a gateway suited for the taxes paid on
243 The I<method> and I<payinfo> options can be used to influence the choice
244 as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
246 When the I<method> is 'CC' then the card number in I<payinfo> can direct
247 this routine to route to a gateway suited for that type of card.
249 If I<thirdparty> is set, the defined self-service payment gateway will
254 sub payment_gateway {
255 my ( $self, %options ) = @_;
257 my $conf = new FS::Conf;
259 if ( $options{thirdparty} ) {
260 # still a kludge, but it gets the job done
261 # and the 'cardtype' semantics don't really apply to thirdparty
262 # gateways because we have to choose a gateway without ever
263 # seeing the card number
265 $conf->config('selfservice-payment_gateway', $self->agentnum);
266 my $gateway = FS::payment_gateway->by_key($gatewaynum)
271 } elsif ( $options{'nofatal'} ) {
274 die "no third-party gateway configured\n";
279 if ( $options{invnum} ) {
281 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
282 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
288 $cust_bill->cust_bill_pkg;
290 my @taxclasses = map $_->taxclass, @part_pkg;
292 $taxclass = $taxclasses[0]
293 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
294 #different taxclasses
297 #look for an agent gateway override first
299 if ( $options{method} ) {
300 if ( $options{method} eq 'CC' && $options{payinfo} ) {
301 $cardtype = cardtype($options{payinfo});
302 } elsif ( $options{method} eq 'ECHECK' ) {
305 $cardtype = $options{method}
310 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
311 cardtype => $cardtype,
312 taxclass => $taxclass, } )
313 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
315 taxclass => $taxclass, } )
316 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
317 cardtype => $cardtype,
319 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
324 if ( $override ) { #use a payment gateway override
326 $payment_gateway = $override->payment_gateway;
328 $payment_gateway->gateway_namespace('Business::OnlinePayment')
329 unless $payment_gateway->gateway_namespace;
331 } else { #use the standard settings from the config
333 # the standard settings from the config could be moved to a null agent
334 # agent_payment_gateway referenced payment_gateway
336 unless ( $conf->exists('business-onlinepayment') ) {
337 if ( $options{'nofatal'} ) {
340 die "Real-time processing not enabled\n";
345 my $bop_config = 'business-onlinepayment';
346 $bop_config .= '-ach'
347 if ( $options{method}
348 && $options{method} =~ /^(ECHECK|CHEK)$/
349 && $conf->exists($bop_config. '-ach')
351 my ( $processor, $login, $password, $action, @bop_options ) =
352 $conf->config($bop_config);
353 $action ||= 'normal authorization';
354 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
355 die "No real-time processor is enabled - ".
356 "did you set the business-onlinepayment configuration value?\n"
359 $payment_gateway = new FS::payment_gateway;
361 $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
362 'Business::OnlinePayment');
363 $payment_gateway->gateway_module($processor);
364 $payment_gateway->gateway_username($login);
365 $payment_gateway->gateway_password($password);
366 $payment_gateway->gateway_action($action);
367 $payment_gateway->set('options', [ @bop_options ]);
371 unless ( $payment_gateway->gateway_namespace ) {
372 $payment_gateway->gateway_namespace(
373 scalar($conf->config('business-onlinepayment-namespace'))
374 || 'Business::OnlinePayment'
383 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
384 those with this agentnum or null agentnum).
391 table => 'invoice_mode',
392 hashref => { agentnum => $self->agentnum },
393 extra_sql => ' OR agentnum IS NULL',
394 order_by => ' ORDER BY modename',
398 =item num_prospect_cust_main
400 Returns the number of prospects (customers with no packages ever ordered) for
405 sub num_prospect_cust_main {
406 shift->num_sql(FS::cust_main->prospect_sql);
410 my( $self, $sql ) = @_;
411 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
412 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
413 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
414 $sth->fetchrow_arrayref->[0];
417 =item prospect_cust_main
419 Returns the prospects (customers with no packages ever ordered) for this agent,
420 as cust_main objects.
424 sub prospect_cust_main {
425 shift->cust_main_sql(FS::cust_main->prospect_sql);
429 my( $self, $sql ) = @_;
430 qsearch( 'cust_main',
431 { 'agentnum' => $self->agentnum },
437 =item num_active_cust_main
439 Returns the number of active customers for this agent (customers with active
444 sub num_active_cust_main {
445 shift->num_sql(FS::cust_main->active_sql);
448 =item active_cust_main
450 Returns the active customers for this agent, as cust_main objects.
454 sub active_cust_main {
455 shift->cust_main_sql(FS::cust_main->active_sql);
458 =item num_inactive_cust_main
460 Returns the number of inactive customers for this agent (customers with no
461 active recurring packages, but otherwise unsuspended/uncancelled).
465 sub num_inactive_cust_main {
466 shift->num_sql(FS::cust_main->inactive_sql);
469 =item inactive_cust_main
471 Returns the inactive customers for this agent, as cust_main objects.
475 sub inactive_cust_main {
476 shift->cust_main_sql(FS::cust_main->inactive_sql);
480 =item num_susp_cust_main
482 Returns the number of suspended customers for this agent.
486 sub num_susp_cust_main {
487 shift->num_sql(FS::cust_main->susp_sql);
492 Returns the suspended customers for this agent, as cust_main objects.
497 shift->cust_main_sql(FS::cust_main->susp_sql);
500 =item num_cancel_cust_main
502 Returns the number of cancelled customer for this agent.
506 sub num_cancel_cust_main {
507 shift->num_sql(FS::cust_main->cancel_sql);
510 =item cancel_cust_main
512 Returns the cancelled customers for this agent, as cust_main objects.
516 sub cancel_cust_main {
517 shift->cust_main_sql(FS::cust_main->cancel_sql);
520 =item num_active_cust_pkg
522 Returns the number of active customer packages for this agent.
526 sub num_active_cust_pkg {
527 shift->num_pkg_sql(FS::cust_pkg->active_sql);
531 my( $self, $sql ) = @_;
533 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
534 " WHERE agentnum = ? AND $sql";
535 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
536 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
537 $sth->fetchrow_arrayref->[0];
540 =item num_inactive_cust_pkg
542 Returns the number of inactive customer packages (one-time packages otherwise
543 unsuspended/uncancelled) for this agent.
547 sub num_inactive_cust_pkg {
548 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
551 =item num_susp_cust_pkg
553 Returns the number of suspended customer packages for this agent.
557 sub num_susp_cust_pkg {
558 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
561 =item num_cancel_cust_pkg
563 Returns the number of cancelled customer packages for this agent.
567 sub num_cancel_cust_pkg {
568 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
571 =item generate_reg_codes NUM PKGPART_ARRAYREF
573 Generates the specified number of registration codes, allowing purchase of the
574 specified package definitions. Returns an array reference of the newly
575 generated codes, or a scalar error message.
579 #false laziness w/prepay_credit::generate
580 sub generate_reg_codes {
581 my( $self, $num, $pkgparts ) = @_;
583 my @codeset = ( 'A'..'Z' );
585 local $SIG{HUP} = 'IGNORE';
586 local $SIG{INT} = 'IGNORE';
587 local $SIG{QUIT} = 'IGNORE';
588 local $SIG{TERM} = 'IGNORE';
589 local $SIG{TSTP} = 'IGNORE';
590 local $SIG{PIPE} = 'IGNORE';
592 my $oldAutoCommit = $FS::UID::AutoCommit;
593 local $FS::UID::AutoCommit = 0;
598 my $reg_code = new FS::reg_code {
599 'agentnum' => $self->agentnum,
600 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
602 my $error = $reg_code->insert($pkgparts);
604 $dbh->rollback if $oldAutoCommit;
607 push @codes, $reg_code->code;
610 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
618 Returns the number of unused registration codes for this agent.
624 my $sth = dbh->prepare(
625 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
626 ) or die dbh->errstr;
627 $sth->execute($self->agentnum) or die $sth->errstr;
628 $sth->fetchrow_arrayref->[0];
631 =item num_prepay_credit
633 Returns the number of unused prepaid cards for this agent.
637 sub num_prepay_credit {
639 my $sth = dbh->prepare(
640 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
641 ) or die dbh->errstr;
642 $sth->execute($self->agentnum) or die $sth->errstr;
643 $sth->fetchrow_arrayref->[0];
648 Returns the number of non-disabled sales people for this agent.
654 my $sth = dbh->prepare(
655 "SELECT COUNT(*) FROM sales WHERE agentnum = ?
656 AND ( disabled = '' OR disabled IS NULL )"
657 ) or die dbh->errstr;
658 $sth->execute($self->agentnum) or die $sth->errstr;
659 $sth->fetchrow_arrayref->[0];
668 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
669 schema.html from the base documentation.