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'
381 =item num_prospect_cust_main
383 Returns the number of prospects (customers with no packages ever ordered) for
388 sub num_prospect_cust_main {
389 shift->num_sql(FS::cust_main->prospect_sql);
393 my( $self, $sql ) = @_;
394 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
395 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
396 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
397 $sth->fetchrow_arrayref->[0];
400 =item prospect_cust_main
402 Returns the prospects (customers with no packages ever ordered) for this agent,
403 as cust_main objects.
407 sub prospect_cust_main {
408 shift->cust_main_sql(FS::cust_main->prospect_sql);
412 my( $self, $sql ) = @_;
413 qsearch( 'cust_main',
414 { 'agentnum' => $self->agentnum },
420 =item num_active_cust_main
422 Returns the number of active customers for this agent (customers with active
427 sub num_active_cust_main {
428 shift->num_sql(FS::cust_main->active_sql);
431 =item active_cust_main
433 Returns the active customers for this agent, as cust_main objects.
437 sub active_cust_main {
438 shift->cust_main_sql(FS::cust_main->active_sql);
441 =item num_inactive_cust_main
443 Returns the number of inactive customers for this agent (customers with no
444 active recurring packages, but otherwise unsuspended/uncancelled).
448 sub num_inactive_cust_main {
449 shift->num_sql(FS::cust_main->inactive_sql);
452 =item inactive_cust_main
454 Returns the inactive customers for this agent, as cust_main objects.
458 sub inactive_cust_main {
459 shift->cust_main_sql(FS::cust_main->inactive_sql);
463 =item num_susp_cust_main
465 Returns the number of suspended customers for this agent.
469 sub num_susp_cust_main {
470 shift->num_sql(FS::cust_main->susp_sql);
475 Returns the suspended customers for this agent, as cust_main objects.
480 shift->cust_main_sql(FS::cust_main->susp_sql);
483 =item num_cancel_cust_main
485 Returns the number of cancelled customer for this agent.
489 sub num_cancel_cust_main {
490 shift->num_sql(FS::cust_main->cancel_sql);
493 =item cancel_cust_main
495 Returns the cancelled customers for this agent, as cust_main objects.
499 sub cancel_cust_main {
500 shift->cust_main_sql(FS::cust_main->cancel_sql);
503 =item num_active_cust_pkg
505 Returns the number of active customer packages for this agent.
509 sub num_active_cust_pkg {
510 shift->num_pkg_sql(FS::cust_pkg->active_sql);
514 my( $self, $sql ) = @_;
516 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
517 " WHERE agentnum = ? AND $sql";
518 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
519 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
520 $sth->fetchrow_arrayref->[0];
523 =item num_inactive_cust_pkg
525 Returns the number of inactive customer packages (one-time packages otherwise
526 unsuspended/uncancelled) for this agent.
530 sub num_inactive_cust_pkg {
531 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
534 =item num_susp_cust_pkg
536 Returns the number of suspended customer packages for this agent.
540 sub num_susp_cust_pkg {
541 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
544 =item num_cancel_cust_pkg
546 Returns the number of cancelled customer packages for this agent.
550 sub num_cancel_cust_pkg {
551 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
554 =item generate_reg_codes NUM PKGPART_ARRAYREF
556 Generates the specified number of registration codes, allowing purchase of the
557 specified package definitions. Returns an array reference of the newly
558 generated codes, or a scalar error message.
562 #false laziness w/prepay_credit::generate
563 sub generate_reg_codes {
564 my( $self, $num, $pkgparts ) = @_;
566 my @codeset = ( 'A'..'Z' );
568 local $SIG{HUP} = 'IGNORE';
569 local $SIG{INT} = 'IGNORE';
570 local $SIG{QUIT} = 'IGNORE';
571 local $SIG{TERM} = 'IGNORE';
572 local $SIG{TSTP} = 'IGNORE';
573 local $SIG{PIPE} = 'IGNORE';
575 my $oldAutoCommit = $FS::UID::AutoCommit;
576 local $FS::UID::AutoCommit = 0;
581 my $reg_code = new FS::reg_code {
582 'agentnum' => $self->agentnum,
583 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
585 my $error = $reg_code->insert($pkgparts);
587 $dbh->rollback if $oldAutoCommit;
590 push @codes, $reg_code->code;
593 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
601 Returns the number of unused registration codes for this agent.
607 my $sth = dbh->prepare(
608 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
609 ) or die dbh->errstr;
610 $sth->execute($self->agentnum) or die $sth->errstr;
611 $sth->fetchrow_arrayref->[0];
614 =item num_prepay_credit
616 Returns the number of unused prepaid cards for this agent.
620 sub num_prepay_credit {
622 my $sth = dbh->prepare(
623 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
624 ) or die dbh->errstr;
625 $sth->execute($self->agentnum) or die $sth->errstr;
626 $sth->fetchrow_arrayref->[0];
636 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
637 schema.html from the base documentation.