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