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