vindicia 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|