71513: Card tokenization [v3 test tweak]
[freeside.git] / FS / FS / agent.pm
1 package FS::agent;
2
3 use strict;
4 use base qw( FS::Commission_Mixin FS::m2m_Common FS::Record );
5 #use Crypt::YAPassGen;
6 use Business::CreditCard 0.28;
7 use FS::Record qw( dbh qsearch qsearchs );
8 use FS::cust_main;
9 use FS::cust_pkg;
10 use FS::agent_type;
11 use FS::reg_code;
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 =cut
184
185 sub agent_type {
186   my $self = shift;
187   qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
188 }
189
190 =item agent_cust_main
191
192 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
193 agent.
194
195 =cut
196
197 sub agent_cust_main {
198   my $self = shift;
199   qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
200 }
201
202 =item pkgpart_hashref
203
204 Returns a hash reference.  The keys of the hash are pkgparts.  The value is
205 true if this agent may purchase the specified package definition.  See
206 L<FS::part_pkg>.
207
208 =cut
209
210 sub pkgpart_hashref {
211   my $self = shift;
212   $self->agent_type->pkgpart_hashref;
213 }
214
215 =item ticketing_queue
216
217 Returns the queue name corresponding with the id from the I<ticketing_queueid>
218 field, or the empty string.
219
220 =cut
221
222 sub ticketing_queue {
223   my $self = shift;
224   FS::TicketSystem->queue($self->ticketing_queueid);
225 }
226
227 =item payment_gateway [ OPTION => VALUE, ... ]
228
229 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
230
231 Currently available options are I<nofatal>, I<method>, I<thirdparty> and I<conf>.
232
233 If I<nofatal> is set, and no gateway is available, then the empty string
234 will be returned instead of throwing a fatal exception.
235
236 The I<method> option can be used to influence the choice
237 as well.  Presently only CHEK/ECHECK and PAYPAL methods are meaningful.
238
239 If I<method> is CHEK/ECHECK and the default gateway is being returned,
240 the business-onlinepayment-ach gateway will be returned if available.
241
242 If I<thirdparty> is set and the I<method> is PAYPAL, the defined paypal
243 gateway will be returned.
244
245 Exisisting I<$conf> may be passed for efficiency.
246
247 =cut
248
249 # opts invnum/payinfo for cardtype/taxclass overrides no longer supported
250 # any future overrides added here need to be reconciled with the tokenization process
251
252 sub payment_gateway {
253   my ( $self, %options ) = @_;
254   
255   $options{'conf'} ||= new FS::Conf;
256   my $conf = $options{'conf'};
257
258   if ( $options{thirdparty} ) {
259
260     # allows PayPal to coexist with credit card gateways
261     my $is_paypal = { op => '!=', value => 'PayPal' };
262     if ( uc($options{method}) eq 'PAYPAL' ) {
263       $is_paypal = 'PayPal';
264     }
265
266     my $gateway = qsearchs({
267         table     => 'payment_gateway',
268         addl_from => ' JOIN agent_payment_gateway USING (gatewaynum) ',
269         hashref   => {
270           gateway_namespace => 'Business::OnlineThirdPartyPayment',
271           gateway_module    => $is_paypal,
272           disabled          => '',
273         },
274         extra_sql => ' AND agentnum = '.$self->agentnum,
275     });
276
277     if ( $gateway ) {
278       return $gateway;
279     } elsif ( $options{'nofatal'} ) {
280       return '';
281     } else {
282       die "no third-party gateway configured\n";
283     }
284   }
285
286   my $override = qsearchs('agent_payment_gateway', { agentnum => $self->agentnum } );
287  
288   my $payment_gateway = FS::payment_gateway->by_key_or_default(
289     gatewaynum => $override ? $override->gatewaynum : '',
290     %options,
291   );
292
293   $payment_gateway;
294 }
295
296 =item invoice_modes
297
298 Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
299 those with this agentnum or null agentnum).
300
301 =cut
302
303 sub invoice_modes {
304   my $self = shift;
305   qsearch( {
306       table     => 'invoice_mode',
307       hashref   => { agentnum => $self->agentnum },
308       extra_sql => ' OR agentnum IS NULL',
309       order_by  => ' ORDER BY modename',
310   } );
311 }
312
313 =item num_prospect_cust_main
314
315 Returns the number of prospects (customers with no packages ever ordered) for
316 this agent.
317
318 =cut
319
320 sub num_prospect_cust_main {
321   shift->num_sql(FS::cust_main->prospect_sql);
322 }
323
324 sub num_sql {
325   my( $self, $sql ) = @_;
326   my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
327   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
328   $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
329   $sth->fetchrow_arrayref->[0];
330 }
331
332 =item prospect_cust_main
333
334 Returns the prospects (customers with no packages ever ordered) for this agent,
335 as cust_main objects.
336
337 =cut
338
339 sub prospect_cust_main {
340   shift->cust_main_sql(FS::cust_main->prospect_sql);
341 }
342
343 sub cust_main_sql {
344   my( $self, $sql ) = @_;
345   qsearch( 'cust_main',
346            { 'agentnum' => $self->agentnum },
347            '',
348            " AND $sql"
349   );
350 }
351
352 =item num_ordered_cust_main
353
354 Returns the number of ordered customers for this agent (customers with packages
355 ordered, but not yet billed).
356
357 =cut
358
359 sub num_ordered_cust_main {
360   shift->num_sql(FS::cust_main->ordered_sql);
361 }
362
363 =item ordered_cust_main
364
365 Returns the ordered customers for this agent (customers with packages ordered,
366 but not yet billed), as cust_main objects.
367
368 =cut
369
370 sub ordered_cust_main {
371   shift->cust_main_sql(FS::cust_main->ordered_sql);
372 }
373
374
375 =item num_active_cust_main
376
377 Returns the number of active customers for this agent (customers with active
378 recurring packages).
379
380 =cut
381
382 sub num_active_cust_main {
383   shift->num_sql(FS::cust_main->active_sql);
384 }
385
386 =item active_cust_main
387
388 Returns the active customers for this agent, as cust_main objects.
389
390 =cut
391
392 sub active_cust_main {
393   shift->cust_main_sql(FS::cust_main->active_sql);
394 }
395
396 =item num_inactive_cust_main
397
398 Returns the number of inactive customers for this agent (customers with no
399 active recurring packages, but otherwise unsuspended/uncancelled).
400
401 =cut
402
403 sub num_inactive_cust_main {
404   shift->num_sql(FS::cust_main->inactive_sql);
405 }
406
407 =item inactive_cust_main
408
409 Returns the inactive customers for this agent, as cust_main objects.
410
411 =cut
412
413 sub inactive_cust_main {
414   shift->cust_main_sql(FS::cust_main->inactive_sql);
415 }
416
417
418 =item num_susp_cust_main
419
420 Returns the number of suspended customers for this agent.
421
422 =cut
423
424 sub num_susp_cust_main {
425   shift->num_sql(FS::cust_main->susp_sql);
426 }
427
428 =item susp_cust_main
429
430 Returns the suspended customers for this agent, as cust_main objects.
431
432 =cut
433
434 sub susp_cust_main {
435   shift->cust_main_sql(FS::cust_main->susp_sql);
436 }
437
438 =item num_cancel_cust_main
439
440 Returns the number of cancelled customer for this agent.
441
442 =cut
443
444 sub num_cancel_cust_main {
445   shift->num_sql(FS::cust_main->cancel_sql);
446 }
447
448 =item cancel_cust_main
449
450 Returns the cancelled customers for this agent, as cust_main objects.
451
452 =cut
453
454 sub cancel_cust_main {
455   shift->cust_main_sql(FS::cust_main->cancel_sql);
456 }
457
458 =item num_active_cust_pkg
459
460 Returns the number of active customer packages for this agent.
461
462 =cut
463
464 sub num_active_cust_pkg {
465   shift->num_pkg_sql(FS::cust_pkg->active_sql);
466 }
467
468 sub num_pkg_sql {
469   my( $self, $sql ) = @_;
470   my $statement = 
471     "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
472     " WHERE agentnum = ? AND $sql";
473   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
474   $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
475   $sth->fetchrow_arrayref->[0];
476 }
477
478 =item num_inactive_cust_pkg
479
480 Returns the number of inactive customer packages (one-time packages otherwise
481 unsuspended/uncancelled) for this agent.
482
483 =cut
484
485 sub num_inactive_cust_pkg {
486   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
487 }
488
489 =item num_susp_cust_pkg
490
491 Returns the number of suspended customer packages for this agent.
492
493 =cut
494
495 sub num_susp_cust_pkg {
496   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
497 }
498
499 =item num_cancel_cust_pkg
500
501 Returns the number of cancelled customer packages for this agent.
502
503 =cut
504
505 sub num_cancel_cust_pkg {
506   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
507 }
508
509 =item num_on_hold_cust_pkg
510
511 Returns the number of inactive customer packages (one-time packages otherwise
512 unsuspended/uncancelled) for this agent.
513
514 =cut
515
516 sub num_on_hold_cust_pkg {
517   shift->num_pkg_sql(FS::cust_pkg->on_hold_sql);
518 }
519
520 =item num_not_yet_billed_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_not_yet_billed_cust_pkg {
528   shift->num_pkg_sql(FS::cust_pkg->not_yet_billed_sql);
529 }
530
531 =item generate_reg_codes NUM PKGPART_ARRAYREF
532
533 Generates the specified number of registration codes, allowing purchase of the
534 specified package definitions.  Returns an array reference of the newly
535 generated codes, or a scalar error message.
536
537 =cut
538
539 #false laziness w/prepay_credit::generate
540 sub generate_reg_codes {
541   my( $self, $num, $pkgparts ) = @_;
542
543   my @codeset = ( 'A'..'Z' );
544
545   local $SIG{HUP} = 'IGNORE';
546   local $SIG{INT} = 'IGNORE';
547   local $SIG{QUIT} = 'IGNORE';
548   local $SIG{TERM} = 'IGNORE';
549   local $SIG{TSTP} = 'IGNORE';
550   local $SIG{PIPE} = 'IGNORE';
551
552   my $oldAutoCommit = $FS::UID::AutoCommit;
553   local $FS::UID::AutoCommit = 0;
554   my $dbh = dbh;
555
556   my @codes = ();
557   for ( 1 ... $num ) {
558     my $reg_code = new FS::reg_code {
559       'agentnum' => $self->agentnum,
560       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
561     };
562     my $error = $reg_code->insert($pkgparts);
563     if ( $error ) {
564       $dbh->rollback if $oldAutoCommit;
565       return $error;
566     }
567     push @codes, $reg_code->code;
568   }
569
570   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
571
572   \@codes;
573
574 }
575
576 =item num_reg_code
577
578 Returns the number of unused registration codes for this agent.
579
580 =cut
581
582 sub num_reg_code {
583   my $self = shift;
584   my $sth = dbh->prepare(
585     "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
586   ) or die dbh->errstr;
587   $sth->execute($self->agentnum) or die $sth->errstr;
588   $sth->fetchrow_arrayref->[0];
589 }
590
591 =item num_prepay_credit
592
593 Returns the number of unused prepaid cards for this agent.
594
595 =cut
596
597 sub num_prepay_credit {
598   my $self = shift;
599   my $sth = dbh->prepare(
600     "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
601   ) or die dbh->errstr;
602   $sth->execute($self->agentnum) or die $sth->errstr;
603   $sth->fetchrow_arrayref->[0];
604 }
605
606 =item num_sales
607
608 Returns the number of non-disabled sales people for this agent.
609
610 =cut
611
612 sub num_sales {
613   my $self = shift;
614   my $sth = dbh->prepare(
615     "SELECT COUNT(*) FROM sales WHERE agentnum = ?
616                                   AND ( disabled = '' OR disabled IS NULL )"
617   ) or die dbh->errstr;
618   $sth->execute($self->agentnum) or die $sth->errstr;
619   $sth->fetchrow_arrayref->[0];
620 }
621
622 sub commission_where {
623   my $self = shift;
624   'cust_credit.commission_agentnum = ' . $self->agentnum;
625 }
626
627 sub sales_where {
628   my $self = shift;
629   'cust_main.agentnum = ' . $self->agentnum;
630 }
631
632 =back
633
634 =head1 BUGS
635
636 =head1 SEE ALSO
637
638 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
639 schema.html from the base documentation.
640
641 =cut
642
643 1;
644