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