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 ADDED
@@ -0,0 +1,3 @@
1
+ gem 'jeweler'
2
+ gem 'rspec'
3
+ gem 'savon'
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