vindicia 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Isolate +3 -0
- data/README.md +44 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/lib/savon_patches.rb +40 -0
- data/lib/vindicia.rb +469 -0
- data/spec/authenticate.example.rb +1 -0
- data/spec/vindicia_spec.rb +267 -0
- metadata +90 -0
data/Isolate
ADDED
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
This library is an API wrapper for the [Vindicia][] payment gateway.
|
2
|
+
|
3
|
+
# Dependencies
|
4
|
+
|
5
|
+
As of the 0.2.0 release, this gem targets Ruby 1.9.
|
6
|
+
|
7
|
+
It requires the [savon][] SOAP client library, and will use [httpclient][] if present (otherwise just using net/http).
|
8
|
+
|
9
|
+
# Usage
|
10
|
+
|
11
|
+
Firstly, call `Vindicia.authenticate` with your login, password, and target environment. These values will remain cached for all subsequent calls.
|
12
|
+
|
13
|
+
After that, all soap calls are methods on classes in the Vindicia namespace, taking either hashes or other instances as arguments. For example,
|
14
|
+
|
15
|
+
account, created = Vindicia::Account.update({
|
16
|
+
:merchantAccountId => "user42",
|
17
|
+
:name => "bob"
|
18
|
+
})
|
19
|
+
|
20
|
+
Almost all interaction is dynamically driven by the content of the WSDL files, but there are two special cases.
|
21
|
+
|
22
|
+
`Vindicia::Thing#ref` will return a minimal hash for lookups in subsequent calls. So after creating a new `Account`, you can substitute `account.ref` in a purchase call, rather than sending the entire object back over the wire.
|
23
|
+
|
24
|
+
`Vindicia::Thing.find` is a convenience method to call the appropriate method to look up the object by merchant id, simply to reduce redundancy.
|
25
|
+
|
26
|
+
# Development
|
27
|
+
|
28
|
+
Developers looking to build upon this gem will need to install [isolate][], which will sandbox installation of savon, [rspec][], and [jeweler][] in `./tmp`.
|
29
|
+
|
30
|
+
To run the specs, you'll need to copy `authenticate.example.rb` to `authenticate.rb` and fill in your own account information. Additionally, a number of specs depend on existing data in my test account, which should probably be fixed at some point (probably with a rake task to populate the test environment).
|
31
|
+
|
32
|
+
# Known Issues
|
33
|
+
|
34
|
+
HTTPI (a savon dependency) is _really_ chatty logging to stdout, and I haven't figured out a good way to mute it.
|
35
|
+
|
36
|
+
WSDL files are being live-downloaded every run. It'd be nice to cache them locally.
|
37
|
+
|
38
|
+
|
39
|
+
[Vindicia]: http://www.vindicia.com/
|
40
|
+
[savon]: https://github.com/rubiii/savon
|
41
|
+
[httpclient]: https://github.com/nahi/httpclient
|
42
|
+
[isolate]: https://github.com/jbarnette/isolate
|
43
|
+
[rspec]: https://rspec.info/
|
44
|
+
[jeweler]: https://github.com/technicalpickles/jeweler
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'isolate/now'
|
2
|
+
require 'isolate/rake'
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
Jeweler::Tasks.new do |gemspec|
|
10
|
+
gemspec.name = "vindicia"
|
11
|
+
gemspec.summary = "Wrapper interface to Vindicia's SOAP API"
|
12
|
+
gemspec.description = gemspec.summary
|
13
|
+
gemspec.email = "jamie@almlabs.com"
|
14
|
+
gemspec.homepage = "http://github.com/almlabs/vindicia"
|
15
|
+
gemspec.authors = ["Jamie Macey"]
|
16
|
+
gemspec.add_dependency('savon', '=0.8.2')
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
20
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.2
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Savon::SOAP::XML
|
2
|
+
def self.to_hash(xml)
|
3
|
+
# Vindicia xml isn't _completely_ self-documenting. Ensure xsi header exists.
|
4
|
+
if xml =~ /soap.vindicia.com/ and xml !~ /xmlns:xsi/
|
5
|
+
xml = xml.sub(/soap:Envelope/, "soap:Envelope\n xmlns:xsi=\"#{SchemaTypes["xmlns:xsi"]}\"")
|
6
|
+
end
|
7
|
+
(Crack::XML.parse(xml) rescue {}).find_soap_body
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Savon::WSDL::Document
|
12
|
+
def arg_list
|
13
|
+
# simple passthrough
|
14
|
+
parser.arg_list
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Savon::WSDL::ParserWithArgList < Savon::WSDL::Parser
|
19
|
+
attr_reader :arg_list
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
super
|
23
|
+
@arg_list = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def tag_start(tag, attrs)
|
27
|
+
super
|
28
|
+
arg_list_from tag, attrs if @section == :message
|
29
|
+
end
|
30
|
+
|
31
|
+
def arg_list_from(tag, attrs)
|
32
|
+
# Track argument lists so I can use arrays instead of hashes when posting data
|
33
|
+
if tag == "message"
|
34
|
+
@section_name = attrs["name"]
|
35
|
+
@arg_list[@section_name] = []
|
36
|
+
elsif tag == "part"
|
37
|
+
@arg_list[@section_name] << attrs
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/vindicia.rb
ADDED
@@ -0,0 +1,469 @@
|
|
1
|
+
require 'savon'
|
2
|
+
require 'savon_patches'
|
3
|
+
|
4
|
+
Savon.configure do |config|
|
5
|
+
config.log = false # disable logging
|
6
|
+
#config.log_level = :info # changing the log level
|
7
|
+
#config.logger = Rails.logger # using the Rails logger
|
8
|
+
config.soap_version = 1
|
9
|
+
end
|
10
|
+
|
11
|
+
module Vindicia
|
12
|
+
NAMESPACE = "http://soap.vindicia.com/Vindicia"
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :login, :password, :environment
|
16
|
+
def authenticate(login, pass, env=:prodtest)
|
17
|
+
@login = login
|
18
|
+
@password = pass
|
19
|
+
@environment = env.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def version
|
23
|
+
'3.4'
|
24
|
+
end
|
25
|
+
|
26
|
+
def auth
|
27
|
+
{'version' => version, 'login' => login, 'password' => password}
|
28
|
+
end
|
29
|
+
|
30
|
+
def domain
|
31
|
+
case @environment
|
32
|
+
when 'production'; "soap.vindicia.com"
|
33
|
+
when 'staging' ; "soap.staging.sj.vindicia.com"
|
34
|
+
else ; "soap.prodtest.sj.vindicia.com"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def endpoint
|
39
|
+
"https://#{domain}/v#{version}/soap.pl"
|
40
|
+
end
|
41
|
+
|
42
|
+
def xsd(klass)
|
43
|
+
require 'open-uri'
|
44
|
+
url = "http://#{domain}/#{version}/Vindicia.xsd"
|
45
|
+
@xsd_data ||= begin
|
46
|
+
doc = REXML::Document.new(open(url).read)
|
47
|
+
doc.root.get_elements("//xsd:complexType").inject({}){|memo, node|
|
48
|
+
memo[node.attributes["name"]] = node.get_elements("xsd:sequence/xsd:element").map{|e|e.attributes}
|
49
|
+
memo
|
50
|
+
}
|
51
|
+
end
|
52
|
+
@xsd_data[klass]
|
53
|
+
end
|
54
|
+
|
55
|
+
def wsdl(object)
|
56
|
+
"http://#{domain}/#{version}/#{object}.wsdl"
|
57
|
+
end
|
58
|
+
|
59
|
+
def class(type)
|
60
|
+
klass = type.split(':').last
|
61
|
+
klass = singularize($1) if klass =~ /^ArrayOf(.*)$/
|
62
|
+
Vindicia.const_get(klass) rescue nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def type_of(arg)
|
66
|
+
case arg
|
67
|
+
when TrueClass, FalseClass
|
68
|
+
'xsd:boolean'
|
69
|
+
when String
|
70
|
+
'xsd:string'
|
71
|
+
when Fixnum
|
72
|
+
'xsd:int'
|
73
|
+
when Float #, Decimal
|
74
|
+
'xsd:decimal'
|
75
|
+
# TODO: 'xsd:long'
|
76
|
+
when Date, DateTime, Time
|
77
|
+
'xsd:dateTime'
|
78
|
+
#TODO: 'xsd:anyURI'
|
79
|
+
when SoapObject
|
80
|
+
"wsdl:#{arg.classname}"
|
81
|
+
else
|
82
|
+
raise "Unknown type for #{arg.class}~#{arg.inspect}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def coerce(name, type, value)
|
87
|
+
return value if value.kind_of? SoapObject
|
88
|
+
|
89
|
+
case type
|
90
|
+
when /ArrayOf/
|
91
|
+
return [] if value.nil?
|
92
|
+
if value.kind_of? Hash
|
93
|
+
if value[name.to_sym]
|
94
|
+
return coerce(name, type, [value[name.to_sym]].flatten)
|
95
|
+
else
|
96
|
+
value = [value]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
value.map do |val|
|
100
|
+
coerce(name, singularize(type), val)
|
101
|
+
end
|
102
|
+
when /^namesp/, /^vin/, /^tns/
|
103
|
+
type = value[:type] if value.kind_of? Hash
|
104
|
+
if klass = Vindicia.class(type)
|
105
|
+
klass.new(value)
|
106
|
+
else
|
107
|
+
value
|
108
|
+
end
|
109
|
+
when "xsd:int"
|
110
|
+
value.to_i
|
111
|
+
else
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def singularize(type)
|
118
|
+
# Specifically formulated for just the ArrayOf types in Vindicia
|
119
|
+
# The '!' is there to handle singularizing "ses" suffix correctly
|
120
|
+
type.sub(/ArrayOf/,'').
|
121
|
+
sub(/ies$/, 'y').
|
122
|
+
sub(/([sx])es$/, '\1!').
|
123
|
+
sub(/s$/, '').
|
124
|
+
chomp('!')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module XMLBuilder
|
129
|
+
def build_xml(xml, name, type, value)
|
130
|
+
if value.kind_of? Array
|
131
|
+
build_array_xml(xml, name, type, value)
|
132
|
+
else
|
133
|
+
build_tag_xml(xml, name, type, value)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def build_array_xml(xml, name, type, value)
|
138
|
+
attrs = {
|
139
|
+
"xmlns:enc" => "http://schemas.xmlsoap.org/soap/encoding/",
|
140
|
+
"xsi:type" => "enc:Array",
|
141
|
+
"enc:arrayType" => "vin:#{name}[#{value.size}]"
|
142
|
+
}
|
143
|
+
xml.tag!(name, attrs) do |x|
|
144
|
+
value.each do |val|
|
145
|
+
build_tag_xml(x, 'item', type, val)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_tag_xml(xml, name, type, value)
|
151
|
+
case value
|
152
|
+
when Hash
|
153
|
+
Vindicia.class(type).new(value).build(xml, name)
|
154
|
+
when SoapObject
|
155
|
+
value.build(xml, name)
|
156
|
+
when NilClass
|
157
|
+
xml.tag!(name, value, {"xsi:nil" => true})
|
158
|
+
else
|
159
|
+
type = type.sub(/^tns/,'vin')
|
160
|
+
xml.tag!(name, value, {"xsi:type" => type})
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
module SoapClient
|
166
|
+
include XMLBuilder
|
167
|
+
|
168
|
+
def find(id)
|
169
|
+
self.send(:"fetch_by_merchant_#{name.downcase}_id", id)
|
170
|
+
end
|
171
|
+
|
172
|
+
def method_missing(method, *args)
|
173
|
+
# TODO: verify that this method _is_ a method callable on the wsdl,
|
174
|
+
# and defer to super if not.
|
175
|
+
method = underscore(method.to_s).to_sym # back compatability from camelCase api
|
176
|
+
out_vars = nil # set up outside variable
|
177
|
+
|
178
|
+
response = soap.request(:wsdl, method) do |soap, wsdl|
|
179
|
+
out_vars = wsdl.arg_list["#{method.to_s.lower_camelcase}_out"]
|
180
|
+
|
181
|
+
soap.namespaces["xmlns:vin"] = Vindicia::NAMESPACE
|
182
|
+
soap.body = begin
|
183
|
+
xml = Builder::XmlMarkup.new
|
184
|
+
|
185
|
+
key = "#{method.to_s.lower_camelcase}_in"
|
186
|
+
wsdl.arg_list[key].zip([Vindicia.auth] + args).each do |arg, data|
|
187
|
+
build_xml(xml, arg['name'], arg['type'], data)
|
188
|
+
end
|
189
|
+
|
190
|
+
xml.target!
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
values = response.to_hash[:"#{method}_response"]
|
195
|
+
objs = out_vars.map do |var|
|
196
|
+
value = values[underscore(var["name"]).to_sym]
|
197
|
+
Vindicia.coerce(var["name"], var["type"], value)
|
198
|
+
end
|
199
|
+
|
200
|
+
ret = objs.shift
|
201
|
+
|
202
|
+
return [ret] + objs unless objs.first.is_a? SoapObject
|
203
|
+
|
204
|
+
case objs.size
|
205
|
+
when 0
|
206
|
+
ret.request_status = ret
|
207
|
+
ret
|
208
|
+
when 1
|
209
|
+
objs.first.request_status = ret
|
210
|
+
objs.first
|
211
|
+
else
|
212
|
+
objs.first.request_status = ret
|
213
|
+
objs
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def name
|
218
|
+
self.to_s.split('::').last
|
219
|
+
end
|
220
|
+
|
221
|
+
def soap
|
222
|
+
@soap ||= begin
|
223
|
+
Savon::Client.new do |wsdl|
|
224
|
+
wsdl.document = Vindicia.wsdl(name)
|
225
|
+
# Test WSDL files contain production endpoints, must override
|
226
|
+
wsdl.endpoint = Vindicia.endpoint
|
227
|
+
|
228
|
+
# Be sure to parse arg lists for revification w/ custom parser
|
229
|
+
def wsdl.parser
|
230
|
+
@parser ||= begin
|
231
|
+
parser = Savon::WSDL::ParserWithArgList.new
|
232
|
+
REXML::Document.parse_stream self.document, parser
|
233
|
+
parser
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
def underscore(camel_cased_word)
|
242
|
+
word = camel_cased_word.to_s.dup
|
243
|
+
word.gsub!(/::/, '/')
|
244
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
245
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
246
|
+
word.tr!("-", "_")
|
247
|
+
word.downcase!
|
248
|
+
word
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
class SoapObject
|
253
|
+
include XMLBuilder
|
254
|
+
include Comparable
|
255
|
+
attr_accessor :request_status
|
256
|
+
|
257
|
+
def attributes
|
258
|
+
@attributes ||= Vindicia.xsd(classname).inject({}) do |memo, attr|
|
259
|
+
memo[attr["name"]] = attr["type"]
|
260
|
+
memo["vid"] = attr["type"] if attr["name"] == "VID" # oh, casing
|
261
|
+
memo
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def initialize(arg=nil)
|
266
|
+
case arg
|
267
|
+
when String, nil
|
268
|
+
arg = {"merchant#{classname}Id" => arg}
|
269
|
+
when Array
|
270
|
+
arg = Hash[arg]
|
271
|
+
end
|
272
|
+
|
273
|
+
arg.each do |key, value|
|
274
|
+
if key == :type
|
275
|
+
# XML->Hash conversion causes conflict between 'type' metadata
|
276
|
+
# and 'type' data field in CreditCard (+others?)
|
277
|
+
# so extract the value we want.
|
278
|
+
value = [value].flatten.reject{|e|e =~ /:/}.first
|
279
|
+
next if value.nil?
|
280
|
+
end
|
281
|
+
# skip metadata
|
282
|
+
next if [:xmlns, :array_type].include? key
|
283
|
+
type = attributes[camelcase(key.to_s)]
|
284
|
+
cast_as_soap_object(type, value) do |obj|
|
285
|
+
value = obj
|
286
|
+
end
|
287
|
+
|
288
|
+
key = underscore(key) # old camelCase back-compat
|
289
|
+
instance_variable_set("@#{key}", value)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def build(xml, tag)
|
294
|
+
xml.tag!(tag, {"xsi:type" => "vin:#{classname}"}) do |xml|
|
295
|
+
attributes.each do |name, type|
|
296
|
+
next if name == 'vid'
|
297
|
+
|
298
|
+
value = instance_variable_get("@#{underscore(name)}") || instance_variable_get("@#{name}")
|
299
|
+
build_xml(xml, name, type, value)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def cast_as_soap_object(type, value)
|
305
|
+
return nil if type.nil? or value.nil?
|
306
|
+
return value unless type =~ /tns:/
|
307
|
+
|
308
|
+
if type =~ /ArrayOf/
|
309
|
+
type = singularize(type.sub('ArrayOf',''))
|
310
|
+
|
311
|
+
if value.kind_of?(Hash) && value[:array_type]
|
312
|
+
key = value.keys - [:type, :array_type, :xmlns]
|
313
|
+
value = value[key.first]
|
314
|
+
end
|
315
|
+
value = [value] unless value.kind_of? Array
|
316
|
+
|
317
|
+
ary = value.map{|e| cast_as_soap_object(type, e) }
|
318
|
+
yield ary if block_given?
|
319
|
+
return ary
|
320
|
+
end
|
321
|
+
|
322
|
+
if klass = Vindicia.class(type)
|
323
|
+
obj = klass.new(value)
|
324
|
+
yield obj if block_given?
|
325
|
+
return obj
|
326
|
+
else
|
327
|
+
value
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def classname
|
332
|
+
self.class.to_s.split('::').last
|
333
|
+
end
|
334
|
+
|
335
|
+
def each
|
336
|
+
attributes.each do |attr, type|
|
337
|
+
value = self.send(attr)
|
338
|
+
yield attr, value if value
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def key?(k)
|
343
|
+
attributes.key?(k.to_s)
|
344
|
+
end
|
345
|
+
|
346
|
+
def type(*args)
|
347
|
+
# type is deprecated, override so that it does a regular attribute lookup
|
348
|
+
method_missing(:type, *args)
|
349
|
+
end
|
350
|
+
|
351
|
+
def method_missing(method, *args)
|
352
|
+
attr = underscore(method.to_s).to_sym # back compatability from camelCase api
|
353
|
+
key = camelcase(attr.to_s)
|
354
|
+
|
355
|
+
if attributes[key]
|
356
|
+
Vindicia.coerce(key, attributes[key], instance_variable_get("@#{attr}"))
|
357
|
+
else
|
358
|
+
super
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# TODO: respond_to?
|
363
|
+
|
364
|
+
def ref
|
365
|
+
key = instance_variable_get("@merchant#{classname}Id")
|
366
|
+
ukey = instance_variable_get("@merchant_#{underscore(classname)}_id")
|
367
|
+
{"merchant#{classname}Id" => ukey || key}
|
368
|
+
end
|
369
|
+
|
370
|
+
def to_hash
|
371
|
+
instance_variables.inject({}) do |result, ivar|
|
372
|
+
name = ivar[1..-1]
|
373
|
+
value = instance_variable_get(ivar)
|
374
|
+
case value
|
375
|
+
when SoapObject
|
376
|
+
value = value.to_hash
|
377
|
+
when Array
|
378
|
+
value = value.map{|e| e.kind_of?(SoapObject) ? e.to_hash : e}
|
379
|
+
end
|
380
|
+
result[name] = value
|
381
|
+
result
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
private
|
386
|
+
def underscore(camel_cased_word)
|
387
|
+
word = camel_cased_word.to_s.dup
|
388
|
+
word.gsub!(/::/, '/')
|
389
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
390
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
391
|
+
word.tr!("-", "_")
|
392
|
+
word.downcase!
|
393
|
+
word
|
394
|
+
end
|
395
|
+
|
396
|
+
def camelcase(underscored_word)
|
397
|
+
underscored_word.gsub(/_(.)/) do |m| m.upcase.sub('_','') end
|
398
|
+
end
|
399
|
+
|
400
|
+
def singularize(type)
|
401
|
+
# Specifically formulated for just the ArrayOf types in Vindicia
|
402
|
+
type.sub(/ies$/, 'y').
|
403
|
+
sub(/([sx])es$/, '\1').
|
404
|
+
sub(/s$/, '')
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# API classes
|
409
|
+
class Account < SoapObject; extend SoapClient end
|
410
|
+
class Activity < SoapObject; extend SoapClient end
|
411
|
+
class Address < SoapObject; extend SoapClient end
|
412
|
+
class AutoBill < SoapObject; extend SoapClient end
|
413
|
+
class BillingPlan < SoapObject; extend SoapClient end
|
414
|
+
class Chargeback < SoapObject; extend SoapClient end
|
415
|
+
class Entitlement < SoapObject; extend SoapClient end
|
416
|
+
class PaymentMethod < SoapObject; extend SoapClient end
|
417
|
+
class PaymentProvider < SoapObject; extend SoapClient end
|
418
|
+
class Product < SoapObject; extend SoapClient end
|
419
|
+
class Refund < SoapObject; extend SoapClient end
|
420
|
+
class Transaction < SoapObject; extend SoapClient end
|
421
|
+
|
422
|
+
# customized data classes
|
423
|
+
class Return < SoapObject
|
424
|
+
def code; self.return_code.to_i; end
|
425
|
+
def response; self.return_string; end
|
426
|
+
end
|
427
|
+
|
428
|
+
# Stub data types
|
429
|
+
class ActivityCancellation < SoapObject ; end
|
430
|
+
class ActivityEmailContact < SoapObject ; end
|
431
|
+
class ActivityFulfillment < SoapObject ; end
|
432
|
+
class ActivityLogin < SoapObject ; end
|
433
|
+
class ActivityLogout < SoapObject ; end
|
434
|
+
class ActivityNamedValue < SoapObject ; end
|
435
|
+
class ActivityNote < SoapObject ; end
|
436
|
+
class ActivityPhoneContact < SoapObject ; end
|
437
|
+
class ActivityTypeArg < SoapObject ; end
|
438
|
+
class ActivityURIView < SoapObject ; end
|
439
|
+
class ActivityUsage < SoapObject ; end
|
440
|
+
class Authentication < SoapObject ; end
|
441
|
+
class BillingPlanPeriod < SoapObject ; end
|
442
|
+
class BillingPlanPrice < SoapObject ; end
|
443
|
+
class Boleto < SoapObject ; end
|
444
|
+
class CancelResult < SoapObject ; end
|
445
|
+
class CaptureResult < SoapObject ; end
|
446
|
+
class CreditCard < SoapObject ; end
|
447
|
+
class DirectDebit < SoapObject ; end
|
448
|
+
class ECP < SoapObject ; end
|
449
|
+
class ElectronicSignature < SoapObject ; end
|
450
|
+
class EmailTemplate < SoapObject ; end
|
451
|
+
class MerchantEntitlementId < SoapObject ; end
|
452
|
+
class MetricStatistics < SoapObject ; end
|
453
|
+
class NameValuePair < SoapObject ; end
|
454
|
+
class PayPal < SoapObject ; end
|
455
|
+
class SalesTax < SoapObject ; end
|
456
|
+
class ScoreCode < SoapObject ; end
|
457
|
+
class TaxExemption < SoapObject ; end
|
458
|
+
class Token < SoapObject ; end
|
459
|
+
class TokenAmount < SoapObject ; end
|
460
|
+
class TokenTransaction < SoapObject ; end
|
461
|
+
class TransactionItem < SoapObject ; end
|
462
|
+
class TransactionStatus < SoapObject ; end
|
463
|
+
class TransactionStatusBoleto < SoapObject ; end
|
464
|
+
class TransactionStatusCreditCard < SoapObject ; end
|
465
|
+
class TransactionStatusDirectDebit < SoapObject ; end
|
466
|
+
class TransactionStatusECP < SoapObject ; end
|
467
|
+
class TransactionStatusPayPal < SoapObject ; end
|
468
|
+
class WebSession < SoapObject ; end
|
469
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Vindicia.authenticate('login', 'password', :prodtest)
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'vindicia'
|
2
|
+
require 'authenticate'
|
3
|
+
|
4
|
+
describe Vindicia::Account do
|
5
|
+
it 'should return a singleton soap wrapper' do
|
6
|
+
a = Vindicia::Account
|
7
|
+
b = Vindicia::Account
|
8
|
+
a.soap.should be_equal(b.soap)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#new' do
|
12
|
+
it 'should handle nil' do
|
13
|
+
Vindicia::Account.new.ref.should == {'merchantAccountId' => nil}
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should handle a string' do
|
17
|
+
Vindicia::Account.new('thing').ref.should == {'merchantAccountId' => 'thing'}
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should handle a hash' do
|
21
|
+
Vindicia::Account.new(:merchantAccountId => 'thing').ref.should == {'merchantAccountId' => 'thing'}
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should handle multiple words' do
|
25
|
+
Vindicia::AutoBill.new(:merchantAutoBillId => 'thing').ref.should == {'merchantAutoBillId' => 'thing'}
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should handle underscored words' do
|
29
|
+
Vindicia::AutoBill.new(:merchant_auto_bill_id => 'thing').ref.should == {'merchantAutoBillId' => 'thing'}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#update' do
|
34
|
+
it 'should create/update an account' do
|
35
|
+
account, created = Vindicia::Account.update({
|
36
|
+
:merchantAccountId => "bob#{Time.now.to_i}",
|
37
|
+
:name => "bob"
|
38
|
+
})
|
39
|
+
account.VID.should =~ /^[0-9a-f]{40}$/
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should accept raw objects' do
|
43
|
+
account, created = Vindicia::Account.update(Vindicia::Account.new({
|
44
|
+
:merchantAccountId => "bob#{Time.now.to_i}",
|
45
|
+
:name => "long"
|
46
|
+
}))
|
47
|
+
account.name.should == "long"
|
48
|
+
account.VID.should =~ /^[0-9a-f]{40}$/
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should update a name' do
|
52
|
+
account, created = Vindicia::Account.update({
|
53
|
+
:merchantAccountId => '123',
|
54
|
+
:name => 'bob'
|
55
|
+
})
|
56
|
+
account.name.should == 'bob'
|
57
|
+
|
58
|
+
account, created = Vindicia::Account.update({
|
59
|
+
:merchantAccountId => '123',
|
60
|
+
:name => 'sam'
|
61
|
+
})
|
62
|
+
account.name.should == 'sam'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#find' do
|
67
|
+
it 'should return an account' do
|
68
|
+
name = "bob#{Time.now.to_i}"
|
69
|
+
Vindicia::Account.update({
|
70
|
+
:merchantAccountId => '123',
|
71
|
+
:name => name
|
72
|
+
})
|
73
|
+
account = Vindicia::Account.find('123')
|
74
|
+
account.name.should == name
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
describe Vindicia::SoapObject do
|
81
|
+
it 'should map associated classes' do
|
82
|
+
product = Vindicia::Product.new(
|
83
|
+
:default_billing_plan => {:status => "Active"}
|
84
|
+
)
|
85
|
+
product.default_billing_plan.should be_kind_of(Vindicia::BillingPlan)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should deserialze arrays' do
|
89
|
+
plan = Vindicia::BillingPlan.new(
|
90
|
+
:periods => [{
|
91
|
+
:quantity => "1"
|
92
|
+
}]
|
93
|
+
)
|
94
|
+
plan.periods.should be_kind_of(Array)
|
95
|
+
plan.periods.size.should == 1
|
96
|
+
plan.periods.first.should be_kind_of(Vindicia::BillingPlanPeriod)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should deserialize arrays from soap' do
|
100
|
+
plan = Vindicia::BillingPlan.new(
|
101
|
+
:status => "Active",
|
102
|
+
:periods => {
|
103
|
+
:periods => {
|
104
|
+
:do_not_notify_first_bill => true,
|
105
|
+
:prices => {
|
106
|
+
:prices => {
|
107
|
+
:type => "namesp32:BillingPlanPrice",
|
108
|
+
:xmlns => "",
|
109
|
+
:currency => "USD",
|
110
|
+
:amount => "49.00",
|
111
|
+
:price_list_name => {:type=>"xsd:string", :xmlns=>""}
|
112
|
+
},
|
113
|
+
:type => "namesp32:ArrayOfBillingPlanPrices",
|
114
|
+
:xmlns => "",
|
115
|
+
:array_type => "namesp32:BillingPlanPrice[1]"
|
116
|
+
},
|
117
|
+
:type => ["Month", "namesp32:BillingPlanPeriod"],
|
118
|
+
:expire_warning_days => "0",
|
119
|
+
:quantity => "1",
|
120
|
+
:cycles => "0",
|
121
|
+
:xmlns => ""
|
122
|
+
},
|
123
|
+
:type => "namesp32:ArrayOfBillingPlanPeriods",
|
124
|
+
:xmlns => "",
|
125
|
+
:array_type => "namesp32:BillingPlanPeriod[1]"
|
126
|
+
}
|
127
|
+
)
|
128
|
+
|
129
|
+
plan.periods.should be_kind_of(Array)
|
130
|
+
plan.periods.size.should == 1
|
131
|
+
plan.periods.first.should be_kind_of(Vindicia::BillingPlanPeriod)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe Vindicia::Product do
|
136
|
+
it 'should look up by merchant id' do
|
137
|
+
product = Vindicia::Product.find('em-2-PREMIUM-USD')
|
138
|
+
product.description.should == 'Premium (49.0 USD)'
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should bundle the "Return" status in the Product' do
|
142
|
+
product = Vindicia::Product.find('em-2-PREMIUM-USD')
|
143
|
+
product.request_status.code.should == 200
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe Vindicia do
|
148
|
+
before :each do
|
149
|
+
# Product, BillingPlan are set up in CashBox by hand
|
150
|
+
account, created = Vindicia::Account.update({
|
151
|
+
:merchantAccountId => Time.now.to_i.to_s,
|
152
|
+
:name => "Integration User #{Time.now.to_i}"
|
153
|
+
})
|
154
|
+
@account, validated = Vindicia::Account.updatePaymentMethod(account.ref, {
|
155
|
+
# Payment Method
|
156
|
+
:type => 'CreditCard',
|
157
|
+
:creditCard => {
|
158
|
+
:account => '4783684405207461',
|
159
|
+
:expirationDate => '201207'
|
160
|
+
},
|
161
|
+
:accountHolderName => 'John Smith',
|
162
|
+
:billingAddress => {
|
163
|
+
:name => 'John Smith',
|
164
|
+
:addr1 => '123 Main St',
|
165
|
+
:city => 'Toronto',
|
166
|
+
:district => 'Ontario',
|
167
|
+
:country => 'Canada',
|
168
|
+
:postalCode => 'M4V 5X7'
|
169
|
+
},
|
170
|
+
:merchantPaymentMethodId => "Purchase.id #{Time.now.to_i}"
|
171
|
+
}, true, 'Validate', nil)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should map associated objects to a Vindicia:: class' do
|
175
|
+
@account.paymentMethods.first.class.should == Vindicia::PaymentMethod
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should map associated arrays to a Vindicia:: class' do
|
179
|
+
transaction = Vindicia::Transaction.auth({
|
180
|
+
:account => @account.ref,
|
181
|
+
:merchantTransactionId => "Purchase.id (#{Time.now.to_i})",
|
182
|
+
:sourcePaymentMethod => {:VID => @account.paymentMethods.first.VID},
|
183
|
+
:amount => 49.00,
|
184
|
+
:transactionItems => [{:sku => 'sku', :name => 'Established Men Subscription', :price => 49.00, :quantity => 1}]
|
185
|
+
}, 100, false)
|
186
|
+
transaction.request_status.code.should == 200
|
187
|
+
|
188
|
+
transaction.statusLog.first.should be_kind_of(Vindicia::TransactionStatus)
|
189
|
+
end
|
190
|
+
|
191
|
+
describe Vindicia::AutoBill do
|
192
|
+
it 'should create recurring billing' do
|
193
|
+
@product = Vindicia::Product.new('em-2-PREMIUM-USD')
|
194
|
+
@billing = Vindicia::BillingPlan.new('em-2-PREMIUM-USD')
|
195
|
+
autobill, created, authstatus, firstBillDate, firstBillAmount, firstBillingCurrency = \
|
196
|
+
Vindicia::AutoBill.update({
|
197
|
+
:account => @account.ref,
|
198
|
+
:product => @product.ref,
|
199
|
+
:billingPlan => @billing.ref
|
200
|
+
}, 'Fail', true, 100)
|
201
|
+
|
202
|
+
autobill.request_status.code.should == 200
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe Vindicia::Transaction do
|
207
|
+
describe '#auth' do
|
208
|
+
it 'should auth a purchase' do
|
209
|
+
payment_vid = @account.paymentMethods.first.VID
|
210
|
+
transaction = Vindicia::Transaction.auth({
|
211
|
+
:account => @account.ref,
|
212
|
+
:merchantTransactionId => "Purchase.id (#{Time.now.to_i})",
|
213
|
+
:sourcePaymentMethod => {:VID => payment_vid},
|
214
|
+
:amount => 49.00,
|
215
|
+
:transactionItems => [{:sku => 'sku', :name => 'Established Men Subscription', :price => 49.00, :quantity => 1}]
|
216
|
+
#:divisionNumber xsd:string
|
217
|
+
#:userAgent xsd:string
|
218
|
+
#:sourceMacAddress xsd:string
|
219
|
+
#:sourceIp xsd:string
|
220
|
+
#:billingStatementIdentifier xsd:string
|
221
|
+
}, 100, false)
|
222
|
+
transaction.request_status.code.should == 200
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe '#capture' do
|
227
|
+
before :each do
|
228
|
+
payment_vid = @account.paymentMethods.first.VID
|
229
|
+
@transaction = Vindicia::Transaction.auth({
|
230
|
+
:account => @account.ref,
|
231
|
+
:merchantTransactionId => "Purchase.id (#{Time.now.to_i})",
|
232
|
+
:sourcePaymentMethod => {:VID => payment_vid},
|
233
|
+
:amount => 49.00,
|
234
|
+
:transactionItems => [{:sku => 'sku', :name => 'Established Men Subscription', :price => 49.00, :quantity => 1}]
|
235
|
+
}, 100, false)
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'should capture an authorized purchase' do
|
239
|
+
ret, success, fail, results = Vindicia::Transaction.capture([@transaction.ref])
|
240
|
+
success.should == 1
|
241
|
+
results.first.merchantTransactionId.should == @transaction.merchantTransactionId
|
242
|
+
results.first.returnCode.should == 200
|
243
|
+
|
244
|
+
pending 'a way to force immediate capturing'
|
245
|
+
transaction = Vindicia::Transaction.find(@transaction.merchantTransactionId)
|
246
|
+
transaction.statusLog.status.should == 'Captured'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
describe '#authCapture' do
|
251
|
+
it 'should return a captured transaction' do
|
252
|
+
payment_vid = @account.paymentMethods.first.VID
|
253
|
+
transaction = Vindicia::Transaction.authCapture({
|
254
|
+
:account => @account.ref,
|
255
|
+
:merchantTransactionId => "Purchase.id (#{Time.now.to_i})",
|
256
|
+
:sourcePaymentMethod => {:VID => payment_vid},
|
257
|
+
:amount => 49.00,
|
258
|
+
:transactionItems => [{:sku => 'sku', :name => 'Established Men Subscription', :price => 49.00, :quantity => 1}]
|
259
|
+
}, false)
|
260
|
+
transaction.request_status.code.should == 200
|
261
|
+
|
262
|
+
pending 'a way to force immediate capturing'
|
263
|
+
transaction.statusLog.status.should == 'Captured'
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vindicia
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 2
|
10
|
+
version: 0.2.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jamie Macey
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-24 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: savon
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 59
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 8
|
33
|
+
- 2
|
34
|
+
version: 0.8.2
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: Wrapper interface to Vindicia's SOAP API
|
38
|
+
email: jamie@almlabs.com
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- README.md
|
45
|
+
files:
|
46
|
+
- Isolate
|
47
|
+
- README.md
|
48
|
+
- Rakefile
|
49
|
+
- VERSION
|
50
|
+
- lib/savon_patches.rb
|
51
|
+
- lib/vindicia.rb
|
52
|
+
- spec/authenticate.example.rb
|
53
|
+
- spec/vindicia_spec.rb
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/almlabs/vindicia
|
56
|
+
licenses: []
|
57
|
+
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
hash: 3
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.3.7
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: Wrapper interface to Vindicia's SOAP API
|
88
|
+
test_files:
|
89
|
+
- spec/authenticate.example.rb
|
90
|
+
- spec/vindicia_spec.rb
|