5fbe2c46d5d69f41c65563be681ce490a88d256b
[freeside.git] / FS / FS / agent.pm
1 package FS::agent;
2
3 use strict;
4 use vars qw( @ISA );
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::TicketSystem;
13 use FS::Conf;
14
15 @ISA = qw( FS::m2m_Common FS::Record );
16
17 =head1 NAME
18
19 FS::agent - Object methods for agent records
20
21 =head1 SYNOPSIS
22
23   use FS::agent;
24
25   $record = new FS::agent \%hash;
26   $record = new FS::agent { 'column' => 'value' };
27
28   $error = $record->insert;
29
30   $error = $new_record->replace($old_record);
31
32   $error = $record->delete;
33
34   $error = $record->check;
35
36   $agent_type = $record->agent_type;
37
38   $hashref = $record->pkgpart_hashref;
39   #may purchase $pkgpart if $hashref->{$pkgpart};
40
41 =head1 DESCRIPTION
42
43 An FS::agent object represents an agent.  Every customer has an agent.  Agents
44 can be used to track things like resellers or salespeople.  FS::agent inherits
45 from FS::Record.  The following fields are currently supported:
46
47 =over 4
48
49 =item agentnum - primary key (assigned automatically for new agents)
50
51 =item agent - Text name of this agent
52
53 =item typenum - Agent type (see L<FS::agent_type>)
54
55 =item ticketing_queueid - Ticketing Queue
56
57 =item invoice_template - Invoice template name
58
59 =item agent_custnum - Optional agent customer (see L<FS::cust_main>)
60
61 =item disabled - Disabled flag, empty or 'Y'
62
63 =item prog - Deprecated (never used)
64
65 =item freq - Deprecated (never used)
66
67 =item username - (Deprecated) Username for the Agent interface
68
69 =item _password - (Deprecated) Password for the Agent interface
70
71 =back
72
73 =head1 METHODS
74
75 =over 4
76
77 =item new HASHREF
78
79 Creates a new agent.  To add the agent to the database, see L<"insert">.
80
81 =cut
82
83 sub table { 'agent'; }
84
85 =item insert
86
87 Adds this agent to the database.  If there is an error, returns the error,
88 otherwise returns false.
89
90 =item delete
91
92 Deletes this agent from the database.  Only agents with no customers can be
93 deleted.  If there is an error, returns the error, otherwise returns false.
94
95 =cut
96
97 sub delete {
98   my $self = shift;
99
100   return "Can't delete an agent with customers!"
101     if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
102
103   $self->SUPER::delete;
104 }
105
106 =item replace OLD_RECORD
107
108 Replaces OLD_RECORD with this one in the database.  If there is an error,
109 returns the error, otherwise returns false.
110
111 =item check
112
113 Checks all fields to make sure this is a valid agent.  If there is an error,
114 returns the error, otherwise returns false.  Called by the insert and replace
115 methods.
116
117 =cut
118
119 sub check {
120   my $self = shift;
121
122   my $error =
123     $self->ut_numbern('agentnum')
124       || $self->ut_text('agent')
125       || $self->ut_number('typenum')
126       || $self->ut_numbern('freq')
127       || $self->ut_textn('prog')
128       || $self->ut_textn('invoice_template')
129       || $self->ut_foreign_keyn('agent_custnum', 'cust_main', 'custnum' )
130   ;
131   return $error if $error;
132
133   if ( $self->dbdef_table->column('disabled') ) {
134     $error = $self->ut_enum('disabled', [ '', 'Y' ] );
135     return $error if $error;
136   }
137
138   if ( $self->dbdef_table->column('username') ) {
139     $error = $self->ut_alphan('username');
140     return $error if $error;
141     if ( length($self->username) ) {
142       my $conflict = qsearchs('agent', { 'username' => $self->username } );
143       return 'duplicate agent username (with '. $conflict->agent. ')'
144         if $conflict && $conflict->agentnum != $self->agentnum;
145       $error = $self->ut_text('password'); # ut_text... arbitrary choice
146     } else {
147       $self->_password('');
148     }
149   }
150
151   return "Unknown typenum!"
152     unless $self->agent_type;
153
154   $self->SUPER::check;
155 }
156
157 =item agent_type
158
159 Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
160
161 =cut
162
163 sub agent_type {
164   my $self = shift;
165   qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
166 }
167
168 =item agent_cust_main
169
170 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
171 agent.
172
173 =cut
174
175 sub agent_cust_main {
176   my $self = shift;
177   qsearchs( 'cust_main', { 'custnum' => $self->agent_custnum } );
178 }
179
180 =item pkgpart_hashref
181
182 Returns a hash reference.  The keys of the hash are pkgparts.  The value is
183 true if this agent may purchase the specified package definition.  See
184 L<FS::part_pkg>.
185
186 =cut
187
188 sub pkgpart_hashref {
189   my $self = shift;
190   $self->agent_type->pkgpart_hashref;
191 }
192
193 =item ticketing_queue
194
195 Returns the queue name corresponding with the id from the I<ticketing_queueid>
196 field, or the empty string.
197
198 =cut
199
200 sub ticketing_queue {
201   my $self = shift;
202   FS::TicketSystem->queue($self->ticketing_queueid);
203 };
204
205 =item payment_gateway [ OPTION => VALUE, ... ]
206
207 Returns a payment gateway object (see L<FS::payment_gateway>) for this agent.
208
209 Currently available options are I<invnum>, I<method>, and I<payinfo>.
210
211 If I<invnum> is set to the number of an invoice (see L<FS::cust_bill>) then
212 an attempt will be made to select a gateway suited for the taxes paid on 
213 the invoice.
214
215 The I<method> and I<payinfo> options can be used to influence the choice
216 as well.  Presently only 'CC' and 'ECHECK' methods are meaningful.
217
218 When the I<method> is 'CC' then the card number in I<payinfo> can direct
219 this routine to route to a gateway suited for that type of card.
220
221 =cut
222
223 sub payment_gateway {
224   my ( $self, %options ) = @_;
225
226   my $taxclass = '';
227   if ( $options{invnum} ) {
228
229     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
230     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
231
232     my @part_pkg =
233       map  { $_->part_pkg }
234       grep { $_ }
235       map  { $_->cust_pkg }
236       $cust_bill->cust_bill_pkg;
237
238     my @taxclasses = map $_->taxclass, @part_pkg;
239
240     $taxclass = $taxclasses[0]
241       unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
242                                                         #different taxclasses
243   }
244
245   #look for an agent gateway override first
246   my $cardtype;
247   if ( $options{method} && $options{method} eq 'CC' ) {
248     $cardtype = cardtype($options{payinfo});
249   } elsif ( $options{method} && $options{method} eq 'ECHECK' ) {
250     $cardtype = 'ACH';
251   } else {
252     $cardtype = $options{method} || '';
253   }
254
255   my $override =
256        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
257                                            cardtype => $cardtype,
258                                            taxclass => $taxclass,       } )
259     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
260                                            cardtype => '',
261                                            taxclass => $taxclass,       } )
262     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
263                                            cardtype => $cardtype,
264                                            taxclass => '',              } )
265     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
266                                            cardtype => '',
267                                            taxclass => '',              } );
268
269   my $payment_gateway = new FS::payment_gateway;
270   if ( $override ) { #use a payment gateway override
271
272     $payment_gateway = $override->payment_gateway;
273
274   } else { #use the standard settings from the config
275     # the standard settings from the config could be moved to a null agent
276     # agent_payment_gateway referenced payment_gateway
277
278     my $conf = new FS::Conf;
279     die "Real-time processing not enabled\n"
280       unless $conf->exists('business-onlinepayment');
281
282     #load up config
283     my $bop_config = 'business-onlinepayment';
284     $bop_config .= '-ach'
285       if ( $options{method}
286            && $options{method} =~ /^(ECHECK|CHEK)$/
287            && $conf->exists($bop_config. '-ach')
288          );
289     my ( $processor, $login, $password, $action, @bop_options ) =
290       $conf->config($bop_config);
291     $action ||= 'normal authorization';
292     pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
293     die "No real-time processor is enabled - ".
294         "did you set the business-onlinepayment configuration value?\n"
295       unless $processor;
296
297     $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
298                                  'Business::OnlinePayment');
299     $payment_gateway->gateway_module($processor);
300     $payment_gateway->gateway_username($login);
301     $payment_gateway->gateway_password($password);
302     $payment_gateway->gateway_action($action);
303     $payment_gateway->set('options', [ @bop_options ]);
304
305   }
306
307   $payment_gateway;
308 }
309
310 =item num_prospect_cust_main
311
312 Returns the number of prospects (customers with no packages ever ordered) for
313 this agent.
314
315 =cut
316
317 sub num_prospect_cust_main {
318   shift->num_sql(FS::cust_main->prospect_sql);
319 }
320
321 sub num_sql {
322   my( $self, $sql ) = @_;
323   my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
324   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
325   $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
326   $sth->fetchrow_arrayref->[0];
327 }
328
329 =item prospect_cust_main
330
331 Returns the prospects (customers with no packages ever ordered) for this agent,
332 as cust_main objects.
333
334 =cut
335
336 sub prospect_cust_main {
337   shift->cust_main_sql(FS::cust_main->prospect_sql);
338 }
339
340 sub cust_main_sql {
341   my( $self, $sql ) = @_;
342   qsearch( 'cust_main',
343            { 'agentnum' => $self->agentnum },
344            '',
345            " AND $sql"
346   );
347 }
348
349 =item num_active_cust_main
350
351 Returns the number of active customers for this agent (customers with active
352 recurring packages).
353
354 =cut
355
356 sub num_active_cust_main {
357   shift->num_sql(FS::cust_main->active_sql);
358 }
359
360 =item active_cust_main
361
362 Returns the active customers for this agent, as cust_main objects.
363
364 =cut
365
366 sub active_cust_main {
367   shift->cust_main_sql(FS::cust_main->active_sql);
368 }
369
370 =item num_inactive_cust_main
371
372 Returns the number of inactive customers for this agent (customers with no
373 active recurring packages, but otherwise unsuspended/uncancelled).
374
375 =cut
376
377 sub num_inactive_cust_main {
378   shift->num_sql(FS::cust_main->inactive_sql);
379 }
380
381 =item inactive_cust_main
382
383 Returns the inactive customers for this agent, as cust_main objects.
384
385 =cut
386
387 sub inactive_cust_main {
388   shift->cust_main_sql(FS::cust_main->inactive_sql);
389 }
390
391
392 =item num_susp_cust_main
393
394 Returns the number of suspended customers for this agent.
395
396 =cut
397
398 sub num_susp_cust_main {
399   shift->num_sql(FS::cust_main->susp_sql);
400 }
401
402 =item susp_cust_main
403
404 Returns the suspended customers for this agent, as cust_main objects.
405
406 =cut
407
408 sub susp_cust_main {
409   shift->cust_main_sql(FS::cust_main->susp_sql);
410 }
411
412 =item num_cancel_cust_main
413
414 Returns the number of cancelled customer for this agent.
415
416 =cut
417
418 sub num_cancel_cust_main {
419   shift->num_sql(FS::cust_main->cancel_sql);
420 }
421
422 =item cancel_cust_main
423
424 Returns the cancelled customers for this agent, as cust_main objects.
425
426 =cut
427
428 sub cancel_cust_main {
429   shift->cust_main_sql(FS::cust_main->cancel_sql);
430 }
431
432 =item num_active_cust_pkg
433
434 Returns the number of active customer packages for this agent.
435
436 =cut
437
438 sub num_active_cust_pkg {
439   shift->num_pkg_sql(FS::cust_pkg->active_sql);
440 }
441
442 sub num_pkg_sql {
443   my( $self, $sql ) = @_;
444   my $statement = 
445     "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
446     " WHERE agentnum = ? AND $sql";
447   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
448   $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
449   $sth->fetchrow_arrayref->[0];
450 }
451
452 =item num_inactive_cust_pkg
453
454 Returns the number of inactive customer packages (one-time packages otherwise
455 unsuspended/uncancelled) for this agent.
456
457 =cut
458
459 sub num_inactive_cust_pkg {
460   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
461 }
462
463 =item num_susp_cust_pkg
464
465 Returns the number of suspended customer packages for this agent.
466
467 =cut
468
469 sub num_susp_cust_pkg {
470   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
471 }
472
473 =item num_cancel_cust_pkg
474
475 Returns the number of cancelled customer packages for this agent.
476
477 =cut
478
479 sub num_cancel_cust_pkg {
480   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
481 }
482
483 =item generate_reg_codes NUM PKGPART_ARRAYREF
484
485 Generates the specified number of registration codes, allowing purchase of the
486 specified package definitions.  Returns an array reference of the newly
487 generated codes, or a scalar error message.
488
489 =cut
490
491 #false laziness w/prepay_credit::generate
492 sub generate_reg_codes {
493   my( $self, $num, $pkgparts ) = @_;
494
495   my @codeset = ( 'A'..'Z' );
496
497   local $SIG{HUP} = 'IGNORE';
498   local $SIG{INT} = 'IGNORE';
499   local $SIG{QUIT} = 'IGNORE';
500   local $SIG{TERM} = 'IGNORE';
501   local $SIG{TSTP} = 'IGNORE';
502   local $SIG{PIPE} = 'IGNORE';
503
504   my $oldAutoCommit = $FS::UID::AutoCommit;
505   local $FS::UID::AutoCommit = 0;
506   my $dbh = dbh;
507
508   my @codes = ();
509   for ( 1 ... $num ) {
510     my $reg_code = new FS::reg_code {
511       'agentnum' => $self->agentnum,
512       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
513     };
514     my $error = $reg_code->insert($pkgparts);
515     if ( $error ) {
516       $dbh->rollback if $oldAutoCommit;
517       return $error;
518     }
519     push @codes, $reg_code->code;
520   }
521
522   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
523
524   \@codes;
525
526 }
527
528 =item num_reg_code
529
530 Returns the number of unused registration codes for this agent.
531
532 =cut
533
534 sub num_reg_code {
535   my $self = shift;
536   my $sth = dbh->prepare(
537     "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
538   ) or die dbh->errstr;
539   $sth->execute($self->agentnum) or die $sth->errstr;
540   $sth->fetchrow_arrayref->[0];
541 }
542
543 =item num_prepay_credit
544
545 Returns the number of unused prepaid cards for this agent.
546
547 =cut
548
549 sub num_prepay_credit {
550   my $self = shift;
551   my $sth = dbh->prepare(
552     "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
553   ) or die dbh->errstr;
554   $sth->execute($self->agentnum) or die $sth->errstr;
555   $sth->fetchrow_arrayref->[0];
556 }
557
558
559 =back
560
561 =head1 BUGS
562
563 =head1 SEE ALSO
564
565 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
566 schema.html from the base documentation.
567
568 =cut
569
570 1;
571