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>, and I<payinfo>.
235 If I<nofatal> is set, and no gateway is available, then the empty string
236 will be returned instead of throwing a fatal exception.
238 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
239 an attempt will be made to select a gateway suited for the taxes paid on
242 The I<method> and I<payinfo> options can be used to influence the choice
243 as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
245 When the I<method> is 'CC' then the card number in I<payinfo> can direct
246 this routine to route to a gateway suited for that type of card.
250 sub payment_gateway {
251 my ( $self, %options ) = @_;
254 if ( $options{invnum} ) {
256 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
257 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
263 $cust_bill->cust_bill_pkg;
265 my @taxclasses = map $_->taxclass, @part_pkg;
267 $taxclass = $taxclasses[0]
268 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
269 #different taxclasses
272 #look for an agent gateway override first
274 if ( $options{method} ) {
275 if ( $options{method} eq 'CC' && $options{payinfo} ) {
276 $cardtype = cardtype($options{payinfo});
277 } elsif ( $options{method} eq 'ECHECK' ) {
279 } elsif ( $options{method} eq 'PAYPAL' ) {
280 $cardtype = 'PayPal';
282 $cardtype = $options{method}
287 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
288 cardtype => $cardtype,
289 taxclass => $taxclass, } )
290 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
292 taxclass => $taxclass, } )
293 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
294 cardtype => $cardtype,
296 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
301 my $conf = new FS::Conf;
302 if ( $override ) { #use a payment gateway override
304 $payment_gateway = $override->payment_gateway;
306 $payment_gateway->gateway_namespace('Business::OnlinePayment')
307 unless $payment_gateway->gateway_namespace;
309 } else { #use the standard settings from the config
311 # the standard settings from the config could be moved to a null agent
312 # agent_payment_gateway referenced payment_gateway
314 unless ( $conf->exists('business-onlinepayment') ) {
315 if ( $options{'nofatal'} ) {
318 die "Real-time processing not enabled\n";
323 my $bop_config = 'business-onlinepayment';
324 $bop_config .= '-ach'
325 if ( $options{method}
326 && $options{method} =~ /^(ECHECK|CHEK)$/
327 && $conf->exists($bop_config. '-ach')
329 my ( $processor, $login, $password, $action, @bop_options ) =
330 $conf->config($bop_config);
331 $action ||= 'normal authorization';
332 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
333 die "No real-time processor is enabled - ".
334 "did you set the business-onlinepayment configuration value?\n"
337 $payment_gateway = new FS::payment_gateway;
339 $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
340 'Business::OnlinePayment');
341 $payment_gateway->gateway_module($processor);
342 $payment_gateway->gateway_username($login);
343 $payment_gateway->gateway_password($password);
344 $payment_gateway->gateway_action($action);
345 $payment_gateway->set('options', [ @bop_options ]);
349 unless ( $payment_gateway->gateway_namespace ) {
350 $payment_gateway->gateway_namespace(
351 scalar($conf->config('business-onlinepayment-namespace'))
352 || 'Business::OnlinePayment'
359 =item num_prospect_cust_main
361 Returns the number of prospects (customers with no packages ever ordered) for
366 sub num_prospect_cust_main {
367 shift->num_sql(FS::cust_main->prospect_sql);
371 my( $self, $sql ) = @_;
372 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
373 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
374 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
375 $sth->fetchrow_arrayref->[0];
378 =item prospect_cust_main
380 Returns the prospects (customers with no packages ever ordered) for this agent,
381 as cust_main objects.
385 sub prospect_cust_main {
386 shift->cust_main_sql(FS::cust_main->prospect_sql);
390 my( $self, $sql ) = @_;
391 qsearch( 'cust_main',
392 { 'agentnum' => $self->agentnum },
398 =item num_active_cust_main
400 Returns the number of active customers for this agent (customers with active
405 sub num_active_cust_main {
406 shift->num_sql(FS::cust_main->active_sql);
409 =item active_cust_main
411 Returns the active customers for this agent, as cust_main objects.
415 sub active_cust_main {
416 shift->cust_main_sql(FS::cust_main->active_sql);
419 =item num_inactive_cust_main
421 Returns the number of inactive customers for this agent (customers with no
422 active recurring packages, but otherwise unsuspended/uncancelled).
426 sub num_inactive_cust_main {
427 shift->num_sql(FS::cust_main->inactive_sql);
430 =item inactive_cust_main
432 Returns the inactive customers for this agent, as cust_main objects.
436 sub inactive_cust_main {
437 shift->cust_main_sql(FS::cust_main->inactive_sql);
441 =item num_susp_cust_main
443 Returns the number of suspended customers for this agent.
447 sub num_susp_cust_main {
448 shift->num_sql(FS::cust_main->susp_sql);
453 Returns the suspended customers for this agent, as cust_main objects.
458 shift->cust_main_sql(FS::cust_main->susp_sql);
461 =item num_cancel_cust_main
463 Returns the number of cancelled customer for this agent.
467 sub num_cancel_cust_main {
468 shift->num_sql(FS::cust_main->cancel_sql);
471 =item cancel_cust_main
473 Returns the cancelled customers for this agent, as cust_main objects.
477 sub cancel_cust_main {
478 shift->cust_main_sql(FS::cust_main->cancel_sql);
481 =item num_active_cust_pkg
483 Returns the number of active customer packages for this agent.
487 sub num_active_cust_pkg {
488 shift->num_pkg_sql(FS::cust_pkg->active_sql);
492 my( $self, $sql ) = @_;
494 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
495 " WHERE agentnum = ? AND $sql";
496 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
497 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
498 $sth->fetchrow_arrayref->[0];
501 =item num_inactive_cust_pkg
503 Returns the number of inactive customer packages (one-time packages otherwise
504 unsuspended/uncancelled) for this agent.
508 sub num_inactive_cust_pkg {
509 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
512 =item num_susp_cust_pkg
514 Returns the number of suspended customer packages for this agent.
518 sub num_susp_cust_pkg {
519 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
522 =item num_cancel_cust_pkg
524 Returns the number of cancelled customer packages for this agent.
528 sub num_cancel_cust_pkg {
529 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
532 =item generate_reg_codes NUM PKGPART_ARRAYREF
534 Generates the specified number of registration codes, allowing purchase of the
535 specified package definitions. Returns an array reference of the newly
536 generated codes, or a scalar error message.
540 #false laziness w/prepay_credit::generate
541 sub generate_reg_codes {
542 my( $self, $num, $pkgparts ) = @_;
544 my @codeset = ( 'A'..'Z' );
546 local $SIG{HUP} = 'IGNORE';
547 local $SIG{INT} = 'IGNORE';
548 local $SIG{QUIT} = 'IGNORE';
549 local $SIG{TERM} = 'IGNORE';
550 local $SIG{TSTP} = 'IGNORE';
551 local $SIG{PIPE} = 'IGNORE';
553 my $oldAutoCommit = $FS::UID::AutoCommit;
554 local $FS::UID::AutoCommit = 0;
559 my $reg_code = new FS::reg_code {
560 'agentnum' => $self->agentnum,
561 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
563 my $error = $reg_code->insert($pkgparts);
565 $dbh->rollback if $oldAutoCommit;
568 push @codes, $reg_code->code;
571 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
579 Returns the number of unused registration codes for this agent.
585 my $sth = dbh->prepare(
586 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
587 ) or die dbh->errstr;
588 $sth->execute($self->agentnum) or die $sth->errstr;
589 $sth->fetchrow_arrayref->[0];
592 =item num_prepay_credit
594 Returns the number of unused prepaid cards for this agent.
598 sub num_prepay_credit {
600 my $sth = dbh->prepare(
601 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
602 ) or die dbh->errstr;
603 $sth->execute($self->agentnum) or die $sth->errstr;
604 $sth->fetchrow_arrayref->[0];
614 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
615 schema.html from the base documentation.