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