merge webpay support in with autoselection of old realtime_bop and realtime_refund_bop
[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     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{invnum} } );
229     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
230     my @taxclasses =
231       map  { $_->part_pkg->taxclass }
232       grep { $_ }
233       map  { $_->cust_pkg }
234       $cust_bill->cust_bill_pkg;
235     unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
236                                                            #different taxclasses      $taxclass = $taxclasses[0];
237     }
238   }
239
240   #look for an agent gateway override first
241   my $cardtype;
242   if ( $options{method} && $options{method} eq 'CC' ) {
243     $cardtype = cardtype($options{payinfo});
244   } elsif ( $options{method} && $options{method} eq 'ECHECK' ) {
245     $cardtype = 'ACH';
246   } else {
247     $cardtype = $options{method} || '';
248   }
249
250   my $override =
251        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
252                                            cardtype => $cardtype,
253                                            taxclass => $taxclass,       } )
254     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
255                                            cardtype => '',
256                                            taxclass => $taxclass,       } )
257     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
258                                            cardtype => $cardtype,
259                                            taxclass => '',              } )
260     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
261                                            cardtype => '',
262                                            taxclass => '',              } );
263
264   my $payment_gateway = new FS::payment_gateway;
265   if ( $override ) { #use a payment gateway override
266
267     $payment_gateway = $override->payment_gateway;
268
269   } else { #use the standard settings from the config
270     # the standard settings from the config could be moved to a null agent
271     # agent_payment_gateway referenced payment_gateway
272
273     my $conf = new FS::Conf;
274     die "Real-time processing not enabled\n"
275       unless $conf->exists('business-onlinepayment');
276
277     #load up config
278     my $bop_config = 'business-onlinepayment';
279     $bop_config .= '-ach'
280       if ( $options{method}
281            && $options{method} =~ /^(ECHECK|CHEK)$/
282            && $conf->exists($bop_config. '-ach')
283          );
284     my ( $processor, $login, $password, $action, @bop_options ) =
285       $conf->config($bop_config);
286     $action ||= 'normal authorization';
287     pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
288     die "No real-time processor is enabled - ".
289         "did you set the business-onlinepayment configuration value?\n"
290       unless $processor;
291
292     $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
293                                  'Business::OnlinePayment');
294     $payment_gateway->gateway_module($processor);
295     $payment_gateway->gateway_username($login);
296     $payment_gateway->gateway_password($password);
297     $payment_gateway->gateway_action($action);
298     $payment_gateway->set('options', [ @bop_options ]);
299
300   }
301
302   $payment_gateway;
303 }
304
305 =item num_prospect_cust_main
306
307 Returns the number of prospects (customers with no packages ever ordered) for
308 this agent.
309
310 =cut
311
312 sub num_prospect_cust_main {
313   shift->num_sql(FS::cust_main->prospect_sql);
314 }
315
316 sub num_sql {
317   my( $self, $sql ) = @_;
318   my $statement = "SELECT COUNT(*) FROM cust_main WHERE agentnum = ? AND $sql";
319   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
320   $sth->execute($self->agentnum) or die $sth->errstr. " executing $statement";
321   $sth->fetchrow_arrayref->[0];
322 }
323
324 =item prospect_cust_main
325
326 Returns the prospects (customers with no packages ever ordered) for this agent,
327 as cust_main objects.
328
329 =cut
330
331 sub prospect_cust_main {
332   shift->cust_main_sql(FS::cust_main->prospect_sql);
333 }
334
335 sub cust_main_sql {
336   my( $self, $sql ) = @_;
337   qsearch( 'cust_main',
338            { 'agentnum' => $self->agentnum },
339            '',
340            " AND $sql"
341   );
342 }
343
344 =item num_active_cust_main
345
346 Returns the number of active customers for this agent (customers with active
347 recurring packages).
348
349 =cut
350
351 sub num_active_cust_main {
352   shift->num_sql(FS::cust_main->active_sql);
353 }
354
355 =item active_cust_main
356
357 Returns the active customers for this agent, as cust_main objects.
358
359 =cut
360
361 sub active_cust_main {
362   shift->cust_main_sql(FS::cust_main->active_sql);
363 }
364
365 =item num_inactive_cust_main
366
367 Returns the number of inactive customers for this agent (customers with no
368 active recurring packages, but otherwise unsuspended/uncancelled).
369
370 =cut
371
372 sub num_inactive_cust_main {
373   shift->num_sql(FS::cust_main->inactive_sql);
374 }
375
376 =item inactive_cust_main
377
378 Returns the inactive customers for this agent, as cust_main objects.
379
380 =cut
381
382 sub inactive_cust_main {
383   shift->cust_main_sql(FS::cust_main->inactive_sql);
384 }
385
386
387 =item num_susp_cust_main
388
389 Returns the number of suspended customers for this agent.
390
391 =cut
392
393 sub num_susp_cust_main {
394   shift->num_sql(FS::cust_main->susp_sql);
395 }
396
397 =item susp_cust_main
398
399 Returns the suspended customers for this agent, as cust_main objects.
400
401 =cut
402
403 sub susp_cust_main {
404   shift->cust_main_sql(FS::cust_main->susp_sql);
405 }
406
407 =item num_cancel_cust_main
408
409 Returns the number of cancelled customer for this agent.
410
411 =cut
412
413 sub num_cancel_cust_main {
414   shift->num_sql(FS::cust_main->cancel_sql);
415 }
416
417 =item cancel_cust_main
418
419 Returns the cancelled customers for this agent, as cust_main objects.
420
421 =cut
422
423 sub cancel_cust_main {
424   shift->cust_main_sql(FS::cust_main->cancel_sql);
425 }
426
427 =item num_active_cust_pkg
428
429 Returns the number of active customer packages for this agent.
430
431 =cut
432
433 sub num_active_cust_pkg {
434   shift->num_pkg_sql(FS::cust_pkg->active_sql);
435 }
436
437 sub num_pkg_sql {
438   my( $self, $sql ) = @_;
439   my $statement = 
440     "SELECT COUNT(*) FROM cust_pkg LEFT JOIN cust_main USING ( custnum )".
441     " WHERE agentnum = ? AND $sql";
442   my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
443   $sth->execute($self->agentnum) or die $sth->errstr. "executing $statement";
444   $sth->fetchrow_arrayref->[0];
445 }
446
447 =item num_inactive_cust_pkg
448
449 Returns the number of inactive customer packages (one-time packages otherwise
450 unsuspended/uncancelled) for this agent.
451
452 =cut
453
454 sub num_inactive_cust_pkg {
455   shift->num_pkg_sql(FS::cust_pkg->inactive_sql);
456 }
457
458 =item num_susp_cust_pkg
459
460 Returns the number of suspended customer packages for this agent.
461
462 =cut
463
464 sub num_susp_cust_pkg {
465   shift->num_pkg_sql(FS::cust_pkg->susp_sql);
466 }
467
468 =item num_cancel_cust_pkg
469
470 Returns the number of cancelled customer packages for this agent.
471
472 =cut
473
474 sub num_cancel_cust_pkg {
475   shift->num_pkg_sql(FS::cust_pkg->cancel_sql);
476 }
477
478 =item generate_reg_codes NUM PKGPART_ARRAYREF
479
480 Generates the specified number of registration codes, allowing purchase of the
481 specified package definitions.  Returns an array reference of the newly
482 generated codes, or a scalar error message.
483
484 =cut
485
486 #false laziness w/prepay_credit::generate
487 sub generate_reg_codes {
488   my( $self, $num, $pkgparts ) = @_;
489
490   my @codeset = ( 'A'..'Z' );
491
492   local $SIG{HUP} = 'IGNORE';
493   local $SIG{INT} = 'IGNORE';
494   local $SIG{QUIT} = 'IGNORE';
495   local $SIG{TERM} = 'IGNORE';
496   local $SIG{TSTP} = 'IGNORE';
497   local $SIG{PIPE} = 'IGNORE';
498
499   my $oldAutoCommit = $FS::UID::AutoCommit;
500   local $FS::UID::AutoCommit = 0;
501   my $dbh = dbh;
502
503   my @codes = ();
504   for ( 1 ... $num ) {
505     my $reg_code = new FS::reg_code {
506       'agentnum' => $self->agentnum,
507       'code'     => join('', map($codeset[int(rand $#codeset)], (0..7) ) ),
508     };
509     my $error = $reg_code->insert($pkgparts);
510     if ( $error ) {
511       $dbh->rollback if $oldAutoCommit;
512       return $error;
513     }
514     push @codes, $reg_code->code;
515   }
516
517   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
518
519   \@codes;
520
521 }
522
523 =item num_reg_code
524
525 Returns the number of unused registration codes for this agent.
526
527 =cut
528
529 sub num_reg_code {
530   my $self = shift;
531   my $sth = dbh->prepare(
532     "SELECT COUNT(*) FROM reg_code WHERE agentnum = ?"
533   ) or die dbh->errstr;
534   $sth->execute($self->agentnum) or die $sth->errstr;
535   $sth->fetchrow_arrayref->[0];
536 }
537
538 =item num_prepay_credit
539
540 Returns the number of unused prepaid cards for this agent.
541
542 =cut
543
544 sub num_prepay_credit {
545   my $self = shift;
546   my $sth = dbh->prepare(
547     "SELECT COUNT(*) FROM prepay_credit WHERE agentnum = ?"
548   ) or die dbh->errstr;
549   $sth->execute($self->agentnum) or die $sth->errstr;
550   $sth->fetchrow_arrayref->[0];
551 }
552
553
554 =back
555
556 =head1 BUGS
557
558 =head1 SEE ALSO
559
560 L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
561 schema.html from the base documentation.
562
563 =cut
564
565 1;
566