RT# 34134 - added documentation for new feature
[freeside.git] / FS / FS / payment_gateway.pm
1 package FS::payment_gateway;
2 use base qw( FS::option_Common );
3
4 use strict;
5 use vars qw( $me $DEBUG );
6 use FS::Record qw( qsearch dbh ); #qw( qsearch qsearchs dbh );
7
8 $me = '[ FS::payment_gateway ]';
9 $DEBUG=0;
10
11 =head1 NAME
12
13 FS::payment_gateway - Object methods for payment_gateway records
14
15 =head1 SYNOPSIS
16
17   use FS::payment_gateway;
18
19   $record = new FS::payment_gateway \%hash;
20   $record = new FS::payment_gateway { 'column' => 'value' };
21
22   $error = $record->insert;
23
24   $error = $new_record->replace($old_record);
25
26   $error = $record->delete;
27
28   $error = $record->check;
29
30 =head1 DESCRIPTION
31
32 An FS::payment_gateway object represents an payment gateway.
33 FS::payment_gateway inherits from FS::Record.  The following fields are
34 currently supported:
35
36 =over 4
37
38 =item gatewaynum - primary key
39
40 =item gateway_namespace - Business::OnlinePayment, Business::OnlineThirdPartyPayment, or Business::BatchPayment
41
42 =item gateway_module - Business::OnlinePayment:: (or other) module name
43
44 =item gateway_username - payment gateway username
45
46 =item gateway_password - payment gateway password
47
48 =item gateway_action - optional action or actions (multiple actions are separated with `,': for example: `Authorization Only, Post Authorization').  Defaults to `Normal Authorization'.
49
50 =item disabled - Disabled flag, empty or 'Y'
51
52 =item gateway_callback_url - For ThirdPartyPayment only, set to the URL that 
53 the user should be redirected to on a successful payment.  This will be sent
54 as a transaction parameter named "return_url".
55
56 =item gateway_cancel_url - For ThirdPartyPayment only, set to the URL that 
57 the user should be redirected to if they cancel the transaction.  This will 
58 be sent as a transaction parameter named "cancel_url".
59
60 =item auto_resolve_status - For BatchPayment only, set to 'approve' to 
61 auto-approve unresolved payments after some number of days, 'reject' to 
62 auto-decline them, or null to do nothing.
63
64 =item auto_resolve_days - For BatchPayment, the number of days to wait before 
65 auto-resolving the batch.
66
67 =back
68
69 =head1 METHODS
70
71 =over 4
72
73 =item new HASHREF
74
75 Creates a new payment gateway.  To add the payment gateway to the database, see
76 L<"insert">.
77
78 Note that this stores the hash reference, not a distinct copy of the hash it
79 points to.  You can ask the object for a copy with the I<hash> method.
80
81 =cut
82
83 # the new method can be inherited from FS::Record, if a table method is defined
84
85 sub table { 'payment_gateway'; }
86
87 =item insert
88
89 Adds this record to the database.  If there is an error, returns the error,
90 otherwise returns false.
91
92 =cut
93
94 # the insert method can be inherited from FS::Record
95
96 =item delete
97
98 Delete this record from the database.
99
100 =cut
101
102 # the delete method can be inherited from FS::Record
103
104 =item replace OLD_RECORD
105
106 Replaces the OLD_RECORD with this one in the database.  If there is an error,
107 returns the error, otherwise returns false.
108
109 =cut
110
111 # the replace method can be inherited from FS::Record
112
113 =item check
114
115 Checks all fields to make sure this is a valid payment gateway.  If there is
116 an error, returns the error, otherwise returns false.  Called by the insert
117 and replace methods.
118
119 =cut
120
121 # the check method should currently be supplied - FS::Record contains some
122 # data checking routines
123
124 sub check {
125   my $self = shift;
126
127   my $error = 
128     $self->ut_numbern('gatewaynum')
129     || $self->ut_alpha('gateway_module')
130     || $self->ut_enum('gateway_namespace', ['Business::OnlinePayment',
131                                             'Business::OnlineThirdPartyPayment',
132                                             'Business::BatchPayment',
133                                            ] )
134     || $self->ut_textn('gateway_username')
135     || $self->ut_anything('gateway_password')
136     || $self->ut_textn('gateway_callback_url')  # a bit too permissive
137     || $self->ut_textn('gateway_cancel_url')
138     || $self->ut_enum('disabled', [ '', 'Y' ] )
139     || $self->ut_enum('auto_resolve_status', [ '', 'approve', 'reject' ])
140     || $self->ut_numbern('auto_resolve_days')
141     #|| $self->ut_textn('gateway_action')
142   ;
143   return $error if $error;
144
145   if ( $self->gateway_namespace eq 'Business::BatchPayment' ) {
146     $self->gateway_action('Payment');
147   } elsif ( $self->gateway_action ) {
148     my @actions = split(/,\s*/, $self->gateway_action);
149     $self->gateway_action(
150       join( ',', map { /^(Normal Authorization|Authorization Only|Credit|Post Authorization)$/
151                          or return "Unknown action $_";
152                        $1
153                      }
154                      @actions
155           )
156    );
157   } else {
158     $self->gateway_action('Normal Authorization');
159   }
160
161   # this little kludge mimics FS::CGI::popurl
162   #$self->gateway_callback_url($self->gateway_callback_url. '/')
163   #  if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ );
164
165   $self->SUPER::check;
166 }
167
168 =item agent_payment_gateway
169
170 Returns any agent overrides for this payment gateway.
171
172 =item disable
173
174 Disables this payment gateway: deletes all associated agent_payment_gateway
175 overrides and sets the I<disabled> field to "B<Y>".
176
177 =cut
178
179 sub disable {
180   my $self = shift;
181
182   local $SIG{HUP} = 'IGNORE';
183   local $SIG{INT} = 'IGNORE';
184   local $SIG{QUIT} = 'IGNORE';
185   local $SIG{TERM} = 'IGNORE';
186   local $SIG{TSTP} = 'IGNORE';
187   local $SIG{PIPE} = 'IGNORE';
188
189   my $oldAutoCommit = $FS::UID::AutoCommit;
190   local $FS::UID::AutoCommit = 0;
191   my $dbh = dbh;
192
193   foreach my $agent_payment_gateway ( $self->agent_payment_gateway ) {
194     my $error = $agent_payment_gateway->delete;
195     if ( $error ) {
196       $dbh->rollback if $oldAutoCommit;
197       return "error deleting agent_payment_gateway override: $error";
198     }
199   }
200
201   $self->disabled('Y');
202   my $error = $self->replace();
203   if ( $error ) {
204     $dbh->rollback if $oldAutoCommit;
205     return "error disabling payment_gateway: $error";
206   }
207
208   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
209   '';
210
211 }
212
213 =item label
214
215 Returns a semi-friendly label for the gateway.
216
217 =cut
218
219 sub label {
220   my $self = shift;
221   $self->gatewaynum . ': ' . 
222   ($self->gateway_username ? $self->gateway_username . '@' : '') . 
223   $self->gateway_module
224 }
225
226 =item namespace_description
227
228 returns a friendly name for the namespace
229
230 =cut
231
232 my %namespace2description = (
233   '' => 'Direct',
234   'Business::OnlinePayment' => 'Direct',
235   'Business::OnlineThirdPartyPayment' => 'Hosted',
236   'Business::BatchPayment' => 'Batch',
237 );
238
239 sub namespace_description {
240   $namespace2description{shift->gateway_namespace} || 'Unknown';
241 }
242
243 =item batch_processor OPTIONS
244
245 For BatchPayment gateways only.  Returns a 
246 L<Business::BatchPayment::Processor> object to communicate with the 
247 gateway.
248
249 OPTIONS will be passed to the constructor, along with any gateway 
250 options in the database for this L<FS::payment_gateway>.  Useful things
251 to include there may include 'input' and 'output' (to direct transport
252 to files), 'debug', and 'test_mode'.
253
254 If the global 'business-batchpayment-test_transaction' flag is set, 
255 'test_mode' will be forced on, and gateways that don't support test
256 mode will be disabled.
257
258 =cut
259
260 sub batch_processor {
261   local $@;
262   my $self = shift;
263   my %opt = @_;
264   my $batch = $opt{batch};
265   my $output = $opt{output};
266   die 'gateway '.$self->gatewaynum.' is not a Business::BatchPayment gateway'
267     unless $self->gateway_namespace eq 'Business::BatchPayment';
268   eval "use Business::BatchPayment;";
269   die "couldn't load Business::BatchPayment: $@" if $@;
270
271   #false laziness with processor
272   foreach (qw(username password)) {
273     if (length($self->get("gateway_$_"))) {
274       $opt{$_} = $self->get("gateway_$_");
275     }
276   }
277
278   my $module = $self->gateway_module;
279   my $processor = eval { 
280     Business::BatchPayment->create($module, $self->options, %opt)
281   };
282   die "failed to create Business::BatchPayment::$module object: $@"
283     if $@;
284
285   die "$module does not support test mode"
286     if $opt{'test_mode'}
287       and not $processor->does('Business::BatchPayment::TestMode');
288
289   return $processor;
290 }
291
292 =item processor OPTIONS
293
294 Loads the module for the processor and returns an instance of it.
295
296 =cut
297
298 sub processor {
299   local $@;
300   my $self = shift;
301   my %opt = @_;
302   foreach (qw(action username password)) {
303     if (length($self->get("gateway_$_"))) {
304       $opt{$_} = $self->get("gateway_$_");
305     }
306   }
307   $opt{'return_url'} = $self->gateway_callback_url;
308   $opt{'cancel_url'} = $self->gateway_cancel_url;
309
310   my $conf = new FS::Conf;
311   my $test_mode = $conf->exists('business-batchpayment-test_transaction');
312   $opt{'test_mode'} = 1 if $test_mode;
313
314   my $namespace = $self->gateway_namespace;
315   eval "use $namespace";
316   die "couldn't load $namespace: $@" if $@;
317
318   if ( $namespace eq 'Business::BatchPayment' ) {
319     # at some point we can merge these, but there's enough special behavior...
320     return $self->batch_processor(%opt);
321   } else {
322     return $namespace->new( $self->gateway_module, $self->options, %opt );
323   }
324 }
325
326 =item default_gateway OPTIONS
327
328 Class method.
329
330 Returns default gateway (from business-onlinepayment conf) as a payment_gateway object.
331
332 Accepts options
333
334 conf - existing conf object
335
336 nofatal - return blank instead of dying if no default gateway is configured
337
338 method - if set to CHEK or ECHECK, returns object for business-onlinepayment-ach if available
339
340 Before using this, be sure you wouldn't rather be using L</by_key_or_default> or,
341 more likely, L<FS::agent/payment_gateway>.
342
343 =cut
344
345 # the standard settings from the config could be moved to a null agent
346 # agent_payment_gateway referenced payment_gateway
347
348 sub default_gateway {
349   my ($self,%options) = @_;
350
351   $options{'conf'} ||= new FS::Conf;
352   my $conf = $options{'conf'};
353
354   unless ( $conf->exists('business-onlinepayment') ) {
355     if ( $options{'nofatal'} ) {
356       return '';
357     } else {
358       die "Real-time processing not enabled\n";
359     }
360   }
361
362   #load up config
363   my $bop_config = 'business-onlinepayment';
364   $bop_config .= '-ach'
365     if ( $options{method}
366          && $options{method} =~ /^(ECHECK|CHEK)$/
367          && $conf->exists($bop_config. '-ach')
368        );
369   my ( $processor, $login, $password, $action, @bop_options ) =
370     $conf->config($bop_config);
371   $action ||= 'normal authorization';
372   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
373   die "No real-time processor is enabled - ".
374       "did you set the business-onlinepayment configuration value?\n"
375     unless $processor;
376
377   my $payment_gateway = new FS::payment_gateway;
378   $payment_gateway->gateway_namespace( $conf->config('business-onlinepayment-namespace') ||
379                                        'Business::OnlinePayment');
380   $payment_gateway->gateway_module($processor);
381   $payment_gateway->gateway_username($login);
382   $payment_gateway->gateway_password($password);
383   $payment_gateway->gateway_action($action);
384   $payment_gateway->set('options', [ @bop_options ]);
385   return $payment_gateway;
386 }
387
388 =item by_key_with_namespace GATEWAYNUM
389
390 Like usual by_key, but makes sure namespace is set,
391 and dies if not found.
392
393 =cut
394
395 sub by_key_with_namespace {
396   my $self = shift;
397   my $payment_gateway = $self->by_key(@_);
398   die "payment_gateway not found"
399     unless $payment_gateway;
400   $payment_gateway->gateway_namespace('Business::OnlinePayment')
401     unless $payment_gateway->gateway_namespace;
402   return $payment_gateway;
403 }
404
405 =item by_key_or_default OPTIONS
406
407 Either returns the gateway specified by option gatewaynum, or the default gateway.
408
409 Accepts the same options as L</default_gateway>.
410
411 Also ensures that the gateway_namespace has been set.
412
413 =cut
414
415 sub by_key_or_default {
416   my ($self,%options) = @_;
417
418   if ($options{'gatewaynum'}) {
419     return $self->by_key_with_namespace($options{'gatewaynum'});
420   } else {
421     return $self->default_gateway(%options);
422   }
423 }
424
425 # if it weren't for the way gateway_namespace default is set, this method would not be necessary
426 # that should really go in check() with an accompanying upgrade, so we could just use qsearch safely,
427 # but currently short on time to test deeper changes...
428 #
429 # if no default gateway is set and nofatal is passed, first value returned is blank string
430 sub all_gateways {
431   my ($self,%options) = @_;
432   my @out;
433   foreach my $gatewaynum ('',( map {$_->gatewaynum} qsearch('payment_gateway') )) {
434     push @out, $self->by_key_or_default( %options, gatewaynum => $gatewaynum );
435   }
436   return @out;
437 }
438
439 # _upgrade_data
440 #
441 # Used by FS::Upgrade to migrate to a new database.
442 #
443 #
444
445 sub _upgrade_data {
446   my ($class, %opts) = @_;
447   my $dbh = dbh;
448
449   warn "$me upgrading $class\n" if $DEBUG;
450
451   foreach ( qsearch( 'payment_gateway', { 'gateway_namespace' => '' } ) ) {
452     $_->gateway_namespace('Business::OnlinePayment');  #defaulting
453     my $error = $_->replace;
454     die "$class had error during upgrade replacement: $error" if $error;
455   }
456 }
457
458 =back
459
460 =head1 BUGS
461
462 =head1 SEE ALSO
463
464 L<FS::Record>, schema.html from the base documentation.
465
466 =cut
467
468 1;
469