* Add enclair_db option to UserDB.pm. Allows logging of enclair password
[interchange.git] / lib / Vend / Payment / PRI.pm
1 # Vend::Payment::PRI - Interchange PRI support
2 #
3 # $Id: PRI.pm,v 1.7 2009-03-16 19:34:01 jon Exp $
4 #
5 # Copyright (C) 2002-2007 Interchange Development Group
6 # Copyright (C) 1999-2002 Red Hat, Inc.
7 #
8 # Written by Marty Tennison, Based on code by Cameron Prince and Mark Johnson,
9 # which in turn was based on code by Mike Heins.
10
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public
22 # License along with this program; if not, write to the Free
23 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
24 # MA  02110-1301  USA.
25
26 package Vend::Payment::PRI;
27
28 =head1 NAME
29
30 Vend::Payment::PRI - Interchange PRI Support
31
32 =head1 SYNOPSIS
33
34     &charge=PRI
35
36         or
37
38     [charge mode=PRI param1=value1 param2=value2]
39
40 =head1 PREREQUISITES
41
42   Net::SSLeay
43  
44     or
45   
46   LWP::UserAgent and Crypt::SSLeay
47
48 Only one of these need be present and working.
49
50 =head1 DESCRIPTION
51
52 The Vend::Payment::PRI module implements the PRI() routine for using
53 Payment Resources International payment services with Interchange. It is
54 compatible on a call level with the other Interchange payment modules.
55
56 To enable this module, place this directive in C<interchange.cfg>:
57
58     Require module Vend::Payment::PRI
59
60 This I<must> be in interchange.cfg or a file included from it.
61
62 NOTE: Make sure CreditCardAuto is off (default in Interchange demos).
63
64 The mode can be named anything, but the C<gateway> parameter must be set
65 to C<PRI>. To make it the default payment gateway for all credit
66 card transactions in a specific catalog, you can set in C<catalog.cfg>:
67
68     Variable   MV_PAYMENT_MODE  PRI
69
70 It uses several of the standard settings from Interchange payment. Any time
71 we speak of a setting, it is obtained either first from the tag/call options,
72 then from an Interchange order Route named for the mode, then finally a
73 default global payment variable, For example, the C<id> parameter would
74 be specified by:
75
76     [charge mode=PRI id=YourPRIID]
77
78 or
79
80     Route PRI id YourPRIID
81
82 or with only PRI as a payment provider
83
84     Variable MV_PAYMENT_ID      YourPRIID
85
86 A fully valid catalog.cfg entry to work with the standard demo would be:
87
88     Variable MV_PAYMENT_MODE    "__MV_PAYMENT_MODE__"
89                 Route  PRI      id          "__PRI_ID__"
90                 Route  PRI      regkey      "__PRI_REGKEY__"
91                 Route  PRI      test_id     "__PRI_TEST_ID__"
92                 Route  PRI      test_regkey "__PRI_TEST_REGKEY__"
93                 Route  PRI      test_mode   "__PRI_TEST_MODE__"
94                 Route  PRI      refid_mode  "__PRI_REFID_MODE__"
95                 
96 A fully valid variable.txt entry to work with the PRI module would be:
97
98                 MV_PAYMENT_MODE PRI     Payment
99                 PRI_ID  your_pri_id     Payment
100                 PRI_REGKEY      your_pri_regkey Payment
101                 PRI_TEST_ID     your_pri_test_id        Payment
102                 PRI_TEST_REGKEY your_pri_test_regkey    Payment
103                 PRI_TEST_MODE   1       Payment
104                 PRI_REFID_MODE  1       Payment
105
106
107 The active settings are:
108
109 =over 4
110
111 =item id
112
113 PRI will supply you with both a test id and production id.  Enter both of these numbers into the the variables above.  You do not need your production id to test. 
114
115 =item regkey
116
117 PRI will supply you with both a test regkey and production regkey.  Enter both of these numbers into the the variables above.  You do not need your production regkey to test. 
118
119 =item refid
120
121 The PRI interface allows (requires) a field called REFID.  This field is stored along with the transaction on the PRI server and allows your to do quick searches for transactions if this number has meaning.  There are three possible values for the PRI_REFID_MODE variable.  1,2 or any other character or null.  
122
123         1.  A "1" in the pri_refid_mode instructs interchange to read the current
124         order number in $Variable->{MV_ORDER_COUNTER_FILE} or "etc/order.number",
125         increment it by one and use that. Do not use this mode if you have a busy catalog.  PRI might reject orders as duplicates if two people try to checkout at the same time.
126         
127         2. A "2" in the pri_refid_mode instructs interchange to use the users
128         session_id as the value.  This is the recommended mode.
129         
130         3. Anything other than a 1 or 2 instructs interchange to generate a unique
131         number from the unix date command and use that.  The number format is Day of
132         year, Hours, Minutes, Seconds.  Example for Jan 1, at 1:00:30 is 001130030.
133
134 =item transaction
135
136 At this time the PRI payment module only processes transactions of type SALE.
137
138 =item test
139
140 Testing with PRI is straight forward.  At this time (2004-05-15), PRI uses the same server for both development and production.  The only difference is the account used.  Some accounts are flagged as TEST accounts and others are live.  When you first sign up with PRI they will supply you a test account and test Registration Key to use.  Enter those numbers in the PRI_ID, PRI_REGKEY (production) and PRI_TEST_ID, PRI_TEST_REGKEY (test) variables.  Set the PRI_TEST_MODE to a value of 1,2 or 3 then do your testing.  Once everything is working correctly, simply set PRI_TEST_MODE to 0 and restart interchange.  Your now live.
141
142 Testing has 3 modes. (1,2,3) (live mode is 0) You set the mode with the PRI_TEST_MODE variable in variable.txt or directly in your catalog.cfg file.  The modes are as follows.
143
144 1) Use PRI_TEST_ID and PRI_TEST_REGKEY values.  Send information to PRI and receive result from PRI.  To generate errors in this mode, simply enter invalid data and PRI should reject it with an error.  
145
146 2) Generate a declined order internally.  Does not send data to PRI.  This mode is convenient if you want to do some testing and do not want to send any data to PRI.  It's also a good way to track down errors.
147
148 3) Generate a successful sale internally.  Does not send data to PRI. This mode is convenient if you want to see if everything works before sending test data to PRI.
149
150 A good way to test this module is to set PRI_TEST_MODE to 3, then 2, then 1, then 0 and make sure your catalog handles all situations correctly.
151
152
153 =item generate_error
154
155 To generate errors in test mode (while using your test ID and regkey) simply enter transactions with bad data and see what happens.  PRI will supply you with a list of test credit card numbers and amounts that they are good for. 
156
157 =item submit_url
158
159 PRI uses different URLs depending on what type of transaction you are requesting, Sale, Recurring, Void etc..  The default URL for single sale transactions is
160
161          https://webservices.primerchants.com/billing/TransactionCentral/processCC.asp?
162
163 At this time, this is the only URL supported by this PRI module
164
165 =back
166
167 =head2 Troubleshooting
168
169 If nothing works:
170
171 =over 4
172
173 =item *
174
175 Make sure you "Require"d the module in interchange.cfg:
176
177     Require module Vend::Payment::PRI
178
179 =item *
180
181 Make sure either Net::SSLeay or Crypt::SSLeay and LWP::UserAgent are installed
182 and working. You can test to see whether your Perl thinks they are:
183
184     perl -MNet::SSLeay -e 'print "It works\n"'
185
186 or
187
188     perl -MLWP::UserAgent -MCrypt::SSLeay -e 'print "It works\n"'
189
190 If either one prints "It works." and returns to the prompt you should be OK
191 (presuming they are in working order otherwise).
192
193 =item *
194
195 Check the error logs, both catalog and global.
196
197 =item *
198
199 Make sure you set your account ID properly.  
200
201 =item *
202
203 Try an order, then put this code in a page:
204
205     <XMP>
206     [calc]
207         my $string = $Tag->uneval( { ref => $Session->{payment_result} });
208         $string =~ s/{/{\n/;
209         $string =~ s/,/,\n/g;
210         return $string;
211     [/calc]
212     </XMP>
213
214 That should show what happened.
215
216 =item *
217
218 If all else fails, consultants are available to help with integration for a fee.
219 See http://www.icdevgroup.org/
220
221 =back
222
223 =head1 BUGS
224
225 There is actually nothing *in* Vend::Payment::PRI. It changes packages
226 to Vend::Payment and places things there.
227
228 =head1 AUTHORS
229
230 Originally developed by New York Connect Net (http://nyct.net)
231 Michael Bacarella <mbac@nyct.net>
232
233 Modified for GetCareer.com by Slipstream.com by Troy Davis <troy@slipstream.com>
234
235 LWP/Crypt::SSLeay interface code by Matthew Schick,
236 <mschick@brightredproductions.com>.
237
238 Interchange implementation by Mike Heins.
239
240 PRI modification by Marty Tennison
241
242 =head1 VERSION HISTORY
243
244 05-24-2004 - Version 1.0
245
246 09-06-2004 -.Version 1.1
247   Added testing mode support.
248         Changed default refid to mode 2.
249         Fixed bug where PRI.pm would not recognize a successful transaction with a mix of digits and letters.  Now checks for "Declined", <space> or <null> to determine declined transaction, all others succeed.
250         Cleaned up some code.
251
252 =cut
253
254 BEGIN {
255
256         my $selected;
257         eval {
258                 package Vend::Payment;
259                 require Net::SSLeay;
260                 import Net::SSLeay qw(post_https make_form make_headers);
261                 $selected = "Net::SSLeay";
262         };
263
264         $Vend::Payment::Have_Net_SSLeay = 1 unless $@;
265
266         unless ($Vend::Payment::Have_Net_SSLeay) {
267
268                 eval {
269                         package Vend::Payment;
270                         require LWP::UserAgent;
271                         require HTTP::Request::Common;
272                         require Crypt::SSLeay;
273                         import HTTP::Request::Common qw(POST);
274                         $selected = "LWP and Crypt::SSLeay";
275                 };
276
277                 $Vend::Payment::Have_LWP = 1 unless $@;
278
279         }
280
281         unless ($Vend::Payment::Have_Net_SSLeay or $Vend::Payment::Have_LWP) {
282                 die __PACKAGE__ . " requires Net::SSLeay or Crypt::SSLeay";
283         }
284
285         ::logGlobal("%s payment module initialized, using %s", __PACKAGE__, $selected)
286                 unless $Vend::Quiet;
287
288 }
289
290 package Vend::Payment;
291
292 use vars qw/%PRI_AVS_CODES/;
293 use vars qw/$Have_LWP $Have_Net_SSLeay/;
294
295 %PRI_AVS_CODES = (
296         'X' => 'Exact match, 9 digit zip',
297         'Y' => 'Exact match, 5 digit zip',
298         'A' => 'Address match only',
299         'W' => '9 digit match only',
300         'Z' => '5 digit match only',
301         'N' => 'No address or zip match',
302         'U' => 'Address unavailable',
303         'R' => 'Retry - Issuer system unavailable',
304         'E' => 'ERROR INELIGIBLE - Not a mail/phone number',
305         'S' => 'SERVICE UNAVAILABLE - Service not supported',
306         'G' => 'AVS NOT AVAILABLE - Non U.S. Issuer',
307         'B' => 'ADDRESS MATCH - Street address match',
308         'C' => 'Street and post not verified for international',
309         'D' => 'Good street and post net match',
310         'F' => 'Good street and post net match - UK',
311         'I' => 'Address not verified',
312         'M' => 'Street address and post net match',
313         'P' => 'Postal net matched, Street address not verified'
314 );
315
316 sub PRI {
317         my ($opt, $amount) = @_;
318
319         my $user = $opt->{id} || charge_param('id');
320
321         my %actual;
322         if($opt->{actual}) {
323                 %actual = %{$opt->{actual}};
324         }
325         else {
326                 %actual = map_actual();
327         }
328
329 #::logDebug("Mapping: " . ::uneval(%actual));
330
331         my $ccem = $actual{mv_credit_card_exp_month};
332         $ccem =~ s/\D//g;
333         $ccem =~ s/^0+//;
334         $ccem = sprintf('%02d', $ccem);
335
336         my $ccey = $actual{mv_credit_card_exp_year};
337         $ccey =~ s/[\t\r\n]//g;
338         $ccey =~ s/^\s*//g;
339         $ccey =~ s/\s*$//g;
340         chomp $ccey;
341
342         $actual{mv_credit_card_number} =~ s/\D//g;
343
344         my $precision = $opt->{precision} || charge_param('precision') || 2;
345
346         $amount = $opt->{total_cost} || undef;
347
348         $opt->{transaction} ||= 'sale';
349         my $transtype = $opt->{transaction};
350
351         my %type_map = (
352         );
353         
354         if ( defined $type_map{$transtype} ) {
355                 $transtype = $type_map{$transtype};
356         }
357
358         if ( ! $amount ) {
359                 $amount = Vend::Interpolate::total_cost();
360                 $amount = Vend::Util::round_to_frac_digits($amount,$precision);
361         }
362
363         # figure out what refid should be
364         if ( $opt->{refid_mode} == 1 ) {
365                 my $cfn = $Variable->{MV_ORDER_COUNTER_FILE} || 'etc/order.number';
366                 $new_order_number = $Tag->file($cfn);
367                 $new_order_number =~ s/.*\n([A-Za-z0-9]+).*$/$1/s;
368                 ++$new_order_number;
369                 $refid = $new_order_number;
370         }
371         elsif ( $opt->{refid_mode} == 2 ) {
372                 $refid = $Vend::SessionID;
373         }
374         else {
375                 $refid = `date +%j%k%M%S`;
376                 chomp $refid;
377         }
378
379         my %result;
380         my $result_page;
381
382         # See if we are in test mode, and if so, what mode
383         if ( $opt->{test_mode} >= 1 ) {
384                 $merchantid = $opt->{test_id};
385                 $regkey = $opt->{test_regkey};
386                 if ( $opt->{test_mode} == 2 ) {
387                         $result_page = "TransID=&REFNO=12345&Auth=Declined&AVSCode=&CVV2ResponseMsg=&Notes=M4 / Please Try Again (Default message)&User1=&User2=&User3=&User4=";
388                 }
389                 elsif ( $opt->{test_mode} == 3 ) {
390                         $result_page = " TransID=12T4567&REFNO=12T45&Auth=12T45&AVSCode=Y&CVV2ResponseMsg=M&Notes=Notes here&User1=&User2=&User3=&User4=";
391                 }
392         } 
393         else {
394                 $merchantid = $opt->{id};
395                 $regkey = $opt->{regkey};
396         }
397         
398         my %values;
399         if ( $transtype eq 'sale' ) {
400                 %values = (
401                         MerchantID => $merchantid,
402                         RegKey => $regkey,
403                         Amount => $amount,
404                         REFID => $refid,
405                         AccountNo => $actual{mv_credit_card_number},
406                         CCMonth => $ccem,
407                         CCYear => $ccey,
408                         NameonAccount => $actual{b_name},
409                         AVSADDR => $actual{b_address},
410                         AVSZIP => $actual{b_zip},
411                         CCRURL => "",
412                         CVV2 => $actual{cvv2},
413                         );
414         }
415
416 #::logDebug("Values to be sent: " . ::uneval(%values));
417
418         $opt->{submit_url} ||=   'https://webservices.primerchants.com/billing/TransactionCentral/processCC.asp?';
419
420 #::logDebug("sending query: " . ::uneval(\%values));
421
422         # Interchange names are on the  left, PRI on the right
423         my %result_map = ( qw/
424                 pop.ref-code          TransID
425                 pop.auth-code         Auth
426                 pop.avs_code          AVSCode
427                 pop.txn-id            RefNo
428                 pop.error-message     Notes
429                 pop.cvv2_code         CVV2ResponseMessage
430         /
431         );
432
433         if ( $opt->{test_mode} <= 1 ) {
434                 my $thing      = post_data($opt, \%values);
435                 $result_page   = $thing->{result_page};
436         }
437
438         ## check for errors
439         my $error;
440
441 #::logDebug("restul_page before cleanup: $result_page");
442
443         # strip html from result_page and clean it up
444         $result_page =~ s/\<.*?\>//g;
445         $result_page =~ s/[\t\r\n]//g;
446         $result_page =~ s/^\s*//g;
447         $result_page =~ s/\s*$//g;
448         
449 #::logDebug("restul_page after cleanup: $result_page");
450
451         %$result = split /[&=]/, $result_page;
452         
453         # if the Auth code contains Declined, or if it is
454         # null or a space, we failed.
455         if ( ! $result->{Auth} || $result->{Auth} =~ /Declined/ || $result->{Auth} eq " "  ) {
456 #::logDebug("Transaction declined: $result->{Auth}: $result->{Notes}");
457                 $result->{MStatus} = 'failed';
458                 if ( $result->{Notes} ) {
459                         $result->{MErrMsg} = "$result->{Auth} $result->{Notes}";
460                 }
461                 else {
462                         $result->{MErrMsg} = "Unknown error";
463                 }
464         }
465         else {
466 #::logDebug("Transaction approved: $result->{Auth}: $result->{Notes}");
467                 $result->{MStatus} = $result->{'pop.status'} = 'success';
468                 $result{MStatus} = $result->{'pop.status'} = 'success';
469                 $result->{'pop.order-id'} = $result->{TransID};
470                 $result->{'order-id'} = $result->{TransID};
471                 $::Values->{avs} = $result->{AVSCode};
472                 $::Values->{cvv2} = $result->{CVV2ResponseMsg};
473                 $::Values->{auth} = $result->{Auth};
474                 $result->{AUTHCODE} = $result->{Auth};
475                 $result->{ICSTATUS} = 'success';
476         }
477
478     for (keys %result_map) {
479         $result->{$_} = $result->{$result_map{$_}}
480             if defined $result->{$result_map{$_}};
481     }
482                 
483 #::logDebug(qq{PRI query result: } . ::uneval($result));
484
485         return %$result;
486
487 }
488
489 package Vend::Payment::PRI;
490
491 1;