6 use Business::CreditCard 0.28;
7 use FS::Record qw( dbh qsearch qsearchs );
15 @ISA = qw( FS::m2m_Common FS::Record );
19 FS::agent - Object methods for agent records
25 $record = new FS::agent \%hash;
26 $record = new FS::agent { 'column' => 'value' };
28 $error = $record->insert;
30 $error = $new_record->replace($old_record);
32 $error = $record->delete;
34 $error = $record->check;
36 $agent_type = $record->agent_type;
38 $hashref = $record->pkgpart_hashref;
39 #may purchase $pkgpart if $hashref->{$pkgpart};
43 An FS::agent object represents an agent. Every customer has an agent. Agents
44 can be used to track things like resellers or salespeople. FS::agent inherits
45 from FS::Record. The following fields are currently supported:
49 =item agentnum - primary key (assigned automatically for new agents)
51 =item agent - Text name of this agent
53 =item typenum - Agent type (see L<FS::agent_type>)
55 =item ticketing_queueid - Ticketing Queue
57 =item invoice_template - Invoice template name
59 =item agent_custnum - Optional agent customer (see L<FS::cust_main>)
61 =item disabled - Disabled flag, empty or 'Y'
63 =item prog - Deprecated (never used)
65 =item freq - Deprecated (never used)
67 =item username - (Deprecated) Username for the Agent interface
69 =item _password - (Deprecated) Password for the Agent interface
79 Creates a new agent. To add the agent to the database, see L<"insert">.
83 sub table { 'agent'; }
87 Adds this agent to the database. If there is an error, returns the error,
88 otherwise returns false.
92 Deletes this agent from the database. Only agents with no customers can be
93 deleted. If there is an error, returns the error, otherwise returns false.
100 return "Can't delete an agent with customers!"
101 if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
103 $self->SUPER::delete;
106 =item replace OLD_RECORD
108 Replaces OLD_RECORD with this one in the database. If there is an error,
109 returns the error, otherwise returns false.
113 Checks all fields to make sure this is a valid agent. If there is an error,
114 returns the error, otherwise returns false. Called by the insert and replace
123 $self->ut_numbern('agentnum')
124 || $self->ut_text('agent')
125 || $self->ut_number('typenum')
126 || $self->ut_numbern('freq')
127 || $self->ut_textn('prog')
128 || $self->ut_textn('invoice_template')
129 || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
131 return $error if $error;
133 if ( $self->dbdef_table->column('disabled') ) {
134 $error = $self->ut_enum('disabled', [ '', 'Y' ] );
135 return $error if $error;
138 if ( $self->dbdef_table->column('username') ) {
139 $error = $self->ut_alphan('username');
140 return $error if $error;
141 if ( length($self->username) ) {
142 my $conflict = qsearchs('agent', { 'username' => $self->username } );
143 return 'duplicate agent username (with '. $conflict->agent. ')'
144 if $conflict && $conflict->agentnum != $self->agentnum;
145 $error = $self->ut_text('password'); # ut_text... arbitrary choice
147 $self->_password('');
151 return "Unknown typenum!"
152 unless $self->agent_type;
159 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
165 qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
168 =item agent_cust_main
170 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
175 sub agent_cust_main {
177 qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
180 =item pkgpart_hashref
182 Returns a hash reference. The keys of the hash are pkgparts. The value is
183 true if this agent may purchase the specified package definition. See
188 sub pkgpart_hashref {
190 $self->agent_type->pkgpart_hashref;
193 =item ticketing_queue
195 Returns the queue name corresponding with the id from the I<ticketing_queueid>
196 field, or the empty string.
200 sub ticketing_queue {
202 FS::TicketSystem->queue($self->ticketing_queueid);
205 =item payment_gateway [ OPTION => VALUE, ... ]
207 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
209 Currently available options are I<nofatal>, I<invnum>, I<method>,
210 I<payinfo>, and I<thirdparty>.
212 If I<nofatal> is set, and no gateway is available, then the empty string
213 will be returned instead of throwing a fatal exception.
215 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
216 an attempt will be made to select a gateway suited for the taxes paid on
219 The I<method> and I<payinfo> options can be used to influence the choice
220 as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
222 When the I<method> is 'CC' then the card number in I<payinfo> can direct
223 this routine to route to a gateway suited for that type of card.
225 If I<thirdparty> is set, the defined self-service payment gateway will
230 sub payment_gateway {
231 my ( $self, %options ) = @_;
233 my $conf = new FS::Conf;
235 if ( $options{thirdparty} ) {
236 # still a kludge, but it gets the job done
237 # and the 'cardtype' semantics don't really apply to thirdparty
238 # gateways because we have to choose a gateway without ever
239 # seeing the card number
241 $conf->config('selfservice-payment_gateway', $self->agentnum);
242 my $gateway = FS::payment_gateway->by_key($gatewaynum)
247 } elsif ( $options{'nofatal'} ) {
250 die "no third-party gateway configured\n";
255 if ( $options{invnum} ) {
257 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
258 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
264 $cust_bill->cust_bill_pkg;
266 my @taxclasses = map $_->taxclass, @part_pkg;
268 $taxclass = $taxclasses[0]
269 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
270 #different taxclasses
273 #look for an agent gateway override first
275 if ( $options{method} ) {
276 if ( $options{method} eq 'CC' && $options{payinfo} ) {
277 $cardtype = cardtype($options{payinfo});
278 } elsif ( $options{method} eq 'ECHECK' ) {
281 $cardtype = $options{method}
286 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
287 cardtype => $cardtype,
288 taxclass => $taxclass, } )
289 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
291 taxclass => $taxclass, } )
292 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
293 cardtype => $cardtype,
295 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
300 if ( $override ) { #use a payment gateway override
302 $payment_gateway = $override->payment_gateway;
304 $payment_gateway->gateway_namespace('Business::OnlinePayment')
305 unless $payment_gateway->gateway_namespace;
307 } else { #use the standard settings from the config
309 # the standard settings from the config could be moved to a null agent
310 # agent_payment_gateway referenced payment_gateway
312 unless ( $conf->exists('business-onlinepayment') ) {
313 if ( $options{'nofatal'} ) {
316 die "Real-time processing not enabled\n";
321 my $bop_config = 'business-onlinepayment';
322 $bop_config .= '-ach'
323 if ( $options{method}
324 && $options{method} =~ /^(ECHECK|CHEK)$/
325 && $conf->exists($bop_config. '-ach')
327 my ( $processor, $login, $password, $action, @bop_options ) =
328 $conf->config($bop_config);
329 $action ||= 'normal authorization';
330 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
331 die "No real-time processor is enabled - ".
332 "did you set the business-onlinepayment configuration value?\n"
335 $payment_gateway = new FS::payment_gateway;
337 $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
338 'Business::OnlinePayment');
339 $payment_gateway->gateway_module($processor);
340 $payment_gateway->gateway_username($login);
341 $payment_gateway->gateway_password($password);
342 $payment_gateway->gateway_action($action);
343 $payment_gateway->set('options', [ @bop_options ]);
347 unless ( $payment_gateway->gateway_namespace ) {
348 $payment_gateway->gateway_namespace(
349 scalar($conf->config('business-onlinepayment-namespace'))
350 || 'Business::OnlinePayment'
359 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
360 those with this agentnum or null agentnum).
367 table => 'invoice_mode',
368 hashref => { agentnum => $self->agentnum },
369 extra_sql => ' OR agentnum IS NULL',
370 order_by => ' ORDER BY modename',
374 =item num_prospect_cust_main
376 Returns the number of prospects (customers with no packages ever ordered) for
381 sub num_prospect_cust_main {
382 shift->num_sql(FS::cust_main->prospect_sql);
386 my( $self, $sql ) = @_;
387 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
388 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
389 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
390 $sth->fetchrow_arrayref->[0];
393 =item prospect_cust_main
395 Returns the prospects (customers with no packages ever ordered) for this agent,
396 as cust_main objects.
400 sub prospect_cust_main {
401 shift->cust_main_sql(FS::cust_main->prospect_sql);
405 my( $self, $sql ) = @_;
406 qsearch( 'cust_main',
407 { 'agentnum' => $self->agentnum },
413 =item num_ordered_cust_main
415 Returns the number of ordered customers for this agent (customers with packages
416 ordered, but not yet billed).
420 sub num_ordered_cust_main {
421 shift->num_sql(FS::cust_main->ordered_sql);
424 =item ordered_cust_main
426 Returns the ordered customers for this agent (customers with packages ordered,
427 but not yet billed), as cust_main objects.
431 sub ordered_cust_main {
432 shift->cust_main_sql(FS::cust_main->ordered_sql);
436 =item num_active_cust_main
438 Returns the number of active customers for this agent (customers with active
443 sub num_active_cust_main {
444 shift->num_sql(FS::cust_main->active_sql);
447 =item active_cust_main
449 Returns the active customers for this agent, as cust_main objects.
453 sub active_cust_main {
454 shift->cust_main_sql(FS::cust_main->active_sql);
457 =item num_inactive_cust_main
459 Returns the number of inactive customers for this agent (customers with no
460 active recurring packages, but otherwise unsuspended/uncancelled).
464 sub num_inactive_cust_main {
465 shift->num_sql(FS::cust_main->inactive_sql);
468 =item inactive_cust_main
470 Returns the inactive customers for this agent, as cust_main objects.
474 sub inactive_cust_main {
475 shift->cust_main_sql(FS::cust_main->inactive_sql);
479 =item num_susp_cust_main
481 Returns the number of suspended customers for this agent.
485 sub num_susp_cust_main {
486 shift->num_sql(FS::cust_main->susp_sql);
491 Returns the suspended customers for this agent, as cust_main objects.
496 shift->cust_main_sql(FS::cust_main->susp_sql);
499 =item num_cancel_cust_main
501 Returns the number of cancelled customer for this agent.
505 sub num_cancel_cust_main {
506 shift->num_sql(FS::cust_main->cancel_sql);
509 =item cancel_cust_main
511 Returns the cancelled customers for this agent, as cust_main objects.
515 sub cancel_cust_main {
516 shift->cust_main_sql(FS::cust_main->cancel_sql);
519 =item num_active_cust_pkg
521 Returns the number of active customer packages for this agent.
525 sub num_active_cust_pkg {
526 shift->num_pkg_sql(FS::cust_pkg->active_sql);
530 my( $self, $sql ) = @_;
532 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
533 " WHERE agentnum = ? AND $sql";
534 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
535 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
536 $sth->fetchrow_arrayref->[0];
539 =item num_inactive_cust_pkg
541 Returns the number of inactive customer packages (one-time packages otherwise
542 unsuspended/uncancelled) for this agent.
546 sub num_inactive_cust_pkg {
547 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
550 =item num_susp_cust_pkg
552 Returns the number of suspended customer packages for this agent.
556 sub num_susp_cust_pkg {
557 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
560 =item num_cancel_cust_pkg
562 Returns the number of cancelled customer packages for this agent.
566 sub num_cancel_cust_pkg {
567 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
570 =item generate_reg_codes NUM PKGPART_ARRAYREF
572 Generates the specified number of registration codes, allowing purchase of the
573 specified package definitions. Returns an array reference of the newly
574 generated codes, or a scalar error message.
578 #false laziness w/prepay_credit::generate
579 sub generate_reg_codes {
580 my( $self, $num, $pkgparts ) = @_;
582 my @codeset = ( 'A'..'Z' );
584 local $SIG{HUP} = 'IGNORE';
585 local $SIG{INT} = 'IGNORE';
586 local $SIG{QUIT} = 'IGNORE';
587 local $SIG{TERM} = 'IGNORE';
588 local $SIG{TSTP} = 'IGNORE';
589 local $SIG{PIPE} = 'IGNORE';
591 my $oldAutoCommit = $FS::UID::AutoCommit;
592 local $FS::UID::AutoCommit = 0;
597 my $reg_code = new FS::reg_code {
598 'agentnum' => $self->agentnum,
599 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
601 my $error = $reg_code->insert($pkgparts);
603 $dbh->rollback if $oldAutoCommit;
606 push @codes, $reg_code->code;
609 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
617 Returns the number of unused registration codes for this agent.
623 my $sth = dbh->prepare(
624 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
625 ) or die dbh->errstr;
626 $sth->execute($self->agentnum) or die $sth->errstr;
627 $sth->fetchrow_arrayref->[0];
630 =item num_prepay_credit
632 Returns the number of unused prepaid cards for this agent.
636 sub num_prepay_credit {
638 my $sth = dbh->prepare(
639 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
640 ) or die dbh->errstr;
641 $sth->execute($self->agentnum) or die $sth->errstr;
642 $sth->fetchrow_arrayref->[0];
647 Returns the number of non-disabled sales people for this agent.
653 my $sth = dbh->prepare(
654 "SELECT COUNT(*) FROM sales WHERE agentnum = ?
655 AND ( disabled = '' OR disabled IS NULL )"
656 ) or die dbh->errstr;
657 $sth->execute($self->agentnum) or die $sth->errstr;
658 $sth->fetchrow_arrayref->[0];
667 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
668 schema.html from the base documentation.