fix redefinition warning and add ordered_cust_main sub for completeness, RT#27442
[freeside.git] / FS / FS / agent.pm
1 package FS::agent;
2 use base qw( FS::m2m_Common FS::m2name_Common FS::Record );
3
4 use strict;
5 use vars qw( @ISA );
6 use Business::CreditCard 0.28;
7 use FS::Record qw( dbh qsearch qsearchs );
8 use FS::cust_main;
9 use FS::cust_pkg;
10 use FS::reg_code;
11 use FS::agent_payment_gateway;
12 use FS::TicketSystem;
13 use FS::Conf;
14
15 =head1 NAME
16
17 FS::agent - Object methods for agent records
18
19 =head1 SYNOPSIS
20
21   use FS::agent;
22
23   $record = new FS::agent \%hash;
24   $record = new FS::agent { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34   $agent_type = $record->agent_type;
35
36   $hashref = $record->pkgpart_hashref;
37   #may purchase $pkgpart if $hashref->{$pkgpart};
38
39 =head1 DESCRIPTION
40
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:
44
45 =over 4
46
47 =item agentnum - primary key (assigned automatically for new agents)
48
49 =item agent - Text name of this agent
50
51 =item typenum - Agent type (see L<FS::agent_type>)
52
53 =item ticketing_queueid - Ticketing Queue
54
55 =item invoice_template - Invoice template name
56
57 =item agent_custnum - Optional agent customer (see L<FS::cust_main>)
58
59 =item disabled - Disabled flag, empty or 'Y'
60
61 =item prog - Deprecated (never used)
62
63 =item freq - Deprecated (never used)
64
65 =item username - (Deprecated) Username for the Agent interface
66
67 =item _password - (Deprecated) Password for the Agent interface
68
69 =back
70
71 =head1 METHODS
72
73 =over 4
74
75 =item new HASHREF
76
77 Creates a new agent.  To add the agent to the database, see L<"insert">.
78
79 =cut
80
81 sub table { 'agent'; }
82
83 =item insert
84
85 Adds this agent to the database.  If there is an error, returns the error,
86 otherwise returns false.
87
88 =item delete
89
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.
92
93 =cut
94
95 sub delete {
96   my $self = shift;
97
98   return "Can't delete an agent with customers!"
99     if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
100
101   $self->SUPER::delete;
102 }
103
104 =item replace OLD_RECORD
105
106 Replaces OLD_RECORD with this one in the database.  If there is an error,
107 returns the error, otherwise returns false.
108
109 =item check
110
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
113 methods.
114
115 =cut
116
117 sub check {
118   my $self = shift;
119
120   my $error =
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' )
128   ;
129   return $error if $error;
130
131   if ( $self->dbdef_table->column('disabled') ) {
132     $error = $self->ut_enum('disabled', [ '', 'Y' ] );
133     return $error if $error;
134   }
135
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
144     } else {
145       $self->_password('');
146     }
147   }
148
149   return "Unknown typenum!"
150     unless $self->agent_type;
151
152   $self->SUPER::check;
153 }
154
155 =item agent_type
156
157 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
158
159 =item agent_cust_main
160
161 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
162 agent.
163
164 =cut
165
166 sub agent_cust_main {
167   my $self = shift;
168   qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
169 }
170
171 =item agent_currency
172
173 Returns the FS::agent_currency objects (see L<FS::agent_currency>), if any, for
174 this agent.
175
176 =item agent_currency_hashref
177
178 Returns a hash references of supported additional currencies for this agent.
179
180 =cut
181
182 sub agent_currency_hashref {
183   my $self = shift;
184   +{ map { $_->currency => 1 }
185        $self->agent_currency
186    };
187 }
188
189 =item pkgpart_hashref
190
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
193 L<FS::part_pkg>.
194
195 =cut
196
197 sub pkgpart_hashref {
198   my $self = shift;
199   $self->agent_type->pkgpart_hashref;
200 }
201
202 =item ticketing_queue
203
204 Returns the queue name corresponding with the id from the I<ticketing_queueid>
205 field, or the empty string.
206
207 =cut
208
209 sub ticketing_queue {
210   my $self = shift;
211   FS::TicketSystem->queue($self->ticketing_queueid);
212 };
213
214 =item payment_gateway [ OPTION => VALUE, ... ]
215
216 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
217
218 Currently available options are I<nofatal>, I<invnum>, I<method>, 
219 I<payinfo>, and I<thirdparty>.
220
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.
223
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 
226 the invoice.
227
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.
230
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.
233
234 If I<thirdparty> is set, the defined self-service payment gateway will 
235 be returned.
236
237 =cut
238
239 sub payment_gateway {
240   my ( $self, %options ) = @_;
241   
242   my $conf = new FS::Conf;
243
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
249     my $gatewaynum =
250       $conf->config('selfservice-payment_gateway', $self->agentnum);
251     my $gateway = FS::payment_gateway->by_key($gatewaynum)
252       if $gatewaynum;
253
254     if ( $gateway ) {
255       return $gateway;
256     } elsif ( $options{'nofatal'} ) {
257       return '';
258     } else {
259       die "no third-party gateway configured\n";
260     }
261   }
262
263   my $taxclass = '';
264   if ( $options{invnum} ) {
265
266     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
267     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
268
269     my @part_pkg =
270       map  { $_->part_pkg }
271       grep { $_ }
272       map  { $_->cust_pkg }
273       $cust_bill->cust_bill_pkg;
274
275     my @taxclasses = map $_->taxclass, @part_pkg;
276
277     $taxclass = $taxclasses[0]
278       unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
279                                                         #different taxclasses
280   }
281
282   #look for an agent gateway override first
283   my $cardtype = '';
284   if ( $options{method} ) {
285     if ( $options{method} eq 'CC' && $options{payinfo} ) {
286       $cardtype = cardtype($options{payinfo});
287     } elsif ( $options{method} eq 'ECHECK' ) {
288       $cardtype = 'ACH';
289     } else {
290       $cardtype = $options{method}
291     }
292   }
293
294   my $override =
295        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
296                                            cardtype => $cardtype,
297                                            taxclass => $taxclass,       } )
298     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
299                                            cardtype => '',
300                                            taxclass => $taxclass,       } )
301     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
302                                            cardtype => $cardtype,
303                                            taxclass => '',              } )
304     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
305                                            cardtype => '',
306                                            taxclass => '',              } );
307
308   my $payment_gateway;
309   if ( $override ) { #use a payment gateway override
310
311     $payment_gateway = $override->payment_gateway;
312
313     $payment_gateway->gateway_namespace('Business::OnlinePayment')
314       unless $payment_gateway->gateway_namespace;
315
316   } else { #use the standard settings from the config
317
318     # the standard settings from the config could be moved to a null agent
319     # agent_payment_gateway referenced payment_gateway
320
321     unless ( $conf->exists('business-onlinepayment') ) {
322       if ( $options{'nofatal'} ) {
323         return '';
324       } else {
325         die "Real-time processing not enabled\n";
326       }
327     }
328
329     #load up config
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')
335          );
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"
342       unless $processor;
343
344     $payment_gateway = new FS::payment_gateway;
345
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 ]);
353
354   }
355
356   unless ( $payment_gateway->gateway_namespace ) {
357     $payment_gateway->gateway_namespace(
358       scalar($conf->config('business-onlinepayment-namespace'))
359       || 'Business::OnlinePayment'
360     );
361   }
362
363   $payment_gateway;
364 }
365
366 =item invoice_modes
367
368 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
369 those with this agentnum or null agentnum).
370
371 =cut
372
373 sub invoice_modes {
374   my $self = shift;
375   qsearch( {
376       table     => 'invoice_mode',
377       hashref   => { agentnum => $self->agentnum },
378       extra_sql => ' OR agentnum IS NULL',
379       order_by  => ' ORDER BY modename',
380   } );
381 }
382
383 =item num_prospect_cust_main
384
385 Returns the number of prospects (customers with no packages ever ordered) for
386 this agent.
387
388 =cut
389
390 sub num_prospect_cust_main {
391   shift->num_sql(FS::cust_main->prospect_sql);
392 }
393
394 sub num_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];
400 }
401
402 =item prospect_cust_main
403
404 Returns the prospects (customers with no packages ever ordered) for this agent,
405 as cust_main objects.
406
407 =cut
408
409 sub prospect_cust_main {
410   shift->cust_main_sql(FS::cust_main->prospect_sql);
411 }
412
413 sub cust_main_sql {
414   my( $self, $sql ) = @_;
415   qsearch( 'cust_main',
416            { 'agentnum' => $self->agentnum },
417            '',
418            " AND $sql"
419   );
420 }
421
422 =item num_ordered_cust_main
423
424 Returns the number of ordered customers for this agent (customers with packages
425 ordered, but not yet billed).
426
427 =cut
428
429 sub num_ordered_cust_main {
430   shift->num_sql(FS::cust_main->ordered_sql);
431 }
432
433 =item ordered_cust_main
434
435 Returns the ordered customers for this agent (customers with packages ordered,
436 but not yet billed), as cust_main objects.
437
438 =cut
439
440 sub ordered_cust_main {
441   shift->cust_main_sql(FS::cust_main->ordered_sql);
442 }
443
444
445 =item num_active_cust_main
446
447 Returns the number of active customers for this agent (customers with active
448 recurring packages).
449
450 =cut
451
452 sub num_active_cust_main {
453   shift->num_sql(FS::cust_main->active_sql);
454 }
455
456 =item active_cust_main
457
458 Returns the active customers for this agent, as cust_main objects.
459
460 =cut
461
462 sub active_cust_main {
463   shift->cust_main_sql(FS::cust_main->active_sql);
464 }
465
466 =item num_inactive_cust_main
467
468 Returns the number of inactive customers for this agent (customers with no
469 active recurring packages, but otherwise unsuspended/uncancelled).
470
471 =cut
472
473 sub num_inactive_cust_main {
474   shift->num_sql(FS::cust_main->inactive_sql);
475 }
476
477 =item inactive_cust_main
478
479 Returns the inactive customers for this agent, as cust_main objects.
480
481 =cut
482
483 sub inactive_cust_main {
484   shift->cust_main_sql(FS::cust_main->inactive_sql);
485 }
486
487
488 =item num_susp_cust_main
489
490 Returns the number of suspended customers for this agent.
491
492 =cut
493
494 sub num_susp_cust_main {
495   shift->num_sql(FS::cust_main->susp_sql);
496 }
497
498 =item susp_cust_main
499
500 Returns the suspended customers for this agent, as cust_main objects.
501
502 =cut
503
504 sub susp_cust_main {
505   shift->cust_main_sql(FS::cust_main->susp_sql);
506 }
507
508 =item num_cancel_cust_main
509
510 Returns the number of cancelled customer for this agent.
511
512 =cut
513
514 sub num_cancel_cust_main {
515   shift->num_sql(FS::cust_main->cancel_sql);
516 }
517
518 =item cancel_cust_main
519
520 Returns the cancelled customers for this agent, as cust_main objects.
521
522 =cut
523
524 sub cancel_cust_main {
525   shift->cust_main_sql(FS::cust_main->cancel_sql);
526 }
527
528 =item num_active_cust_pkg
529
530 Returns the number of active customer packages for this agent.
531
532 =cut
533
534 sub num_active_cust_pkg {
535   shift->num_pkg_sql(FS::cust_pkg->active_sql);
536 }
537
538 sub num_pkg_sql {
539   my( $self, $sql ) = @_;
540   my $statement = 
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];
546 }
547
548 =item num_inactive_cust_pkg
549
550 Returns the number of inactive customer packages (one-time packages otherwise
551 unsuspended/uncancelled) for this agent.
552
553 =cut
554
555 sub num_inactive_cust_pkg {
556   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
557 }
558
559 =item num_susp_cust_pkg
560
561 Returns the number of suspended customer packages for this agent.
562
563 =cut
564
565 sub num_susp_cust_pkg {
566   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
567 }
568
569 =item num_cancel_cust_pkg
570
571 Returns the number of cancelled customer packages for this agent.
572
573 =cut
574
575 sub num_cancel_cust_pkg {
576   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
577 }
578
579 =item generate_reg_codes NUM PKGPART_ARRAYREF
580
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.
584
585 =cut
586
587 #false laziness w/prepay_credit::generate
588 sub generate_reg_codes {
589   my( $self, $num, $pkgparts ) = @_;
590
591   my @codeset = ( 'A'..'Z' );
592
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';
599
600   my $oldAutoCommit = $FS::UID::AutoCommit;
601   local $FS::UID::AutoCommit = 0;
602   my $dbh = dbh;
603
604   my @codes = ();
605   for ( 1 ... $num ) {
606     my $reg_code = new FS::reg_code {
607       'agentnum' => $self->agentnum,
608       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
609     };
610     my $error = $reg_code->insert($pkgparts);
611     if ( $error ) {
612       $dbh->rollback if $oldAutoCommit;
613       return $error;
614     }
615     push @codes, $reg_code->code;
616   }
617
618   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
619
620   \@codes;
621
622 }
623
624 =item num_reg_code
625
626 Returns the number of unused registration codes for this agent.
627
628 =cut
629
630 sub num_reg_code {
631   my $self = shift;
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];
637 }
638
639 =item num_prepay_credit
640
641 Returns the number of unused prepaid cards for this agent.
642
643 =cut
644
645 sub num_prepay_credit {
646   my $self = shift;
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];
652 }
653
654 =item num_sales
655
656 Returns the number of non-disabled sales people for this agent.
657
658 =cut
659
660 sub num_sales {
661   my $self = shift;
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];
668 }
669
670 =back
671
672 =head1 BUGS
673
674 =head1 SEE ALSO
675
676 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
677 schema.html from the base documentation.
678
679 =cut
680
681 1;
682