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_payment_gateway;
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:
47 =item agentnum - primary key (assigned automatically for new agents)
49 =item agent - Text name of this agent
51 =item typenum - Agent type (see L<FS::agent_type>)
53 =item ticketing_queueid - Ticketing Queue
55 =item invoice_template - Invoice template name
57 =item agent_custnum - Optional agent customer (see L<FS::cust_main>)
59 =item disabled - Disabled flag, empty or 'Y'
61 =item prog - Deprecated (never used)
63 =item freq - Deprecated (never used)
65 =item username - (Deprecated) Username for the Agent interface
67 =item _password - (Deprecated) Password for the Agent interface
77 Creates a new agent. To add the agent to the database, see L<"insert">.
81 sub table { 'agent'; }
85 Adds this agent to the database. If there is an error, returns the error,
86 otherwise returns false.
90 Deletes this agent from the database. Only agents with no customers can be
91 deleted. If there is an error, returns the error, otherwise returns false.
98 return "Can't delete an agent with customers!"
99 if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
101 $self->SUPER::delete;
104 =item replace OLD_RECORD
106 Replaces OLD_RECORD with this one in the database. If there is an error,
107 returns the error, otherwise returns false.
111 Checks all fields to make sure this is a valid agent. If there is an error,
112 returns the error, otherwise returns false. Called by the insert and replace
121 $self->ut_numbern('agentnum')
122 || $self->ut_text('agent')
123 || $self->ut_number('typenum')
124 || $self->ut_numbern('freq')
125 || $self->ut_textn('prog')
126 || $self->ut_textn('invoice_template')
127 || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
129 return $error if $error;
131 if ( $self->dbdef_table->column('disabled') ) {
132 $error = $self->ut_enum('disabled', [ '', 'Y' ] );
133 return $error if $error;
136 if ( $self->dbdef_table->column('username') ) {
137 $error = $self->ut_alphan('username');
138 return $error if $error;
139 if ( length($self->username) ) {
140 my $conflict = qsearchs('agent', { 'username' => $self->username } );
141 return 'duplicate agent username (with '. $conflict->agent. ')'
142 if $conflict && $conflict->agentnum != $self->agentnum;
143 $error = $self->ut_text('password'); # ut_text... arbitrary choice
145 $self->_password('');
149 return "Unknown typenum!"
150 unless $self->agent_type;
157 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
159 =item agent_cust_main
161 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
166 sub agent_cust_main {
168 qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
173 Returns the FS::agent_currency objects (see L<FS::agent_currency>), if any, for
176 =item agent_currency_hashref
178 Returns a hash references of supported additional currencies for this agent.
182 sub agent_currency_hashref {
184 +{ map { $_->currency => 1 }
185 $self->agent_currency
189 =item pkgpart_hashref
191 Returns a hash reference. The keys of the hash are pkgparts. The value is
192 true if this agent may purchase the specified package definition. See
197 sub pkgpart_hashref {
199 $self->agent_type->pkgpart_hashref;
202 =item ticketing_queue
204 Returns the queue name corresponding with the id from the I<ticketing_queueid>
205 field, or the empty string.
209 sub ticketing_queue {
211 FS::TicketSystem->queue($self->ticketing_queueid);
214 =item payment_gateway [ OPTION => VALUE, ... ]
216 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
218 Currently available options are I<nofatal>, I<invnum>, I<method>,
219 I<payinfo>, and I<thirdparty>.
221 If I<nofatal> is set, and no gateway is available, then the empty string
222 will be returned instead of throwing a fatal exception.
224 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
225 an attempt will be made to select a gateway suited for the taxes paid on
228 The I<method> and I<payinfo> options can be used to influence the choice
229 as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
231 When the I<method> is 'CC' then the card number in I<payinfo> can direct
232 this routine to route to a gateway suited for that type of card.
234 If I<thirdparty> is set, the defined self-service payment gateway will
239 sub payment_gateway {
240 my ( $self, %options ) = @_;
242 my $conf = new FS::Conf;
244 if ( $options{thirdparty} ) {
245 # still a kludge, but it gets the job done
246 # and the 'cardtype' semantics don't really apply to thirdparty
247 # gateways because we have to choose a gateway without ever
248 # seeing the card number
250 $conf->config('selfservice-payment_gateway', $self->agentnum);
251 my $gateway = FS::payment_gateway->by_key($gatewaynum)
256 } elsif ( $options{'nofatal'} ) {
259 die "no third-party gateway configured\n";
264 if ( $options{invnum} ) {
266 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
267 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
273 $cust_bill->cust_bill_pkg;
275 my @taxclasses = map $_->taxclass, @part_pkg;
277 $taxclass = $taxclasses[0]
278 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
279 #different taxclasses
282 #look for an agent gateway override first
284 if ( $options{method} ) {
285 if ( $options{method} eq 'CC' && $options{payinfo} ) {
286 $cardtype = cardtype($options{payinfo});
287 } elsif ( $options{method} eq 'ECHECK' ) {
290 $cardtype = $options{method}
295 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
296 cardtype => $cardtype,
297 taxclass => $taxclass, } )
298 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
300 taxclass => $taxclass, } )
301 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
302 cardtype => $cardtype,
304 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
309 if ( $override ) { #use a payment gateway override
311 $payment_gateway = $override->payment_gateway;
313 $payment_gateway->gateway_namespace('Business::OnlinePayment')
314 unless $payment_gateway->gateway_namespace;
316 } else { #use the standard settings from the config
318 # the standard settings from the config could be moved to a null agent
319 # agent_payment_gateway referenced payment_gateway
321 unless ( $conf->exists('business-onlinepayment') ) {
322 if ( $options{'nofatal'} ) {
325 die "Real-time processing not enabled\n";
330 my $bop_config = 'business-onlinepayment';
331 $bop_config .= '-ach'
332 if ( $options{method}
333 && $options{method} =~ /^(ECHECK|CHEK)$/
334 && $conf->exists($bop_config. '-ach')
336 my ( $processor, $login, $password, $action, @bop_options ) =
337 $conf->config($bop_config);
338 $action ||= 'normal authorization';
339 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
340 die "No real-time processor is enabled - ".
341 "did you set the business-onlinepayment configuration value?\n"
344 $payment_gateway = new FS::payment_gateway;
346 $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
347 'Business::OnlinePayment');
348 $payment_gateway->gateway_module($processor);
349 $payment_gateway->gateway_username($login);
350 $payment_gateway->gateway_password($password);
351 $payment_gateway->gateway_action($action);
352 $payment_gateway->set('options', [ @bop_options ]);
356 unless ( $payment_gateway->gateway_namespace ) {
357 $payment_gateway->gateway_namespace(
358 scalar($conf->config('business-onlinepayment-namespace'))
359 || 'Business::OnlinePayment'
368 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
369 those with this agentnum or null agentnum).
376 table => 'invoice_mode',
377 hashref => { agentnum => $self->agentnum },
378 extra_sql => ' OR agentnum IS NULL',
379 order_by => ' ORDER BY modename',
383 =item num_prospect_cust_main
385 Returns the number of prospects (customers with no packages ever ordered) for
390 sub num_prospect_cust_main {
391 shift->num_sql(FS::cust_main->prospect_sql);
395 my( $self, $sql ) = @_;
396 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
397 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
398 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
399 $sth->fetchrow_arrayref->[0];
402 =item prospect_cust_main
404 Returns the prospects (customers with no packages ever ordered) for this agent,
405 as cust_main objects.
409 sub prospect_cust_main {
410 shift->cust_main_sql(FS::cust_main->prospect_sql);
414 my( $self, $sql ) = @_;
415 qsearch( 'cust_main',
416 { 'agentnum' => $self->agentnum },
422 =item num_ordered_cust_main
424 Returns the number of ordered customers for this agent (customers with packages
425 ordered, but not yet billed).
429 sub num_ordered_cust_main {
430 shift->num_sql(FS::cust_main->ordered_sql);
433 =item ordered_cust_main
435 Returns the ordered customers for this agent (customers with packages ordered,
436 but not yet billed), as cust_main objects.
440 sub ordered_cust_main {
441 shift->cust_main_sql(FS::cust_main->ordered_sql);
445 =item num_active_cust_main
447 Returns the number of active customers for this agent (customers with active
452 sub num_active_cust_main {
453 shift->num_sql(FS::cust_main->active_sql);
456 =item active_cust_main
458 Returns the active customers for this agent, as cust_main objects.
462 sub active_cust_main {
463 shift->cust_main_sql(FS::cust_main->active_sql);
466 =item num_inactive_cust_main
468 Returns the number of inactive customers for this agent (customers with no
469 active recurring packages, but otherwise unsuspended/uncancelled).
473 sub num_inactive_cust_main {
474 shift->num_sql(FS::cust_main->inactive_sql);
477 =item inactive_cust_main
479 Returns the inactive customers for this agent, as cust_main objects.
483 sub inactive_cust_main {
484 shift->cust_main_sql(FS::cust_main->inactive_sql);
488 =item num_susp_cust_main
490 Returns the number of suspended customers for this agent.
494 sub num_susp_cust_main {
495 shift->num_sql(FS::cust_main->susp_sql);
500 Returns the suspended customers for this agent, as cust_main objects.
505 shift->cust_main_sql(FS::cust_main->susp_sql);
508 =item num_cancel_cust_main
510 Returns the number of cancelled customer for this agent.
514 sub num_cancel_cust_main {
515 shift->num_sql(FS::cust_main->cancel_sql);
518 =item cancel_cust_main
520 Returns the cancelled customers for this agent, as cust_main objects.
524 sub cancel_cust_main {
525 shift->cust_main_sql(FS::cust_main->cancel_sql);
528 =item num_active_cust_pkg
530 Returns the number of active customer packages for this agent.
534 sub num_active_cust_pkg {
535 shift->num_pkg_sql(FS::cust_pkg->active_sql);
539 my( $self, $sql ) = @_;
541 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
542 " WHERE agentnum = ? AND $sql";
543 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
544 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
545 $sth->fetchrow_arrayref->[0];
548 =item num_inactive_cust_pkg
550 Returns the number of inactive customer packages (one-time packages otherwise
551 unsuspended/uncancelled) for this agent.
555 sub num_inactive_cust_pkg {
556 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
559 =item num_susp_cust_pkg
561 Returns the number of suspended customer packages for this agent.
565 sub num_susp_cust_pkg {
566 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
569 =item num_cancel_cust_pkg
571 Returns the number of cancelled customer packages for this agent.
575 sub num_cancel_cust_pkg {
576 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
579 =item generate_reg_codes NUM PKGPART_ARRAYREF
581 Generates the specified number of registration codes, allowing purchase of the
582 specified package definitions. Returns an array reference of the newly
583 generated codes, or a scalar error message.
587 #false laziness w/prepay_credit::generate
588 sub generate_reg_codes {
589 my( $self, $num, $pkgparts ) = @_;
591 my @codeset = ( 'A'..'Z' );
593 local $SIG{HUP} = 'IGNORE';
594 local $SIG{INT} = 'IGNORE';
595 local $SIG{QUIT} = 'IGNORE';
596 local $SIG{TERM} = 'IGNORE';
597 local $SIG{TSTP} = 'IGNORE';
598 local $SIG{PIPE} = 'IGNORE';
600 my $oldAutoCommit = $FS::UID::AutoCommit;
601 local $FS::UID::AutoCommit = 0;
606 my $reg_code = new FS::reg_code {
607 'agentnum' => $self->agentnum,
608 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
610 my $error = $reg_code->insert($pkgparts);
612 $dbh->rollback if $oldAutoCommit;
615 push @codes, $reg_code->code;
618 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
626 Returns the number of unused registration codes for this agent.
632 my $sth = dbh->prepare(
633 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
634 ) or die dbh->errstr;
635 $sth->execute($self->agentnum) or die $sth->errstr;
636 $sth->fetchrow_arrayref->[0];
639 =item num_prepay_credit
641 Returns the number of unused prepaid cards for this agent.
645 sub num_prepay_credit {
647 my $sth = dbh->prepare(
648 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
649 ) or die dbh->errstr;
650 $sth->execute($self->agentnum) or die $sth->errstr;
651 $sth->fetchrow_arrayref->[0];
656 Returns the number of non-disabled sales people for this agent.
662 my $sth = dbh->prepare(
663 "SELECT COUNT(*) FROM sales WHERE agentnum = ?
664 AND ( disabled = '' OR disabled IS NULL )"
665 ) or die dbh->errstr;
666 $sth->execute($self->agentnum) or die $sth->errstr;
667 $sth->fetchrow_arrayref->[0];
676 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
677 schema.html from the base documentation.