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:
51 primary key (assigned automatically for new agents)
55 Text name of this agent
59 Agent type (see L<FS::agent_type>)
61 =item ticketing_queueid
65 =item invoice_template
71 Optional agent customer (see L<FS::cust_main>)
75 Disabled flag, empty or 'Y'
79 Deprecated (never used)
83 Deprecated (never used)
87 (Deprecated) Username for the Agent interface
91 (Deprecated) Password for the Agent interface
101 Creates a new agent. To add the agent to the database, see L<"insert">.
105 sub table { 'agent'; }
109 Adds this agent to the database. If there is an error, returns the error,
110 otherwise returns false.
114 Deletes this agent from the database. Only agents with no customers can be
115 deleted. If there is an error, returns the error, otherwise returns false.
122 return "Can't delete an agent with customers!"
123 if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
125 $self->SUPER::delete;
128 =item replace OLD_RECORD
130 Replaces OLD_RECORD with this one in the database. If there is an error,
131 returns the error, otherwise returns false.
135 Checks all fields to make sure this is a valid agent. If there is an error,
136 returns the error, otherwise returns false. Called by the insert and replace
145 $self->ut_numbern('agentnum')
146 || $self->ut_text('agent')
147 || $self->ut_number('typenum')
148 || $self->ut_numbern('freq')
149 || $self->ut_textn('prog')
150 || $self->ut_textn('invoice_template')
151 || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
152 || $self->ut_numbern('ticketing_queueid')
154 return $error if $error;
156 if ( $self->dbdef_table->column('disabled') ) {
157 $error = $self->ut_enum('disabled', [ '', 'Y' ] );
158 return $error if $error;
161 if ( $self->dbdef_table->column('username') ) {
162 $error = $self->ut_alphan('username');
163 return $error if $error;
164 if ( length($self->username) ) {
165 my $conflict = qsearchs('agent', { 'username' => $self->username } );
166 return 'duplicate agent username (with '. $conflict->agent. ')'
167 if $conflict && $conflict->agentnum != $self->agentnum;
168 $error = $self->ut_text('password'); # ut_text... arbitrary choice
170 $self->_password('');
174 return "Unknown typenum!"
175 unless $self->agent_type;
182 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
188 qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
191 =item agent_cust_main
193 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
198 sub agent_cust_main {
200 qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
203 =item pkgpart_hashref
205 Returns a hash reference. The keys of the hash are pkgparts. The value is
206 true if this agent may purchase the specified package definition. See
211 sub pkgpart_hashref {
213 $self->agent_type->pkgpart_hashref;
216 =item ticketing_queue
218 Returns the queue name corresponding with the id from the I<ticketing_queueid>
219 field, or the empty string.
223 sub ticketing_queue {
225 FS::TicketSystem->queue($self->ticketing_queueid);
228 =item payment_gateway [ OPTION => VALUE, ... ]
230 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
232 Currently available options are I<nofatal>, I<invnum>, I<method>,
233 I<payinfo>, and I<thirdparty>.
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.
248 If I<thirdparty> is set, the defined self-service payment gateway will
253 sub payment_gateway {
254 my ( $self, %options ) = @_;
256 my $conf = new FS::Conf;
258 if ( $options{thirdparty} ) {
259 # still a kludge, but it gets the job done
260 # and the 'cardtype' semantics don't really apply to thirdparty
261 # gateways because we have to choose a gateway without ever
262 # seeing the card number
264 $conf->config('selfservice-payment_gateway', $self->agentnum);
266 $gateway = FS::payment_gateway->by_key($gatewaynum) if $gatewaynum;
267 return $gateway if $gateway;
269 # a little less kludgey than the above, and allows PayPal to coexist
270 # with credit card gateways
271 my $is_paypal = { op => '!=', value => 'PayPal' };
272 if ( uc($options{method}) eq 'PAYPAL' ) {
273 $is_paypal = 'PayPal';
276 $gateway = qsearchs({
277 table => 'payment_gateway',
278 addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
280 gateway_namespace => 'Business::OnlineThirdPartyPayment',
281 gateway_module => $is_paypal,
284 extra_sql => ' AND agentnum = '.$self->agentnum,
289 } elsif ( $options{'nofatal'} ) {
292 die "no third-party gateway configured\n";
297 if ( $options{invnum} ) {
299 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
300 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
306 $cust_bill->cust_bill_pkg;
308 my @taxclasses = map $_->taxclass, @part_pkg;
310 $taxclass = $taxclasses[0]
311 unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
312 #different taxclasses
315 #look for an agent gateway override first
317 if ( $options{method} ) {
318 if ( $options{method} eq 'CC' && $options{payinfo} ) {
319 $cardtype = cardtype($options{payinfo});
320 } elsif ( $options{method} eq 'ECHECK' ) {
323 $cardtype = $options{method}
328 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
329 cardtype => $cardtype,
330 taxclass => $taxclass, } )
331 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
333 taxclass => $taxclass, } )
334 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
335 cardtype => $cardtype,
337 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
342 if ( $override ) { #use a payment gateway override
344 $payment_gateway = $override->payment_gateway;
346 $payment_gateway->gateway_namespace('Business::OnlinePayment')
347 unless $payment_gateway->gateway_namespace;
349 } else { #use the standard settings from the config
351 # the standard settings from the config could be moved to a null agent
352 # agent_payment_gateway referenced payment_gateway
354 unless ( $conf->exists('business-onlinepayment') ) {
355 if ( $options{'nofatal'} ) {
358 die "Real-time processing not enabled\n";
363 my $bop_config = 'business-onlinepayment';
364 $bop_config .= '-ach'
365 if ( $options{method}
366 && $options{method} =~ /^(ECHECK|CHEK)$/
367 && $conf->exists($bop_config. '-ach')
369 my ( $processor, $login, $password, $action, @bop_options ) =
370 $conf->config($bop_config);
371 $action ||= 'normal authorization';
372 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
373 die "No real-time processor is enabled - ".
374 "did you set the business-onlinepayment configuration value?\n"
377 $payment_gateway = new FS::payment_gateway;
379 $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
380 'Business::OnlinePayment');
381 $payment_gateway->gateway_module($processor);
382 $payment_gateway->gateway_username($login);
383 $payment_gateway->gateway_password($password);
384 $payment_gateway->gateway_action($action);
385 $payment_gateway->set('options', [ @bop_options ]);
389 unless ( $payment_gateway->gateway_namespace ) {
390 $payment_gateway->gateway_namespace(
391 scalar($conf->config('business-onlinepayment-namespace'))
392 || 'Business::OnlinePayment'
401 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
402 those with this agentnum or null agentnum).
409 table => 'invoice_mode',
410 hashref => { agentnum => $self->agentnum },
411 extra_sql => ' OR agentnum IS NULL',
412 order_by => ' ORDER BY modename',
416 =item num_prospect_cust_main
418 Returns the number of prospects (customers with no packages ever ordered) for
423 sub num_prospect_cust_main {
424 shift->num_sql(FS::cust_main->prospect_sql);
428 my( $self, $sql ) = @_;
429 my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
430 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
431 $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
432 $sth->fetchrow_arrayref->[0];
435 =item prospect_cust_main
437 Returns the prospects (customers with no packages ever ordered) for this agent,
438 as cust_main objects.
442 sub prospect_cust_main {
443 shift->cust_main_sql(FS::cust_main->prospect_sql);
447 my( $self, $sql ) = @_;
448 qsearch( 'cust_main',
449 { 'agentnum' => $self->agentnum },
455 =item num_ordered_cust_main
457 Returns the number of ordered customers for this agent (customers with packages
458 ordered, but not yet billed).
462 sub num_ordered_cust_main {
463 shift->num_sql(FS::cust_main->ordered_sql);
466 =item ordered_cust_main
468 Returns the ordered customers for this agent (customers with packages ordered,
469 but not yet billed), as cust_main objects.
473 sub ordered_cust_main {
474 shift->cust_main_sql(FS::cust_main->ordered_sql);
478 =item num_active_cust_main
480 Returns the number of active customers for this agent (customers with active
485 sub num_active_cust_main {
486 shift->num_sql(FS::cust_main->active_sql);
489 =item active_cust_main
491 Returns the active customers for this agent, as cust_main objects.
495 sub active_cust_main {
496 shift->cust_main_sql(FS::cust_main->active_sql);
499 =item num_inactive_cust_main
501 Returns the number of inactive customers for this agent (customers with no
502 active recurring packages, but otherwise unsuspended/uncancelled).
506 sub num_inactive_cust_main {
507 shift->num_sql(FS::cust_main->inactive_sql);
510 =item inactive_cust_main
512 Returns the inactive customers for this agent, as cust_main objects.
516 sub inactive_cust_main {
517 shift->cust_main_sql(FS::cust_main->inactive_sql);
521 =item num_susp_cust_main
523 Returns the number of suspended customers for this agent.
527 sub num_susp_cust_main {
528 shift->num_sql(FS::cust_main->susp_sql);
533 Returns the suspended customers for this agent, as cust_main objects.
538 shift->cust_main_sql(FS::cust_main->susp_sql);
541 =item num_cancel_cust_main
543 Returns the number of cancelled customer for this agent.
547 sub num_cancel_cust_main {
548 shift->num_sql(FS::cust_main->cancel_sql);
551 =item cancel_cust_main
553 Returns the cancelled customers for this agent, as cust_main objects.
557 sub cancel_cust_main {
558 shift->cust_main_sql(FS::cust_main->cancel_sql);
561 =item num_active_cust_pkg
563 Returns the number of active customer packages for this agent.
567 sub num_active_cust_pkg {
568 shift->num_pkg_sql(FS::cust_pkg->active_sql);
572 my( $self, $sql ) = @_;
574 "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
575 " WHERE agentnum = ? AND $sql";
576 my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
577 $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
578 $sth->fetchrow_arrayref->[0];
581 =item num_inactive_cust_pkg
583 Returns the number of inactive customer packages (one-time packages otherwise
584 unsuspended/uncancelled) for this agent.
588 sub num_inactive_cust_pkg {
589 shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
592 =item num_susp_cust_pkg
594 Returns the number of suspended customer packages for this agent.
598 sub num_susp_cust_pkg {
599 shift->num_pkg_sql(FS::cust_pkg->susp_sql);
602 =item num_cancel_cust_pkg
604 Returns the number of cancelled customer packages for this agent.
608 sub num_cancel_cust_pkg {
609 shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
612 =item num_on_hold_cust_pkg
614 Returns the number of inactive customer packages (one-time packages otherwise
615 unsuspended/uncancelled) for this agent.
619 sub num_on_hold_cust_pkg {
620 shift->num_pkg_sql(FS::cust_pkg->on_hold_sql);
623 =item num_not_yet_billed_cust_pkg
625 Returns the number of inactive customer packages (one-time packages otherwise
626 unsuspended/uncancelled) for this agent.
630 sub num_not_yet_billed_cust_pkg {
631 shift->num_pkg_sql(FS::cust_pkg->not_yet_billed_sql);
634 =item generate_reg_codes NUM PKGPART_ARRAYREF
636 Generates the specified number of registration codes, allowing purchase of the
637 specified package definitions. Returns an array reference of the newly
638 generated codes, or a scalar error message.
642 #false laziness w/prepay_credit::generate
643 sub generate_reg_codes {
644 my( $self, $num, $pkgparts ) = @_;
646 my @codeset = ( 'A'..'Z' );
648 local $SIG{HUP} = 'IGNORE';
649 local $SIG{INT} = 'IGNORE';
650 local $SIG{QUIT} = 'IGNORE';
651 local $SIG{TERM} = 'IGNORE';
652 local $SIG{TSTP} = 'IGNORE';
653 local $SIG{PIPE} = 'IGNORE';
655 my $oldAutoCommit = $FS::UID::AutoCommit;
656 local $FS::UID::AutoCommit = 0;
661 my $reg_code = new FS::reg_code {
662 'agentnum' => $self->agentnum,
663 'code' => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
665 my $error = $reg_code->insert($pkgparts);
667 $dbh->rollback if $oldAutoCommit;
670 push @codes, $reg_code->code;
673 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
681 Returns the number of unused registration codes for this agent.
687 my $sth = dbh->prepare(
688 "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
689 ) or die dbh->errstr;
690 $sth->execute($self->agentnum) or die $sth->errstr;
691 $sth->fetchrow_arrayref->[0];
694 =item num_prepay_credit
696 Returns the number of unused prepaid cards for this agent.
700 sub num_prepay_credit {
702 my $sth = dbh->prepare(
703 "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
704 ) or die dbh->errstr;
705 $sth->execute($self->agentnum) or die $sth->errstr;
706 $sth->fetchrow_arrayref->[0];
711 Returns the number of non-disabled sales people for this agent.
717 my $sth = dbh->prepare(
718 "SELECT COUNT(*) FROM sales WHERE agentnum = ?
719 AND ( disabled = '' OR disabled IS NULL )"
720 ) or die dbh->errstr;
721 $sth->execute($self->agentnum) or die $sth->errstr;
722 $sth->fetchrow_arrayref->[0];
731 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>,
732 schema.html from the base documentation.