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