Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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> and I<conf>.
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 Exisisting I<$conf> may be passed for efficiency.
257
258 =cut
259
260 # opts invnum/payinfo for cardtype/taxclass overrides no longer supported
261 # any future overrides added here need to be reconciled with the tokenization process
262
263 sub payment_gateway {
264   my ( $self, %options ) = @_;
265   
266   $options{'conf'} ||= new FS::Conf;
267   my $conf = $options{'conf'};
268
269   if ( $options{thirdparty} ) {
270
271     # allows PayPal to coexist with credit card gateways
272     my $is_paypal = { op => '!=', value => 'PayPal' };
273     if ( uc($options{method}) eq 'PAYPAL' ) {
274       $is_paypal = 'PayPal';
275     }
276
277     my $gateway = qsearchs({
278         table     => 'payment_gateway',
279         addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
280         hashref   => {
281           gateway_namespace => 'Business::OnlineThirdPartyPayment',
282           gateway_module    => $is_paypal,
283           disabled          => '',
284         },
285         extra_sql => ' AND agentnum = '.$self->agentnum,
286     });
287
288     if ( $gateway ) {
289       return $gateway;
290     } elsif ( $options{'nofatal'} ) {
291       return '';
292     } else {
293       die "no third-party gateway configured\n";
294     }
295   }
296
297   my $override = qsearchs('agent_payment_gateway', { agentnum => $self->agentnum } );
298
299   my $payment_gateway = FS::payment_gateway->by_key_or_default(
300     gatewaynum => $override ? $override->gatewaynum : '',
301     %options,
302   );
303
304   $payment_gateway;
305 }
306
307 =item invoice_modes
308
309 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
310 those with this agentnum or null agentnum).
311
312 =cut
313
314 sub invoice_modes {
315   my $self = shift;
316   qsearch( {
317       table     => 'invoice_mode',
318       hashref   => { agentnum => $self->agentnum },
319       extra_sql => ' OR agentnum IS NULL',
320       order_by  => ' ORDER BY modename',
321   } );
322 }
323
324 =item num_prospect_cust_main
325
326 Returns the number of prospects (customers with no packages ever ordered) for
327 this agent.
328
329 =cut
330
331 sub num_prospect_cust_main {
332   shift->num_sql(FS::cust_main->prospect_sql);
333 }
334
335 sub num_sql {
336   my( $self, $sql ) = @_;
337   my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
338   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
339   $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
340   $sth->fetchrow_arrayref->[0];
341 }
342
343 =item prospect_cust_main
344
345 Returns the prospects (customers with no packages ever ordered) for this agent,
346 as cust_main objects.
347
348 =cut
349
350 sub prospect_cust_main {
351   shift->cust_main_sql(FS::cust_main->prospect_sql);
352 }
353
354 sub cust_main_sql {
355   my( $self, $sql ) = @_;
356   qsearch( 'cust_main',
357            { 'agentnum' => $self->agentnum },
358            '',
359            " AND $sql"
360   );
361 }
362
363 =item num_ordered_cust_main
364
365 Returns the number of ordered customers for this agent (customers with packages
366 ordered, but not yet billed).
367
368 =cut
369
370 sub num_ordered_cust_main {
371   shift->num_sql(FS::cust_main->ordered_sql);
372 }
373
374 =item ordered_cust_main
375
376 Returns the ordered customers for this agent (customers with packages ordered,
377 but not yet billed), as cust_main objects.
378
379 =cut
380
381 sub ordered_cust_main {
382   shift->cust_main_sql(FS::cust_main->ordered_sql);
383 }
384
385
386 =item num_active_cust_main
387
388 Returns the number of active customers for this agent (customers with active
389 recurring packages).
390
391 =cut
392
393 sub num_active_cust_main {
394   shift->num_sql(FS::cust_main->active_sql);
395 }
396
397 =item active_cust_main
398
399 Returns the active customers for this agent, as cust_main objects.
400
401 =cut
402
403 sub active_cust_main {
404   shift->cust_main_sql(FS::cust_main->active_sql);
405 }
406
407 =item num_inactive_cust_main
408
409 Returns the number of inactive customers for this agent (customers with no
410 active recurring packages, but otherwise unsuspended/uncancelled).
411
412 =cut
413
414 sub num_inactive_cust_main {
415   shift->num_sql(FS::cust_main->inactive_sql);
416 }
417
418 =item inactive_cust_main
419
420 Returns the inactive customers for this agent, as cust_main objects.
421
422 =cut
423
424 sub inactive_cust_main {
425   shift->cust_main_sql(FS::cust_main->inactive_sql);
426 }
427
428
429 =item num_susp_cust_main
430
431 Returns the number of suspended customers for this agent.
432
433 =cut
434
435 sub num_susp_cust_main {
436   shift->num_sql(FS::cust_main->susp_sql);
437 }
438
439 =item susp_cust_main
440
441 Returns the suspended customers for this agent, as cust_main objects.
442
443 =cut
444
445 sub susp_cust_main {
446   shift->cust_main_sql(FS::cust_main->susp_sql);
447 }
448
449 =item num_cancel_cust_main
450
451 Returns the number of cancelled customer for this agent.
452
453 =cut
454
455 sub num_cancel_cust_main {
456   shift->num_sql(FS::cust_main->cancel_sql);
457 }
458
459 =item cancel_cust_main
460
461 Returns the cancelled customers for this agent, as cust_main objects.
462
463 =cut
464
465 sub cancel_cust_main {
466   shift->cust_main_sql(FS::cust_main->cancel_sql);
467 }
468
469 =item num_active_cust_pkg
470
471 Returns the number of active customer packages for this agent.
472
473 =cut
474
475 sub num_active_cust_pkg {
476   shift->num_pkg_sql(FS::cust_pkg->active_sql);
477 }
478
479 sub num_pkg_sql {
480   my( $self, $sql ) = @_;
481   my $statement = 
482     "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
483     " WHERE agentnum = ? AND $sql";
484   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
485   $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
486   $sth->fetchrow_arrayref->[0];
487 }
488
489 =item num_inactive_cust_pkg
490
491 Returns the number of inactive customer packages (one-time packages otherwise
492 unsuspended/uncancelled) for this agent.
493
494 =cut
495
496 sub num_inactive_cust_pkg {
497   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
498 }
499
500 =item num_susp_cust_pkg
501
502 Returns the number of suspended customer packages for this agent.
503
504 =cut
505
506 sub num_susp_cust_pkg {
507   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
508 }
509
510 =item num_cancel_cust_pkg
511
512 Returns the number of cancelled customer packages for this agent.
513
514 =cut
515
516 sub num_cancel_cust_pkg {
517   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
518 }
519
520 =item num_on_hold_cust_pkg
521
522 Returns the number of inactive customer packages (one-time packages otherwise
523 unsuspended/uncancelled) for this agent.
524
525 =cut
526
527 sub num_on_hold_cust_pkg {
528   shift->num_pkg_sql(FS::cust_pkg->on_hold_sql);
529 }
530
531 =item num_not_yet_billed_cust_pkg
532
533 Returns the number of inactive customer packages (one-time packages otherwise
534 unsuspended/uncancelled) for this agent.
535
536 =cut
537
538 sub num_not_yet_billed_cust_pkg {
539   shift->num_pkg_sql(FS::cust_pkg->not_yet_billed_sql);
540 }
541
542 =item generate_reg_codes NUM PKGPART_ARRAYREF
543
544 Generates the specified number of registration codes, allowing purchase of the
545 specified package definitions.  Returns an array reference of the newly
546 generated codes, or a scalar error message.
547
548 =cut
549
550 #false laziness w/prepay_credit::generate
551 sub generate_reg_codes {
552   my( $self, $num, $pkgparts ) = @_;
553
554   my @codeset = ( 'A'..'Z' );
555
556   local $SIG{HUP} = 'IGNORE';
557   local $SIG{INT} = 'IGNORE';
558   local $SIG{QUIT} = 'IGNORE';
559   local $SIG{TERM} = 'IGNORE';
560   local $SIG{TSTP} = 'IGNORE';
561   local $SIG{PIPE} = 'IGNORE';
562
563   my $oldAutoCommit = $FS::UID::AutoCommit;
564   local $FS::UID::AutoCommit = 0;
565   my $dbh = dbh;
566
567   my @codes = ();
568   for ( 1 ... $num ) {
569     my $reg_code = new FS::reg_code {
570       'agentnum' => $self->agentnum,
571       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
572     };
573     my $error = $reg_code->insert($pkgparts);
574     if ( $error ) {
575       $dbh->rollback if $oldAutoCommit;
576       return $error;
577     }
578     push @codes, $reg_code->code;
579   }
580
581   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
582
583   \@codes;
584
585 }
586
587 =item num_reg_code
588
589 Returns the number of unused registration codes for this agent.
590
591 =cut
592
593 sub num_reg_code {
594   my $self = shift;
595   my $sth = dbh->prepare(
596     "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
597   ) or die dbh->errstr;
598   $sth->execute($self->agentnum) or die $sth->errstr;
599   $sth->fetchrow_arrayref->[0];
600 }
601
602 =item num_prepay_credit
603
604 Returns the number of unused prepaid cards for this agent.
605
606 =cut
607
608 sub num_prepay_credit {
609   my $self = shift;
610   my $sth = dbh->prepare(
611     "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
612   ) or die dbh->errstr;
613   $sth->execute($self->agentnum) or die $sth->errstr;
614   $sth->fetchrow_arrayref->[0];
615 }
616
617 =item num_sales
618
619 Returns the number of non-disabled sales people for this agent.
620
621 =cut
622
623 sub num_sales {
624   my $self = shift;
625   my $sth = dbh->prepare(
626     "SELECT COUNT(*) FROM sales WHERE agentnum = ?
627                                   AND ( disabled = '' OR disabled IS NULL )"
628   ) or die dbh->errstr;
629   $sth->execute($self->agentnum) or die $sth->errstr;
630   $sth->fetchrow_arrayref->[0];
631 }
632
633 sub commission_where {
634   my $self = shift;
635   'cust_credit.commission_agentnum = ' . $self->agentnum;
636 }
637
638 sub sales_where {
639   my $self = shift;
640   'cust_main.agentnum = ' . $self->agentnum;
641 }
642
643 =back
644
645 =head1 BUGS
646
647 =head1 SEE ALSO
648
649 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
650 schema.html from the base documentation.
651
652 =cut
653
654 1;
655