vaulted_billing 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +4 -0
- data/lib/vaulted_billing.rb +30 -0
- data/lib/vaulted_billing/certificate_authorities/entrust.pem +24 -0
- data/lib/vaulted_billing/certificate_authorities/verisign.pem +14 -0
- data/lib/vaulted_billing/core_ext/hash.rb +22 -0
- data/lib/vaulted_billing/credit_card.rb +24 -0
- data/lib/vaulted_billing/customer.rb +12 -0
- data/lib/vaulted_billing/gateway.rb +61 -0
- data/lib/vaulted_billing/gateways.rb +9 -0
- data/lib/vaulted_billing/gateways/authorize_net_cim.rb +217 -0
- data/lib/vaulted_billing/gateways/bogus.rb +61 -0
- data/lib/vaulted_billing/gateways/nmi_customer_vault.rb +171 -0
- data/lib/vaulted_billing/https_interface.rb +80 -0
- data/lib/vaulted_billing/transaction.rb +16 -0
- data/lib/vaulted_billing/version.rb +3 -0
- metadata +177 -0
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
autoload :Version, 'vaulted_billing/version'
|
3
|
+
autoload :Gateway, 'vaulted_billing/gateway'
|
4
|
+
autoload :Gateways, 'vaulted_billing/gateways'
|
5
|
+
autoload :Customer, 'vaulted_billing/customer'
|
6
|
+
autoload :CreditCard, 'vaulted_billing/credit_card'
|
7
|
+
autoload :Transaction, 'vaulted_billing/transaction'
|
8
|
+
autoload :HttpsInterface, 'vaulted_billing/https_interface'
|
9
|
+
|
10
|
+
mattr_accessor :logger
|
11
|
+
|
12
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'vaulted_billing', 'core_ext', '**', '*.rb'))].each do |extension|
|
13
|
+
require extension
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Return the matching gateway for the name provided.
|
18
|
+
#
|
19
|
+
# * <tt>bogus</tt>:: BogusGateway - always successful, does nothing.
|
20
|
+
# * <tt>nmi_customer_vault</tt>:: NMICustomerVaultGateway
|
21
|
+
#
|
22
|
+
def self.gateway(name)
|
23
|
+
Gateways.const_get(name.to_s.camelize)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logger?
|
27
|
+
@@logger.present?
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV
|
3
|
+
BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg
|
4
|
+
cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl
|
5
|
+
ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv
|
6
|
+
cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG
|
7
|
+
A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi
|
8
|
+
eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p
|
9
|
+
dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0
|
10
|
+
aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ
|
11
|
+
aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5
|
12
|
+
gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw
|
13
|
+
ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw
|
14
|
+
CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l
|
15
|
+
dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF
|
16
|
+
bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl
|
17
|
+
cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu
|
18
|
+
dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw
|
19
|
+
NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow
|
20
|
+
HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA
|
21
|
+
BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN
|
22
|
+
Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9
|
23
|
+
n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI=
|
24
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG
|
3
|
+
A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
|
4
|
+
cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
|
5
|
+
MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
|
6
|
+
BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
|
7
|
+
YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
|
8
|
+
ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
|
9
|
+
BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
|
10
|
+
I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
|
11
|
+
CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do
|
12
|
+
lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc
|
13
|
+
AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k
|
14
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
module CoreExt
|
3
|
+
module Hash
|
4
|
+
def to_querystring
|
5
|
+
to_a.reject { |pair| pair.last.nil? }.
|
6
|
+
sort_by { |item| item.first.to_s }.
|
7
|
+
collect { |key, value| "#{key}=#{value}" }.join('&')
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def from_querystring(string)
|
12
|
+
::Hash[string.split(/&/).
|
13
|
+
collect { |i| i.split(/=/) }.
|
14
|
+
collect { |e| e.size == 1 ? (e << '') : e }]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
::Hash.send :include, VaultedBilling::CoreExt::Hash
|
22
|
+
::Hash.extend(VaultedBilling::CoreExt::Hash::ClassMethods)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
class CreditCard
|
3
|
+
attr_accessor :id
|
4
|
+
attr_accessor :currency
|
5
|
+
attr_accessor :card_number
|
6
|
+
attr_accessor :cvv_number
|
7
|
+
attr_accessor :expires_on
|
8
|
+
attr_accessor :first_name
|
9
|
+
attr_accessor :last_name
|
10
|
+
attr_accessor :street_address
|
11
|
+
attr_accessor :locality
|
12
|
+
attr_accessor :region
|
13
|
+
attr_accessor :postal_code
|
14
|
+
attr_accessor :country
|
15
|
+
attr_accessor :phone
|
16
|
+
|
17
|
+
def initialize(attributes = {})
|
18
|
+
attributes = HashWithIndifferentAccess.new(attributes)
|
19
|
+
attributes.each_pair do |key, value|
|
20
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
module Gateway
|
3
|
+
module Response
|
4
|
+
attr_accessor :response_message
|
5
|
+
attr_writer :success
|
6
|
+
def success?; @success; end
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_customer(customer)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_customer(customer)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def remove_customer(customer)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_customer_credit_card(customer, credit_card)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_customer_credit_card(customer, credit_card)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_customer_credit_card(customer, credit_card)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def authorize(customer, credit_card, amount)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
def capture(transaction_id, amount)
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
def refund(transaction_id, amount)
|
42
|
+
raise NotImplementedError
|
43
|
+
end
|
44
|
+
|
45
|
+
def void(transaction_id)
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
|
53
|
+
def respond_with(object, options = {})
|
54
|
+
object.tap do |o|
|
55
|
+
o.extend(VaultedBilling::Gateway::Response)
|
56
|
+
o.success = options.has_key?(:success) ? options[:success] : true
|
57
|
+
yield(o) if block_given?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
module Gateways
|
3
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'gateways', '**', '*.rb'))].each do |file|
|
4
|
+
filename = File.basename(file, '.rb')
|
5
|
+
gateway_class = filename.camelize
|
6
|
+
autoload gateway_class, file
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
module Gateways
|
3
|
+
|
4
|
+
##
|
5
|
+
# An interface to Authorize.net's CIM.
|
6
|
+
#
|
7
|
+
class AuthorizeNetCim
|
8
|
+
include VaultedBilling::Gateway
|
9
|
+
include VaultedBilling::HttpsInterface
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
self.test_uri = 'https://apitest.authorize.net/xml/v1/request.api'
|
13
|
+
self.live_uri = 'https://api.authorize.net/xml/v1/request.api'
|
14
|
+
self.ssl_pem = File.read(File.expand_path(File.join(File.dirname(__FILE__), '..', 'certificate_authorities', 'entrust.pem')))
|
15
|
+
|
16
|
+
options = HashWithIndifferentAccess.new(options)
|
17
|
+
@login = options[:username]
|
18
|
+
@password = options[:password]
|
19
|
+
self.use_test_uri = options[:test]
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_customer(customer)
|
23
|
+
data = build_request('createCustomerProfileRequest') do |xml|
|
24
|
+
xml.tag!('profile') do
|
25
|
+
xml.merchantCustomerId customer.id if customer.id
|
26
|
+
xml.email customer.email if customer.email
|
27
|
+
end
|
28
|
+
end
|
29
|
+
result = post_data(data)
|
30
|
+
respond_with(customer, :success => result.success?) { |c| c.id = (result.body['createCustomerProfileResponse'] || {})['customerProfileId'] }
|
31
|
+
end
|
32
|
+
|
33
|
+
def update_customer(customer)
|
34
|
+
result = post_data(build_request('updateCustomerProfileRequest') { |xml|
|
35
|
+
xml.tag!('profile') {
|
36
|
+
xml.merchantCustomerId customer.id
|
37
|
+
xml.email customer.email
|
38
|
+
}
|
39
|
+
})
|
40
|
+
respond_with(customer, :success => result.success?)
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove_customer(customer)
|
44
|
+
result = post_data(build_request('deleteCustomerProfileRequest') { |xml|
|
45
|
+
xml.customerProfileId customer.id
|
46
|
+
})
|
47
|
+
respond_with(customer, :success => result.success?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_customer_credit_card(customer, credit_card)
|
51
|
+
result = post_data(build_request('createCustomerPaymentProfileRequest') { |xml|
|
52
|
+
xml.customerProfileId customer.id
|
53
|
+
xml.paymentProfile do
|
54
|
+
billing_info!(xml, customer, credit_card)
|
55
|
+
credit_card_info!(xml, customer, credit_card)
|
56
|
+
end
|
57
|
+
})
|
58
|
+
respond_with(credit_card, :success => result.success?) { |c| c.id = (result.body['createCustomerPaymentProfileResponse'] || {})['customerPaymentProfileId'] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_customer_credit_card(customer, credit_card)
|
62
|
+
result = post_data(build_request('updateCustomerPaymentProfileRequest') { |xml|
|
63
|
+
xml.customerProfileId customer.id
|
64
|
+
xml.paymentProfile do
|
65
|
+
billing_info!(xml, customer, credit_card)
|
66
|
+
credit_card_info!(xml, customer, credit_card)
|
67
|
+
xml.customerPaymentProfileId credit_card.id
|
68
|
+
end
|
69
|
+
})
|
70
|
+
respond_with(credit_card, :success => result.success?)
|
71
|
+
end
|
72
|
+
|
73
|
+
def remove_customer_credit_card(customer, credit_card)
|
74
|
+
result = post_data(build_request('deleteCustomerPaymentProfileRequest') { |xml|
|
75
|
+
xml.customerProfileId customer.id
|
76
|
+
xml.customerPaymentProfileId credit_card.id
|
77
|
+
})
|
78
|
+
respond_with(credit_card, :success => result.success?)
|
79
|
+
end
|
80
|
+
|
81
|
+
def authorize(customer, credit_card, amount)
|
82
|
+
result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
|
83
|
+
xml.transaction do
|
84
|
+
xml.profileTransAuthOnly do
|
85
|
+
xml.amount amount
|
86
|
+
xml.customerProfileId customer.id
|
87
|
+
xml.customerPaymentProfileId credit_card.id
|
88
|
+
end
|
89
|
+
end
|
90
|
+
})
|
91
|
+
respond_with(new_transaction_from_response(result.body), :success => result.success?)
|
92
|
+
end
|
93
|
+
|
94
|
+
def capture(transaction_id, amount)
|
95
|
+
result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
|
96
|
+
xml.transaction do
|
97
|
+
xml.profileTransPriorAuthCapture do
|
98
|
+
xml.amount amount
|
99
|
+
xml.transId transaction_id
|
100
|
+
end
|
101
|
+
end
|
102
|
+
})
|
103
|
+
respond_with(new_transaction_from_response(result.body), :success => result.success?)
|
104
|
+
end
|
105
|
+
|
106
|
+
def refund(transaction_id, amount)
|
107
|
+
result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
|
108
|
+
xml.transaction do
|
109
|
+
xml.profileTransRefund do
|
110
|
+
xml.amount amount
|
111
|
+
xml.transId transaction_id
|
112
|
+
end
|
113
|
+
end
|
114
|
+
})
|
115
|
+
respond_with(new_transaction_from_response(result.body), :success => result.success?)
|
116
|
+
end
|
117
|
+
|
118
|
+
def void(transaction_id)
|
119
|
+
result = post_data(build_request('createCustomerProfileTransactionRequest') { |xml|
|
120
|
+
xml.transaction do
|
121
|
+
xml.profileTransVoid do
|
122
|
+
xml.transId transaction_id
|
123
|
+
end
|
124
|
+
end
|
125
|
+
})
|
126
|
+
respond_with(new_transaction_from_response(result.body), :success => result.success?)
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
|
133
|
+
def post_data(data, headers = {})
|
134
|
+
super(data, {'Content-Type' => 'text/xml'}.merge(headers))
|
135
|
+
end
|
136
|
+
|
137
|
+
def before_post(data)
|
138
|
+
VaultedBilling.logger.debug { "Posting: %s to %s" % [data.inspect, uri.inspect] } if VaultedBilling.logger?
|
139
|
+
end
|
140
|
+
|
141
|
+
def after_post(response)
|
142
|
+
VaultedBilling.logger.debug { "Response code %s (HTTP %d), %s" % [response.message, response.code, response.body.inspect] } if VaultedBilling.logger?
|
143
|
+
response.body = Hash.from_xml(response.body)
|
144
|
+
response.success = response.body[response.body.keys.first]['messages']['resultCode'] == 'Ok'
|
145
|
+
end
|
146
|
+
|
147
|
+
def build_request(request, xml = Builder::XmlMarkup.new(:indent => 2))
|
148
|
+
xml.instruct!
|
149
|
+
xml.tag!(request, :xmlns => 'AnetApi/xml/v1/schema/AnetApiSchema.xsd') do
|
150
|
+
xml.tag!('merchantAuthentication') do
|
151
|
+
xml.name @login
|
152
|
+
xml.transactionKey @password
|
153
|
+
end
|
154
|
+
yield(xml)
|
155
|
+
end
|
156
|
+
xml.target!
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
|
163
|
+
def billing_info!(xml, customer, credit_card)
|
164
|
+
xml.billTo do
|
165
|
+
xml.firstName credit_card.first_name if credit_card.first_name.present?
|
166
|
+
xml.lastName credit_card.last_name if credit_card.last_name.present?
|
167
|
+
xml.address credit_card.street_address if credit_card.street_address.present?
|
168
|
+
xml.city credit_card.locality if credit_card.locality.present?
|
169
|
+
xml.state credit_card.region if credit_card.region.present?
|
170
|
+
xml.zip credit_card.postal_code if credit_card.postal_code.present?
|
171
|
+
xml.country credit_card.country if credit_card.country.present?
|
172
|
+
xml.phoneNumber credit_card.phone if credit_card.phone.present?
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def credit_card_info!(xml, customer, credit_card)
|
177
|
+
xml.payment do
|
178
|
+
xml.creditCard do
|
179
|
+
xml.cardNumber credit_card.card_number if credit_card.card_number.present?
|
180
|
+
xml.expirationDate credit_card.expires_on.strftime("%Y-%m") if credit_card.expires_on.present?
|
181
|
+
xml.cardCode credit_card.cvv_number if credit_card.cvv_number.present?
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def new_transaction_from_response(response)
|
187
|
+
root = response.keys.first
|
188
|
+
if root == 'ErrorResponse'
|
189
|
+
Transaction.new
|
190
|
+
else
|
191
|
+
direct_response = parse_direct_response(response[root]['directResponse'])
|
192
|
+
Transaction.new({
|
193
|
+
:id => direct_response['transaction_id'],
|
194
|
+
:avs_response => direct_response['avs_response'],
|
195
|
+
:cvv_response => direct_response['cvv_response'],
|
196
|
+
:authcode => direct_response['approval_code'],
|
197
|
+
:message => response[root]['messages']['text'],
|
198
|
+
:code => response[root]['messages']['code']
|
199
|
+
})
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def parse_direct_response(string)
|
204
|
+
fields = string.split(',')
|
205
|
+
{
|
206
|
+
'message' => fields[3],
|
207
|
+
'approval_code' => fields[4],
|
208
|
+
'avs_response' => fields[5],
|
209
|
+
'transaction_id' => fields[6],
|
210
|
+
'cvv_response' => fields[39]
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module VaultedBilling
|
4
|
+
module Gateways
|
5
|
+
class Bogus
|
6
|
+
include VaultedBilling::Gateway
|
7
|
+
|
8
|
+
def add_customer(customer)
|
9
|
+
respond_with(customer) { |c| c.id = new_identifier }
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_customer(customer)
|
13
|
+
respond_with customer
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove_customer(customer)
|
17
|
+
respond_with customer
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_customer_credit_card(customer, credit_card)
|
21
|
+
respond_with(credit_card) { |c| c.id = new_identifier }
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_customer_credit_card(customer, credit_card)
|
25
|
+
respond_with credit_card
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove_customer_credit_card(customer, credit_card)
|
29
|
+
respond_with credit_card
|
30
|
+
end
|
31
|
+
|
32
|
+
def authorize(customer, credit_card, amount)
|
33
|
+
transaction_response
|
34
|
+
end
|
35
|
+
|
36
|
+
def void(transaction_id)
|
37
|
+
transaction_response
|
38
|
+
end
|
39
|
+
|
40
|
+
def capture(transaction_id, amount)
|
41
|
+
transaction_response
|
42
|
+
end
|
43
|
+
|
44
|
+
def refund(transaction_id, amount)
|
45
|
+
transaction_response
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
|
52
|
+
def new_identifier
|
53
|
+
Digest::MD5.hexdigest("--#{Time.now.to_f}--#{rand(1_000_000)}--#{rand(1_000_000)}--")
|
54
|
+
end
|
55
|
+
|
56
|
+
def transaction_response
|
57
|
+
respond_with VaultedBilling::Transaction.new(:id => new_identifier)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
module Gateways
|
3
|
+
|
4
|
+
##
|
5
|
+
# An interface to the NMI Customer Vault.
|
6
|
+
#
|
7
|
+
# Currently, the Customer Vault is setup to be one-to-one with
|
8
|
+
# a customer to a credit card. Unlike Authorize.net's CIM, a
|
9
|
+
# single customer cannot carry multiple credit cards. Therefore
|
10
|
+
# most of the individual customer manipulation methods are
|
11
|
+
# simply stubbed to always be successful. The meat of the library
|
12
|
+
# is in the credit card methods and transactions.
|
13
|
+
#
|
14
|
+
class NmiCustomerVault
|
15
|
+
include VaultedBilling::Gateway
|
16
|
+
include VaultedBilling::HttpsInterface
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
self.live_uri = self.test_uri = "https://secure.nmi.com/api/transact.php"
|
20
|
+
self.ssl_pem = File.read(File.expand_path(File.join(File.dirname(__FILE__), '..', 'certificate_authorities', 'verisign.pem')))
|
21
|
+
|
22
|
+
options = HashWithIndifferentAccess.new(options)
|
23
|
+
@username = options[:username]
|
24
|
+
@password = options[:password]
|
25
|
+
self.use_test_uri = options[:test]
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# A stub, since the vault requires both customer information
|
30
|
+
# and credit card information. Actual additions are handled
|
31
|
+
# via the add_customer_credit_card method.
|
32
|
+
#
|
33
|
+
def add_customer(customer)
|
34
|
+
respond_with customer
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# A stub, since the vault requires both customer information
|
39
|
+
# and credit card information. Actual modifications are
|
40
|
+
# handled via the update_customer_credit_card method.
|
41
|
+
#
|
42
|
+
def update_customer(customer)
|
43
|
+
respond_with customer
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# A stub, since the vault requires both customer information
|
48
|
+
# and credit card information. Actual removals are
|
49
|
+
# handled via the remove_customer_credit_card method.
|
50
|
+
#
|
51
|
+
def remove_customer(customer)
|
52
|
+
respond_with customer
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_customer_credit_card(customer, credit_card)
|
56
|
+
response = post_data(storage_data('add_customer', customer, credit_card))
|
57
|
+
respond_with(credit_card, :success => response.success?) do |c|
|
58
|
+
c.id = response.body['customer_vault_id']
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_customer_credit_card(customer, credit_card)
|
63
|
+
response = post_data(storage_data('update_customer', customer, credit_card))
|
64
|
+
respond_with(credit_card, :success => response.success?)
|
65
|
+
end
|
66
|
+
|
67
|
+
def remove_customer_credit_card(customer, credit_card)
|
68
|
+
response = post_data(core_data.merge({
|
69
|
+
:customer_vault => 'delete_customer',
|
70
|
+
:customer_vault_id => credit_card.id
|
71
|
+
}).to_querystring)
|
72
|
+
respond_with(credit_card, :success => response.success?)
|
73
|
+
end
|
74
|
+
|
75
|
+
def authorize(customer, credit_card, amount)
|
76
|
+
response = post_data(transaction_data('auth', {
|
77
|
+
:customer_vault_id => credit_card.id,
|
78
|
+
:amount => amount
|
79
|
+
}))
|
80
|
+
respond_with(new_transaction_from_response(response.body),
|
81
|
+
:success => response.success?)
|
82
|
+
end
|
83
|
+
|
84
|
+
def capture(transaction_id, amount)
|
85
|
+
response = post_data(transaction_data('capture', {
|
86
|
+
:transactionid => transaction_id,
|
87
|
+
:amount => amount
|
88
|
+
}))
|
89
|
+
respond_with(new_transaction_from_response(response.body),
|
90
|
+
:success => response.success?)
|
91
|
+
end
|
92
|
+
|
93
|
+
def refund(transaction_id, amount)
|
94
|
+
response = post_data(transaction_data('refund', {
|
95
|
+
:transactionid => transaction_id,
|
96
|
+
:amount => amount
|
97
|
+
}))
|
98
|
+
respond_with(new_transaction_from_response(response.body),
|
99
|
+
:success => response.success?)
|
100
|
+
end
|
101
|
+
|
102
|
+
def void(transaction_id)
|
103
|
+
response = post_data(transaction_data('void', {
|
104
|
+
:transactionid => transaction_id
|
105
|
+
}))
|
106
|
+
respond_with(new_transaction_from_response(response.body),
|
107
|
+
:success => response.success?)
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
|
114
|
+
def before_post(data)
|
115
|
+
VaultedBilling.logger.debug { "Posting %s to %s" % [data.inspect, uri.to_s] } if VaultedBilling.logger?
|
116
|
+
end
|
117
|
+
|
118
|
+
def after_post(response)
|
119
|
+
VaultedBilling.logger.debug { "Response code %s (HTTP %d), %s" % [response.message, response.code, response.body.inspect] } if VaultedBilling.logger?
|
120
|
+
response.body = Hash.from_querystring(response.body)
|
121
|
+
response.success = response.body['response'] == '1'
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
|
128
|
+
def core_data
|
129
|
+
{
|
130
|
+
:username => @username,
|
131
|
+
:password => @password
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
def transaction_data(method, overrides = {})
|
136
|
+
core_data.merge(overrides).to_querystring
|
137
|
+
end
|
138
|
+
|
139
|
+
def storage_data(method, customer, credit_card)
|
140
|
+
core_data.merge({
|
141
|
+
:customer_vault => method.to_s,
|
142
|
+
:customer_vault_id => credit_card.id,
|
143
|
+
:currency => credit_card.currency,
|
144
|
+
:method => 'creditcard',
|
145
|
+
:ccnumber => credit_card.card_number,
|
146
|
+
:ccexp => credit_card.expires_on.try(:strftime, "%y%m"),
|
147
|
+
:first_name => credit_card.first_name,
|
148
|
+
:last_name => credit_card.last_name,
|
149
|
+
:address1 => credit_card.street_address,
|
150
|
+
:city => credit_card.locality,
|
151
|
+
:state => credit_card.region,
|
152
|
+
:zip => credit_card.postal_code,
|
153
|
+
:country => credit_card.country,
|
154
|
+
:phone => credit_card.phone,
|
155
|
+
:email => customer.email
|
156
|
+
}).to_querystring
|
157
|
+
end
|
158
|
+
|
159
|
+
def new_transaction_from_response(response)
|
160
|
+
Transaction.new({
|
161
|
+
:id => response['transactionid'],
|
162
|
+
:avs_response => response['avsresponse'] == 'Y',
|
163
|
+
:cvv_response => response['cvvresponse'] == 'Y',
|
164
|
+
:authcode => response['authcode'],
|
165
|
+
:message => response['responsetext'],
|
166
|
+
:code => response['response_code']
|
167
|
+
})
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module VaultedBilling
|
5
|
+
module HttpsInterface
|
6
|
+
|
7
|
+
class PostResponse
|
8
|
+
attr_accessor :code
|
9
|
+
attr_accessor :message
|
10
|
+
attr_accessor :body
|
11
|
+
attr_accessor :success
|
12
|
+
attr_accessor :raw_response
|
13
|
+
|
14
|
+
def initialize(http_response)
|
15
|
+
self.raw_response = http_response
|
16
|
+
self.code = http_response.code
|
17
|
+
self.message = http_response.message
|
18
|
+
self.body = http_response.body
|
19
|
+
self.success = ((http_response.code =~ /^2\d{2}/) == 0)
|
20
|
+
end
|
21
|
+
|
22
|
+
alias :success? :success
|
23
|
+
alias :status_code :code
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_writer :use_test_uri
|
27
|
+
attr_writer :ssl_pem
|
28
|
+
|
29
|
+
def live_uri=(input)
|
30
|
+
@live_uri = input ? URI.parse(input) : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_uri=(input)
|
34
|
+
@test_uri = input ? URI.parse(input) : nil
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Returns the protocol and host and any path details that is used
|
39
|
+
# when making communications.
|
40
|
+
#
|
41
|
+
def uri
|
42
|
+
@use_test_uri ? @test_uri : @live_uri
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Posts the given data to the uri and returns the response.
|
47
|
+
#
|
48
|
+
def post_data(data, request_headers = {})
|
49
|
+
request = Net::HTTP::Post.new(uri.path)
|
50
|
+
request.initialize_http_header({
|
51
|
+
'User-Agent' => "vaulted_billing #{VaultedBilling::Version}"
|
52
|
+
}.reverse_merge(request_headers))
|
53
|
+
request.body = data
|
54
|
+
response = Net::HTTP.new(uri.host, uri.port).tap do |https|
|
55
|
+
https.use_ssl = true
|
56
|
+
if @ssl_pem
|
57
|
+
https.cert = OpenSSL::X509::Certificate.new(@ssl_pem)
|
58
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
59
|
+
else
|
60
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
before_post(data)
|
65
|
+
response = response.request(request)
|
66
|
+
PostResponse.new(response).tap do |post_response|
|
67
|
+
after_post(post_response)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
protected :post_data
|
71
|
+
|
72
|
+
def before_post(data)
|
73
|
+
end
|
74
|
+
protected :before_post
|
75
|
+
|
76
|
+
def after_post(response)
|
77
|
+
end
|
78
|
+
protected :after_post
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module VaultedBilling
|
2
|
+
class Transaction
|
3
|
+
attr_accessor :id
|
4
|
+
attr_accessor :authcode
|
5
|
+
attr_accessor :avs_response
|
6
|
+
attr_accessor :cvv_response
|
7
|
+
attr_accessor :code
|
8
|
+
attr_accessor :message
|
9
|
+
|
10
|
+
def initialize(attributes = {})
|
11
|
+
attributes.each_pair do |key, value|
|
12
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vaulted_billing
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 0.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Nathaniel Bibler
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-08-23 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activesupport
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 3
|
30
|
+
version: "2.3"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: builder
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 2
|
42
|
+
- 1
|
43
|
+
- 2
|
44
|
+
version: 2.1.2
|
45
|
+
type: :runtime
|
46
|
+
version_requirements: *id002
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
prerelease: false
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 2
|
56
|
+
- 0
|
57
|
+
- 0
|
58
|
+
- beta
|
59
|
+
- 19
|
60
|
+
version: 2.0.0.beta.19
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: vcr
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 1
|
72
|
+
- 0
|
73
|
+
- 3
|
74
|
+
version: 1.0.3
|
75
|
+
type: :development
|
76
|
+
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: webmock
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 1
|
86
|
+
- 3
|
87
|
+
- 4
|
88
|
+
version: 1.3.4
|
89
|
+
type: :development
|
90
|
+
version_requirements: *id005
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: factory_girl
|
93
|
+
prerelease: false
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
segments:
|
99
|
+
- 1
|
100
|
+
- 3
|
101
|
+
- 2
|
102
|
+
version: 1.3.2
|
103
|
+
type: :development
|
104
|
+
version_requirements: *id006
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
name: faker
|
107
|
+
prerelease: false
|
108
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
- 3
|
115
|
+
- 1
|
116
|
+
version: 0.3.1
|
117
|
+
type: :development
|
118
|
+
version_requirements: *id007
|
119
|
+
description: Several card processors and gateways support offloading the storage of credit card information onto their service. This offloads PCI compliance to the gateway rather than keeping it with each retailer. This library abstracts the interface to many of them, making it trivial to work with any or all of them.
|
120
|
+
email:
|
121
|
+
- nate@envylabs.com
|
122
|
+
executables: []
|
123
|
+
|
124
|
+
extensions: []
|
125
|
+
|
126
|
+
extra_rdoc_files: []
|
127
|
+
|
128
|
+
files:
|
129
|
+
- lib/vaulted_billing/certificate_authorities/entrust.pem
|
130
|
+
- lib/vaulted_billing/certificate_authorities/verisign.pem
|
131
|
+
- lib/vaulted_billing/core_ext/hash.rb
|
132
|
+
- lib/vaulted_billing/credit_card.rb
|
133
|
+
- lib/vaulted_billing/customer.rb
|
134
|
+
- lib/vaulted_billing/gateway.rb
|
135
|
+
- lib/vaulted_billing/gateways/authorize_net_cim.rb
|
136
|
+
- lib/vaulted_billing/gateways/bogus.rb
|
137
|
+
- lib/vaulted_billing/gateways/nmi_customer_vault.rb
|
138
|
+
- lib/vaulted_billing/gateways.rb
|
139
|
+
- lib/vaulted_billing/https_interface.rb
|
140
|
+
- lib/vaulted_billing/transaction.rb
|
141
|
+
- lib/vaulted_billing/version.rb
|
142
|
+
- lib/vaulted_billing.rb
|
143
|
+
- README.md
|
144
|
+
has_rdoc: true
|
145
|
+
homepage: http://github.com/envylabs/vaulted_billing
|
146
|
+
licenses: []
|
147
|
+
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
segments:
|
158
|
+
- 0
|
159
|
+
version: "0"
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
segments:
|
165
|
+
- 1
|
166
|
+
- 3
|
167
|
+
- 6
|
168
|
+
version: 1.3.6
|
169
|
+
requirements: []
|
170
|
+
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 1.3.6
|
173
|
+
signing_key:
|
174
|
+
specification_version: 3
|
175
|
+
summary: A library for working with credit card storage gateways
|
176
|
+
test_files: []
|
177
|
+
|