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