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