1dd6796cbd710988044abd9f55f0012ab19a109f
[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;
266     $gateway = FS::payment_gateway->by_key($gatewaynum) if $gatewaynum;
267     return $gateway if $gateway;
268
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';
274     }
275
276     $gateway = qsearchs({
277         table     => 'payment_gateway',
278         addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
279         hashref   => {
280           gateway_namespace => 'Business::OnlineThirdPartyPayment',
281           gateway_module    => $is_paypal,
282           disabled          => '',
283         },
284         extra_sql => ' AND agentnum = '.$self->agentnum,
285     });
286
287     if ( $gateway ) {
288       return $gateway;
289     } elsif ( $options{'nofatal'} ) {
290       return '';
291     } else {
292       die "no third-party gateway configured\n";
293     }
294   }
295
296   my $taxclass = '';
297   if ( $options{invnum} ) {
298
299     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
300     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
301
302     my @part_pkg =
303       map  { $_->part_pkg }
304       grep { $_ }
305       map  { $_->cust_pkg }
306       $cust_bill->cust_bill_pkg;
307
308     my @taxclasses = map $_->taxclass, @part_pkg;
309
310     $taxclass = $taxclasses[0]
311       unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
312                                                         #different taxclasses
313   }
314
315   #look for an agent gateway override first
316   my $cardtype = '';
317   if ( $options{method} ) {
318     if ( $options{method} eq 'CC' && $options{payinfo} ) {
319       $cardtype = cardtype($options{payinfo});
320     } elsif ( $options{method} eq 'ECHECK' ) {
321       $cardtype = 'ACH';
322     } else {
323       $cardtype = $options{method}
324     }
325   }
326
327   my $override =
328        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
329                                            cardtype => $cardtype,
330                                            taxclass => $taxclass,       } )
331     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
332                                            cardtype => '',
333                                            taxclass => $taxclass,       } )
334     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
335                                            cardtype => $cardtype,
336                                            taxclass => '',              } )
337     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
338                                            cardtype => '',
339                                            taxclass => '',              } );
340
341   my $payment_gateway;
342   if ( $override ) { #use a payment gateway override
343
344     $payment_gateway = $override->payment_gateway;
345
346     $payment_gateway->gateway_namespace('Business::OnlinePayment')
347       unless $payment_gateway->gateway_namespace;
348
349   } else { #use the standard settings from the config
350
351     # the standard settings from the config could be moved to a null agent
352     # agent_payment_gateway referenced payment_gateway
353
354     unless ( $conf->exists('business-onlinepayment') ) {
355       if ( $options{'nofatal'} ) {
356         return '';
357       } else {
358         die "Real-time processing not enabled\n";
359       }
360     }
361
362     #load up config
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')
368          );
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"
375       unless $processor;
376
377     $payment_gateway = new FS::payment_gateway;
378
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 ]);
386
387   }
388
389   unless ( $payment_gateway->gateway_namespace ) {
390     $payment_gateway->gateway_namespace(
391       scalar($conf->config('business-onlinepayment-namespace'))
392       || 'Business::OnlinePayment'
393     );
394   }
395
396   $payment_gateway;
397 }
398
399 =item invoice_modes
400
401 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
402 those with this agentnum or null agentnum).
403
404 =cut
405
406 sub invoice_modes {
407   my $self = shift;
408   qsearch( {
409       table     => 'invoice_mode',
410       hashref   => { agentnum => $self->agentnum },
411       extra_sql => ' OR agentnum IS NULL',
412       order_by  => ' ORDER BY modename',
413   } );
414 }
415
416 =item num_prospect_cust_main
417
418 Returns the number of prospects (customers with no packages ever ordered) for
419 this agent.
420
421 =cut
422
423 sub num_prospect_cust_main {
424   shift->num_sql(FS::cust_main->prospect_sql);
425 }
426
427 sub num_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];
433 }
434
435 =item prospect_cust_main
436
437 Returns the prospects (customers with no packages ever ordered) for this agent,
438 as cust_main objects.
439
440 =cut
441
442 sub prospect_cust_main {
443   shift->cust_main_sql(FS::cust_main->prospect_sql);
444 }
445
446 sub cust_main_sql {
447   my( $self, $sql ) = @_;
448   qsearch( 'cust_main',
449            { 'agentnum' => $self->agentnum },
450            '',
451            " AND $sql"
452   );
453 }
454
455 =item num_ordered_cust_main
456
457 Returns the number of ordered customers for this agent (customers with packages
458 ordered, but not yet billed).
459
460 =cut
461
462 sub num_ordered_cust_main {
463   shift->num_sql(FS::cust_main->ordered_sql);
464 }
465
466 =item ordered_cust_main
467
468 Returns the ordered customers for this agent (customers with packages ordered,
469 but not yet billed), as cust_main objects.
470
471 =cut
472
473 sub ordered_cust_main {
474   shift->cust_main_sql(FS::cust_main->ordered_sql);
475 }
476
477
478 =item num_active_cust_main
479
480 Returns the number of active customers for this agent (customers with active
481 recurring packages).
482
483 =cut
484
485 sub num_active_cust_main {
486   shift->num_sql(FS::cust_main->active_sql);
487 }
488
489 =item active_cust_main
490
491 Returns the active customers for this agent, as cust_main objects.
492
493 =cut
494
495 sub active_cust_main {
496   shift->cust_main_sql(FS::cust_main->active_sql);
497 }
498
499 =item num_inactive_cust_main
500
501 Returns the number of inactive customers for this agent (customers with no
502 active recurring packages, but otherwise unsuspended/uncancelled).
503
504 =cut
505
506 sub num_inactive_cust_main {
507   shift->num_sql(FS::cust_main->inactive_sql);
508 }
509
510 =item inactive_cust_main
511
512 Returns the inactive customers for this agent, as cust_main objects.
513
514 =cut
515
516 sub inactive_cust_main {
517   shift->cust_main_sql(FS::cust_main->inactive_sql);
518 }
519
520
521 =item num_susp_cust_main
522
523 Returns the number of suspended customers for this agent.
524
525 =cut
526
527 sub num_susp_cust_main {
528   shift->num_sql(FS::cust_main->susp_sql);
529 }
530
531 =item susp_cust_main
532
533 Returns the suspended customers for this agent, as cust_main objects.
534
535 =cut
536
537 sub susp_cust_main {
538   shift->cust_main_sql(FS::cust_main->susp_sql);
539 }
540
541 =item num_cancel_cust_main
542
543 Returns the number of cancelled customer for this agent.
544
545 =cut
546
547 sub num_cancel_cust_main {
548   shift->num_sql(FS::cust_main->cancel_sql);
549 }
550
551 =item cancel_cust_main
552
553 Returns the cancelled customers for this agent, as cust_main objects.
554
555 =cut
556
557 sub cancel_cust_main {
558   shift->cust_main_sql(FS::cust_main->cancel_sql);
559 }
560
561 =item num_active_cust_pkg
562
563 Returns the number of active customer packages for this agent.
564
565 =cut
566
567 sub num_active_cust_pkg {
568   shift->num_pkg_sql(FS::cust_pkg->active_sql);
569 }
570
571 sub num_pkg_sql {
572   my( $self, $sql ) = @_;
573   my $statement = 
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];
579 }
580
581 =item num_inactive_cust_pkg
582
583 Returns the number of inactive customer packages (one-time packages otherwise
584 unsuspended/uncancelled) for this agent.
585
586 =cut
587
588 sub num_inactive_cust_pkg {
589   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
590 }
591
592 =item num_susp_cust_pkg
593
594 Returns the number of suspended customer packages for this agent.
595
596 =cut
597
598 sub num_susp_cust_pkg {
599   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
600 }
601
602 =item num_cancel_cust_pkg
603
604 Returns the number of cancelled customer packages for this agent.
605
606 =cut
607
608 sub num_cancel_cust_pkg {
609   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
610 }
611
612 =item generate_reg_codes NUM PKGPART_ARRAYREF
613
614 Generates the specified number of registration codes, allowing purchase of the
615 specified package definitions.  Returns an array reference of the newly
616 generated codes, or a scalar error message.
617
618 =cut
619
620 #false laziness w/prepay_credit::generate
621 sub generate_reg_codes {
622   my( $self, $num, $pkgparts ) = @_;
623
624   my @codeset = ( 'A'..'Z' );
625
626   local $SIG{HUP} = 'IGNORE';
627   local $SIG{INT} = 'IGNORE';
628   local $SIG{QUIT} = 'IGNORE';
629   local $SIG{TERM} = 'IGNORE';
630   local $SIG{TSTP} = 'IGNORE';
631   local $SIG{PIPE} = 'IGNORE';
632
633   my $oldAutoCommit = $FS::UID::AutoCommit;
634   local $FS::UID::AutoCommit = 0;
635   my $dbh = dbh;
636
637   my @codes = ();
638   for ( 1 ... $num ) {
639     my $reg_code = new FS::reg_code {
640       'agentnum' => $self->agentnum,
641       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
642     };
643     my $error = $reg_code->insert($pkgparts);
644     if ( $error ) {
645       $dbh->rollback if $oldAutoCommit;
646       return $error;
647     }
648     push @codes, $reg_code->code;
649   }
650
651   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
652
653   \@codes;
654
655 }
656
657 =item num_reg_code
658
659 Returns the number of unused registration codes for this agent.
660
661 =cut
662
663 sub num_reg_code {
664   my $self = shift;
665   my $sth = dbh->prepare(
666     "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
667   ) or die dbh->errstr;
668   $sth->execute($self->agentnum) or die $sth->errstr;
669   $sth->fetchrow_arrayref->[0];
670 }
671
672 =item num_prepay_credit
673
674 Returns the number of unused prepaid cards for this agent.
675
676 =cut
677
678 sub num_prepay_credit {
679   my $self = shift;
680   my $sth = dbh->prepare(
681     "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
682   ) or die dbh->errstr;
683   $sth->execute($self->agentnum) or die $sth->errstr;
684   $sth->fetchrow_arrayref->[0];
685 }
686
687 =item num_sales
688
689 Returns the number of non-disabled sales people for this agent.
690
691 =cut
692
693 sub num_sales {
694   my $self = shift;
695   my $sth = dbh->prepare(
696     "SELECT COUNT(*) FROM sales WHERE agentnum = ?
697                                   AND ( disabled = '' OR disabled IS NULL )"
698   ) or die dbh->errstr;
699   $sth->execute($self->agentnum) or die $sth->errstr;
700   $sth->fetchrow_arrayref->[0];
701 }
702
703 =back
704
705 =head1 BUGS
706
707 =head1 SEE ALSO
708
709 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
710 schema.html from the base documentation.
711
712 =cut
713
714 1;
715