d6171c672ddbe0cca7900cb1fb683c5134d06eca
[freeside.git] / FS / FS / agent.pm
1 package FS::agent;
2
3 use strict;
4 use base qw( FS::Commission_Mixin FS::m2m_Common FS::Record );
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 =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 =cut
183
184 sub agent_type {
185   my $self = shift;
186   qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
187 }
188
189 =item agent_cust_main
190
191 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
192 agent.
193
194 =cut
195
196 sub agent_cust_main {
197   my $self = shift;
198   qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
199 }
200
201 =item pkgpart_hashref
202
203 Returns a hash reference.  The keys of the hash are pkgparts.  The value is
204 true if this agent may purchase the specified package definition.  See
205 L<FS::part_pkg>.
206
207 =cut
208
209 sub pkgpart_hashref {
210   my $self = shift;
211   $self->agent_type->pkgpart_hashref;
212 }
213
214 =item ticketing_queue
215
216 Returns the queue name corresponding with the id from the I<ticketing_queueid>
217 field, or the empty string.
218
219 =cut
220
221 sub ticketing_queue {
222   my $self = shift;
223   FS::TicketSystem->queue($self->ticketing_queueid);
224 }
225
226 =item payment_gateway [ OPTION => VALUE, ... ]
227
228 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
229
230 Currently available options are I<nofatal>, I<invnum>, I<method>, 
231 I<payinfo>, and I<thirdparty>.
232
233 If I<nofatal> is set, and no gateway is available, then the empty string
234 will be returned instead of throwing a fatal exception.
235
236 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
237 an attempt will be made to select a gateway suited for the taxes paid on 
238 the invoice.
239
240 The I<method> and I<payinfo> options can be used to influence the choice
241 as well.  Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
242
243 When the I<method> is 'CC' then the card number in I<payinfo> can direct
244 this routine to route to a gateway suited for that type of card.
245
246 If I<thirdparty> is set, the defined self-service payment gateway will 
247 be returned.
248
249 =cut
250
251 sub payment_gateway {
252   my ( $self, %options ) = @_;
253   
254   my $conf = new FS::Conf;
255
256   if ( $options{thirdparty} ) {
257     # still a kludge, but it gets the job done
258     # and the 'cardtype' semantics don't really apply to thirdparty
259     # gateways because we have to choose a gateway without ever 
260     # seeing the card number
261     my $gatewaynum =
262       $conf->config('selfservice-payment_gateway', $self->agentnum);
263     my $gateway;
264     $gateway = FS::payment_gateway->by_key($gatewaynum) if $gatewaynum;
265     return $gateway if $gateway;
266
267     # a little less kludgey than the above, and allows PayPal to coexist 
268     # with credit card gateways
269     my $is_paypal = { op => '!=', value => 'PayPal' };
270     if ( uc($options{method}) eq 'PAYPAL' ) {
271       $is_paypal = 'PayPal';
272     }
273
274     $gateway = qsearchs({
275         table     => 'payment_gateway',
276         addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
277         hashref   => {
278           gateway_namespace => 'Business::OnlineThirdPartyPayment',
279           gateway_module    => $is_paypal,
280           disabled          => '',
281         },
282         extra_sql => ' AND agentnum = '.$self->agentnum,
283     });
284
285     if ( $gateway ) {
286       return $gateway;
287     } elsif ( $options{'nofatal'} ) {
288       return '';
289     } else {
290       die "no third-party gateway configured\n";
291     }
292   }
293
294   my $taxclass = '';
295   if ( $options{invnum} ) {
296
297     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
298     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
299
300     my @part_pkg =
301       map  { $_->part_pkg }
302       grep { $_ }
303       map  { $_->cust_pkg }
304       $cust_bill->cust_bill_pkg;
305
306     my @taxclasses = map $_->taxclass, @part_pkg;
307
308     $taxclass = $taxclasses[0]
309       unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
310                                                         #different taxclasses
311   }
312
313   #look for an agent gateway override first
314   my $cardtype = '';
315   if ( $options{method} ) {
316     if ( $options{method} eq 'CC' && $options{payinfo} ) {
317       $cardtype = cardtype($options{payinfo});
318     } elsif ( $options{method} eq 'ECHECK' ) {
319       $cardtype = 'ACH';
320     } else {
321       $cardtype = $options{method}
322     }
323   }
324
325   my $override =
326        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
327                                            cardtype => $cardtype,
328                                            taxclass => $taxclass,       } )
329     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
330                                            cardtype => '',
331                                            taxclass => $taxclass,       } )
332     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
333                                            cardtype => $cardtype,
334                                            taxclass => '',              } )
335     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
336                                            cardtype => '',
337                                            taxclass => '',              } );
338
339   my $payment_gateway;
340   if ( $override ) { #use a payment gateway override
341
342     $payment_gateway = $override->payment_gateway;
343
344     $payment_gateway->gateway_namespace('Business::OnlinePayment')
345       unless $payment_gateway->gateway_namespace;
346
347   } else { #use the standard settings from the config
348
349     # the standard settings from the config could be moved to a null agent
350     # agent_payment_gateway referenced payment_gateway
351
352     unless ( $conf->exists('business-onlinepayment') ) {
353       if ( $options{'nofatal'} ) {
354         return '';
355       } else {
356         die "Real-time processing not enabled\n";
357       }
358     }
359
360     #load up config
361     my $bop_config = 'business-onlinepayment';
362     $bop_config .= '-ach'
363       if ( $options{method}
364            && $options{method} =~ /^(ECHECK|CHEK)$/
365            && $conf->exists($bop_config. '-ach')
366          );
367     my ( $processor, $login, $password, $action, @bop_options ) =
368       $conf->config($bop_config);
369     $action ||= 'normal authorization';
370     pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
371     die "No real-time processor is enabled - ".
372         "did you set the business-onlinepayment configuration value?\n"
373       unless $processor;
374
375     $payment_gateway = new FS::payment_gateway;
376
377     $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
378                                  'Business::OnlinePayment');
379     $payment_gateway->gateway_module($processor);
380     $payment_gateway->gateway_username($login);
381     $payment_gateway->gateway_password($password);
382     $payment_gateway->gateway_action($action);
383     $payment_gateway->set('options', [ @bop_options ]);
384
385   }
386
387   unless ( $payment_gateway->gateway_namespace ) {
388     $payment_gateway->gateway_namespace(
389       scalar($conf->config('business-onlinepayment-namespace'))
390       || 'Business::OnlinePayment'
391     );
392   }
393
394   $payment_gateway;
395 }
396
397 =item invoice_modes
398
399 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
400 those with this agentnum or null agentnum).
401
402 =cut
403
404 sub invoice_modes {
405   my $self = shift;
406   qsearch( {
407       table     => 'invoice_mode',
408       hashref   => { agentnum => $self->agentnum },
409       extra_sql => ' OR agentnum IS NULL',
410       order_by  => ' ORDER BY modename',
411   } );
412 }
413
414 =item num_prospect_cust_main
415
416 Returns the number of prospects (customers with no packages ever ordered) for
417 this agent.
418
419 =cut
420
421 sub num_prospect_cust_main {
422   shift->num_sql(FS::cust_main->prospect_sql);
423 }
424
425 sub num_sql {
426   my( $self, $sql ) = @_;
427   my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
428   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
429   $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
430   $sth->fetchrow_arrayref->[0];
431 }
432
433 =item prospect_cust_main
434
435 Returns the prospects (customers with no packages ever ordered) for this agent,
436 as cust_main objects.
437
438 =cut
439
440 sub prospect_cust_main {
441   shift->cust_main_sql(FS::cust_main->prospect_sql);
442 }
443
444 sub cust_main_sql {
445   my( $self, $sql ) = @_;
446   qsearch( 'cust_main',
447            { 'agentnum' => $self->agentnum },
448            '',
449            " AND $sql"
450   );
451 }
452
453 =item num_ordered_cust_main
454
455 Returns the number of ordered customers for this agent (customers with packages
456 ordered, but not yet billed).
457
458 =cut
459
460 sub num_ordered_cust_main {
461   shift->num_sql(FS::cust_main->ordered_sql);
462 }
463
464 =item ordered_cust_main
465
466 Returns the ordered customers for this agent (customers with packages ordered,
467 but not yet billed), as cust_main objects.
468
469 =cut
470
471 sub ordered_cust_main {
472   shift->cust_main_sql(FS::cust_main->ordered_sql);
473 }
474
475
476 =item num_active_cust_main
477
478 Returns the number of active customers for this agent (customers with active
479 recurring packages).
480
481 =cut
482
483 sub num_active_cust_main {
484   shift->num_sql(FS::cust_main->active_sql);
485 }
486
487 =item active_cust_main
488
489 Returns the active customers for this agent, as cust_main objects.
490
491 =cut
492
493 sub active_cust_main {
494   shift->cust_main_sql(FS::cust_main->active_sql);
495 }
496
497 =item num_inactive_cust_main
498
499 Returns the number of inactive customers for this agent (customers with no
500 active recurring packages, but otherwise unsuspended/uncancelled).
501
502 =cut
503
504 sub num_inactive_cust_main {
505   shift->num_sql(FS::cust_main->inactive_sql);
506 }
507
508 =item inactive_cust_main
509
510 Returns the inactive customers for this agent, as cust_main objects.
511
512 =cut
513
514 sub inactive_cust_main {
515   shift->cust_main_sql(FS::cust_main->inactive_sql);
516 }
517
518
519 =item num_susp_cust_main
520
521 Returns the number of suspended customers for this agent.
522
523 =cut
524
525 sub num_susp_cust_main {
526   shift->num_sql(FS::cust_main->susp_sql);
527 }
528
529 =item susp_cust_main
530
531 Returns the suspended customers for this agent, as cust_main objects.
532
533 =cut
534
535 sub susp_cust_main {
536   shift->cust_main_sql(FS::cust_main->susp_sql);
537 }
538
539 =item num_cancel_cust_main
540
541 Returns the number of cancelled customer for this agent.
542
543 =cut
544
545 sub num_cancel_cust_main {
546   shift->num_sql(FS::cust_main->cancel_sql);
547 }
548
549 =item cancel_cust_main
550
551 Returns the cancelled customers for this agent, as cust_main objects.
552
553 =cut
554
555 sub cancel_cust_main {
556   shift->cust_main_sql(FS::cust_main->cancel_sql);
557 }
558
559 =item num_active_cust_pkg
560
561 Returns the number of active customer packages for this agent.
562
563 =cut
564
565 sub num_active_cust_pkg {
566   shift->num_pkg_sql(FS::cust_pkg->active_sql);
567 }
568
569 sub num_pkg_sql {
570   my( $self, $sql ) = @_;
571   my $statement = 
572     "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
573     " WHERE agentnum = ? AND $sql";
574   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
575   $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
576   $sth->fetchrow_arrayref->[0];
577 }
578
579 =item num_inactive_cust_pkg
580
581 Returns the number of inactive customer packages (one-time packages otherwise
582 unsuspended/uncancelled) for this agent.
583
584 =cut
585
586 sub num_inactive_cust_pkg {
587   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
588 }
589
590 =item num_susp_cust_pkg
591
592 Returns the number of suspended customer packages for this agent.
593
594 =cut
595
596 sub num_susp_cust_pkg {
597   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
598 }
599
600 =item num_cancel_cust_pkg
601
602 Returns the number of cancelled customer packages for this agent.
603
604 =cut
605
606 sub num_cancel_cust_pkg {
607   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
608 }
609
610 =item num_on_hold_cust_pkg
611
612 Returns the number of inactive customer packages (one-time packages otherwise
613 unsuspended/uncancelled) for this agent.
614
615 =cut
616
617 sub num_on_hold_cust_pkg {
618   shift->num_pkg_sql(FS::cust_pkg->on_hold_sql);
619 }
620
621 =item num_not_yet_billed_cust_pkg
622
623 Returns the number of inactive customer packages (one-time packages otherwise
624 unsuspended/uncancelled) for this agent.
625
626 =cut
627
628 sub num_not_yet_billed_cust_pkg {
629   shift->num_pkg_sql(FS::cust_pkg->not_yet_billed_sql);
630 }
631
632 =item generate_reg_codes NUM PKGPART_ARRAYREF
633
634 Generates the specified number of registration codes, allowing purchase of the
635 specified package definitions.  Returns an array reference of the newly
636 generated codes, or a scalar error message.
637
638 =cut
639
640 #false laziness w/prepay_credit::generate
641 sub generate_reg_codes {
642   my( $self, $num, $pkgparts ) = @_;
643
644   my @codeset = ( 'A'..'Z' );
645
646   local $SIG{HUP} = 'IGNORE';
647   local $SIG{INT} = 'IGNORE';
648   local $SIG{QUIT} = 'IGNORE';
649   local $SIG{TERM} = 'IGNORE';
650   local $SIG{TSTP} = 'IGNORE';
651   local $SIG{PIPE} = 'IGNORE';
652
653   my $oldAutoCommit = $FS::UID::AutoCommit;
654   local $FS::UID::AutoCommit = 0;
655   my $dbh = dbh;
656
657   my @codes = ();
658   for ( 1 ... $num ) {
659     my $reg_code = new FS::reg_code {
660       'agentnum' => $self->agentnum,
661       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
662     };
663     my $error = $reg_code->insert($pkgparts);
664     if ( $error ) {
665       $dbh->rollback if $oldAutoCommit;
666       return $error;
667     }
668     push @codes, $reg_code->code;
669   }
670
671   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
672
673   \@codes;
674
675 }
676
677 =item num_reg_code
678
679 Returns the number of unused registration codes for this agent.
680
681 =cut
682
683 sub num_reg_code {
684   my $self = shift;
685   my $sth = dbh->prepare(
686     "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
687   ) or die dbh->errstr;
688   $sth->execute($self->agentnum) or die $sth->errstr;
689   $sth->fetchrow_arrayref->[0];
690 }
691
692 =item num_prepay_credit
693
694 Returns the number of unused prepaid cards for this agent.
695
696 =cut
697
698 sub num_prepay_credit {
699   my $self = shift;
700   my $sth = dbh->prepare(
701     "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
702   ) or die dbh->errstr;
703   $sth->execute($self->agentnum) or die $sth->errstr;
704   $sth->fetchrow_arrayref->[0];
705 }
706
707 =item num_sales
708
709 Returns the number of non-disabled sales people for this agent.
710
711 =cut
712
713 sub num_sales {
714   my $self = shift;
715   my $sth = dbh->prepare(
716     "SELECT COUNT(*) FROM sales WHERE agentnum = ?
717                                   AND ( disabled = '' OR disabled IS NULL )"
718   ) or die dbh->errstr;
719   $sth->execute($self->agentnum) or die $sth->errstr;
720   $sth->fetchrow_arrayref->[0];
721 }
722
723 sub commission_where {
724   my $self = shift;
725   'cust_credit.commission_agentnum = ' . $self->agentnum;
726 }
727
728 sub sales_where {
729   my $self = shift;
730   'cust_main.agentnum = ' . $self->agentnum;
731 }
732
733 =back
734
735 =head1 BUGS
736
737 =head1 SEE ALSO
738
739 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
740 schema.html from the base documentation.
741
742 =cut
743
744 1;
745