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