vindicia 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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