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