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