forked from activemerchant/active_merchant
/
linkpoint.rb
449 lines (408 loc) · 17.4 KB
/
linkpoint.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Initialization Options
# :login Your store number
# :pem The text of your linkpoint PEM file. Note
# this is not the path to file, but its
# contents. If you are only using one PEM
# file on your site you can declare it
# globally and then you won't need to
# include this option
#
#
# A valid store number is required. Unfortunately, with LinkPoint
# YOU CAN'T JUST USE ANY OLD STORE NUMBER. Also, you can't just
# generate your own PEM file. You'll need to use a special PEM file
# provided by LinkPoint.
#
# Go to http://www.linkpoint.com/support/sup_teststore.asp to set up
# a test account and obtain your PEM file.
#
# Declaring PEM file Globally
# ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
#
#
# Valid Order Options
# :result =>
# LIVE Production mode
# GOOD Approved response in test mode
# DECLINE Declined response in test mode
# DUPLICATE Duplicate response in test mode
#
# :ponumber Order number
#
# :transactionorigin => Source of the transaction
# ECI Email or Internet
# MAIL Mail order
# MOTO Mail order/Telephone
# TELEPHONE Telephone
# RETAIL Face-to-face
#
# :ordertype =>
# SALE Real live sale
# PREAUTH Authorize only
# POSTAUTH Forced Ticket or Ticket Only transaction
# VOID
# CREDIT
# CALCSHIPPING For shipping charges calculations
# CALCTAX For sales tax calculations
#
# Recurring Options
# :action =>
# SUBMIT
# MODIFY
# CANCEL
#
# :installments Identifies how many recurring payments to charge the customer
# :startdate Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
# :periodicity =>
# MONTHLY
# BIMONTHLY
# WEEKLY
# BIWEEKLY
# YEARLY
# DAILY
# :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant.
# :comments Uh... comments
#
#
# For reference:
#
# https://www.linkpointcentral.com/lpc/docs/Help/APIHelp/lpintguide.htm
#
# Entities = {
# :payment => [:subtotal, :tax, :vattax, :shipping, :chargetotal],
# :billing => [:name, :address1, :address2, :city, :state, :zip, :country, :email, :phone, :fax, :addrnum],
# :shipping => [:name, :address1, :address2, :city, :state, :zip, :country, :weight, :items, :carrier, :total],
# :creditcard => [:cardnumber, :cardexpmonth, :cardexpyear, :cvmvalue, :track],
# :telecheck => [:routing, :account, :checknumber, :bankname, :bankstate, :dl, :dlstate, :void, :accounttype, :ssn],
# :transactiondetails => [:transactionorigin, :oid, :ponumber, :taxexempt, :terminaltype, :ip, :reference_number, :recurring, :tdate],
# :periodic => [:action, :installments, :threshold, :startdate, :periodicity, :comments],
# :notes => [:comments, :referred]
# :items => [:item => [:price, :quantity, :description, :id, :options => [:option => [:name, :value]]]]
# }
#
#
# LinkPoint's Items entity is an optional entity that can be attached to orders.
# It is entered as :line_items to be consistent with the CyberSource implementation
#
# The line_item hash goes in the options hash and should look like
#
# :line_items => [
# {
# :id => '123456',
# :description => 'Logo T-Shirt',
# :price => '12.00',
# :quantity => '1',
# :options => [
# {
# :name => 'Color',
# :value => 'Red'
# },
# {
# :name => 'Size',
# :value => 'XL'
# }
# ]
# },
# {
# :id => '111',
# :description => 'keychain',
# :price => '3.00',
# :quantity => '1'
# }
# ]
# This functionality is only supported by this particular gateway may
# be changed at any time
#
class LinkpointGateway < Gateway
# Your global PEM file. This will be assigned to you by linkpoint
#
# Example:
#
# ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
#
cattr_accessor :pem_file
TEST_URL = 'https://staging.linkpt.net:1129/'
LIVE_URL = 'https://secure.linkpt.net:1129/'
# We don't have the certificate to verify LinkPoint
self.ssl_strict = false
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'http://www.linkpoint.com/'
self.display_name = 'LinkPoint'
def initialize(options = {})
requires!(options, :login)
@options = {
:result => 'LIVE',
:pem => LinkpointGateway.pem_file
}.update(options)
raise ArgumentError, "You need to pass in your pem file using the :pem parameter or set it globally using ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' ) or similar" if @options[:pem].blank?
end
# Send a purchase request with periodic options
# Recurring Options
# :action =>
# SUBMIT
# MODIFY
# CANCEL
#
# :installments Identifies how many recurring payments to charge the customer
# :startdate Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
# :periodicity =>
# :monthly
# :bimonthly
# :weekly
# :biweekly
# :yearly
# :daily
# :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant.
# :comments Uh... comments
#
def recurring(money, creditcard, options={})
requires!(options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily], :installments, :order_id )
options.update(
:ordertype => "SALE",
:action => options[:action] || "SUBMIT",
:installments => options[:installments] || 12,
:startdate => options[:startdate] || "immediate",
:periodicity => options[:periodicity].to_s || "monthly",
:comments => options[:comments] || nil,
:threshold => options[:threshold] || 3
)
commit(money, creditcard, options)
end
# Buy the thing
def purchase(money, creditcard, options={})
requires!(options, :order_id)
options.update(
:ordertype => "SALE"
)
commit(money, creditcard, options)
end
#
# Authorize the transaction
#
# Reserves the funds on the customer's credit card, but does not charge the card.
#
def authorize(money, creditcard, options = {})
requires!(options, :order_id)
options.update(
:ordertype => "PREAUTH"
)
commit(money, creditcard, options)
end
#
# Post an authorization.
#
# Captures the funds from an authorized transaction.
# Order_id must be a valid order id from a prior authorized transaction.
#
def capture(money, authorization, options = {})
options.update(
:order_id => authorization,
:ordertype => "POSTAUTH"
)
commit(money, nil, options)
end
# Void a previous transaction
def void(identification, options = {})
options.update(
:order_id => identification,
:ordertype => "VOID"
)
commit(nil, nil, options)
end
#
# Refund an order
#
# identification must be a valid order id previously submitted by SALE
#
def credit(money, identification, options = {})
options.update(
:ordertype => "CREDIT",
:order_id => identification
)
commit(money, nil, options)
end
def test?
@options[:test] || super
end
private
# Commit the transaction by posting the XML file to the LinkPoint server
def commit(money, creditcard, options = {})
response = parse(ssl_post(test? ? TEST_URL : LIVE_URL, post_data(money, creditcard, options)))
Response.new(successful?(response), response[:message], response,
:test => test?,
:authorization => response[:ordernum],
:avs_result => { :code => response[:avs].to_s[2,1] },
:cvv_result => response[:avs].to_s[3,1]
)
end
def successful?(response)
response[:approved] == "APPROVED"
end
# Build the XML file
def post_data(money, creditcard, options)
params = parameters(money, creditcard, options)
xml = REXML::Document.new
order = xml.add_element("order")
# Merchant Info
merchantinfo = order.add_element("merchantinfo")
merchantinfo.add_element("configfile").text = @options[:login]
# Loop over the params hash to construct the XML string
for key, value in params
elem = order.add_element(key.to_s)
if key == :items
build_items(elem, value)
else
for k, v in params[key]
elem.add_element(k.to_s).text = params[key][k].to_s if params[key][k]
end
end
# Linkpoint doesn't understand empty elements:
order.delete(elem) if elem.size == 0
end
return xml.to_s
end
# adds LinkPoint's Items entity to the XML. Called from post_data
def build_items(element, items)
for item in items
item_element = element.add_element("item")
for key, value in item
if key == :options
options_element = item_element.add_element("options")
for option in value
opt_element = options_element.add_element("option")
opt_element.add_element("name").text = option[:name] unless option[:name].blank?
opt_element.add_element("value").text = option[:value] unless option[:value].blank?
end
else
item_element.add_element(key.to_s).text = item[key].to_s unless item[key].blank?
end
end
end
end
# Set up the parameters hash just once so we don't have to do it
# for every action.
def parameters(money, creditcard, options = {})
params = {
:payment => {
:subtotal => amount(options[:subtotal]),
:tax => amount(options[:tax]),
:vattax => amount(options[:vattax]),
:shipping => amount(options[:shipping]),
:chargetotal => amount(money)
},
:transactiondetails => {
:transactionorigin => options[:transactionorigin] || "ECI",
:oid => options[:order_id],
:ponumber => options[:ponumber],
:taxexempt => options[:taxexempt],
:terminaltype => options[:terminaltype],
:ip => options[:ip],
:reference_number => options[:reference_number],
:recurring => options[:recurring] || "NO", #DO NOT USE if you are using the periodic billing option.
:tdate => options[:tdate]
},
:orderoptions => {
:ordertype => options[:ordertype],
:result => @options[:result]
},
:periodic => {
:action => options[:action],
:installments => options[:installments],
:threshold => options[:threshold],
:startdate => options[:startdate],
:periodicity => options[:periodicity],
:comments => options[:comments]
},
:telecheck => {
:routing => options[:telecheck_routing],
:account => options[:telecheck_account],
:checknumber => options[:telecheck_checknumber],
:bankname => options[:telecheck_bankname],
:dl => options[:telecheck_dl],
:dlstate => options[:telecheck_dlstate],
:void => options[:telecheck_void],
:accounttype => options[:telecheck_accounttype],
:ssn => options[:telecheck_ssn],
}
}
if creditcard
params[:creditcard] = {
:cardnumber => creditcard.number,
:cardexpmonth => creditcard.month,
:cardexpyear => format_creditcard_expiry_year(creditcard.year),
:track => nil
}
if creditcard.verification_value?
params[:creditcard][:cvmvalue] = creditcard.verification_value
params[:creditcard][:cvmindicator] = 'provided'
else
params[:creditcard][:cvmindicator] = 'not_provided'
end
end
if billing_address = options[:billing_address] || options[:address]
params[:billing] = {}
params[:billing][:name] = billing_address[:name] || creditcard ? creditcard.name : nil
params[:billing][:address1] = billing_address[:address1] unless billing_address[:address1].blank?
params[:billing][:address2] = billing_address[:address2] unless billing_address[:address2].blank?
params[:billing][:city] = billing_address[:city] unless billing_address[:city].blank?
params[:billing][:state] = billing_address[:state] unless billing_address[:state].blank?
params[:billing][:zip] = billing_address[:zip] unless billing_address[:zip].blank?
params[:billing][:country] = billing_address[:country] unless billing_address[:country].blank?
params[:billing][:company] = billing_address[:company] unless billing_address[:company].blank?
params[:billing][:phone] = billing_address[:phone] unless billing_address[:phone].blank?
params[:billing][:email] = options[:email] unless options[:email].blank?
end
if shipping_address = options[:shipping_address]
params[:shipping] = {}
params[:shipping][:name] = shipping_address[:name] || creditcard ? creditcard.name : nil
params[:shipping][:address1] = shipping_address[:address1] unless shipping_address[:address1].blank?
params[:shipping][:address2] = shipping_address[:address2] unless shipping_address[:address2].blank?
params[:shipping][:city] = shipping_address[:city] unless shipping_address[:city].blank?
params[:shipping][:state] = shipping_address[:state] unless shipping_address[:state].blank?
params[:shipping][:zip] = shipping_address[:zip] unless shipping_address[:zip].blank?
params[:shipping][:country] = shipping_address[:country] unless shipping_address[:country].blank?
end
params[:items] = options[:line_items] if options[:line_items]
return params
end
def parse(xml)
# For reference, a typical response...
# <r_csp></r_csp>
# <r_time></r_time>
# <r_ref></r_ref>
# <r_error></r_error>
# <r_ordernum></r_ordernum>
# <r_message>This is a test transaction and will not show up in the Reports</r_message>
# <r_code></r_code>
# <r_tdate>Thu Feb 2 15:40:21 2006</r_tdate>
# <r_score></r_score>
# <r_authresponse></r_authresponse>
# <r_approved>APPROVED</r_approved>
# <r_avs></r_avs>
response = {:message => "Global Error Receipt", :complete => false}
xml = REXML::Document.new("<response>#{xml}</response>")
xml.root.elements.each do |node|
response[node.name.downcase.sub(/^r_/, '').to_sym] = normalize(node.text)
end unless xml.root.nil?
response
end
# Make a ruby type out of the response string
def normalize(field)
case field
when "true" then true
when "false" then false
when "" then nil
when "null" then nil
else field
end
end
def format_creditcard_expiry_year(year)
sprintf("%.4i", year)[-2..-1]
end
end
end
end