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