active_zuora 1.3.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.octopolo.yml +4 -0
  4. data/.soyuz.yml +13 -0
  5. data/.travis.yml +7 -0
  6. data/CHANGELOG.markdown +41 -0
  7. data/Gemfile +4 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.md +191 -0
  10. data/Rakefile +9 -53
  11. data/TODO.md +2 -0
  12. data/active_zuora.gemspec +25 -59
  13. data/lib/active_zuora.rb +44 -12
  14. data/lib/active_zuora/amend.rb +43 -0
  15. data/lib/active_zuora/base.rb +84 -0
  16. data/lib/active_zuora/batch_subscribe.rb +53 -0
  17. data/lib/active_zuora/belongs_to_associations.rb +56 -0
  18. data/lib/active_zuora/billing_preview.rb +49 -0
  19. data/lib/active_zuora/collection_proxy.rb +38 -0
  20. data/lib/active_zuora/connection.rb +47 -0
  21. data/lib/active_zuora/fields.rb +129 -0
  22. data/lib/active_zuora/fields/array_field_decorator.rb +28 -0
  23. data/lib/active_zuora/fields/boolean_field.rb +12 -0
  24. data/lib/active_zuora/fields/date_field.rb +18 -0
  25. data/lib/active_zuora/fields/date_time_field.rb +19 -0
  26. data/lib/active_zuora/fields/decimal_field.rb +12 -0
  27. data/lib/active_zuora/fields/field.rb +76 -0
  28. data/lib/active_zuora/fields/integer_field.rb +11 -0
  29. data/lib/active_zuora/fields/object_field.rb +31 -0
  30. data/lib/active_zuora/fields/string_field.rb +11 -0
  31. data/lib/active_zuora/generate.rb +43 -0
  32. data/lib/active_zuora/generator.rb +244 -0
  33. data/lib/active_zuora/has_many_associations.rb +37 -0
  34. data/lib/active_zuora/has_many_proxy.rb +50 -0
  35. data/lib/active_zuora/lazy_attr.rb +52 -0
  36. data/lib/active_zuora/persistence.rb +172 -0
  37. data/lib/active_zuora/relation.rb +260 -0
  38. data/lib/active_zuora/scoping.rb +50 -0
  39. data/lib/active_zuora/subscribe.rb +42 -0
  40. data/lib/active_zuora/version.rb +3 -0
  41. data/lib/active_zuora/z_object.rb +21 -0
  42. data/spec/account_integration_spec.rb +41 -0
  43. data/spec/base_spec.rb +39 -0
  44. data/spec/belongs_to_associations_spec.rb +35 -0
  45. data/spec/collection_proxy_spec.rb +28 -0
  46. data/spec/connection_spec.rb +66 -0
  47. data/spec/fields/date_field_spec.rb +35 -0
  48. data/spec/has_many_integration_spec.rb +53 -0
  49. data/spec/lazy_attr_spec.rb +22 -0
  50. data/spec/spec_helper.rb +34 -0
  51. data/spec/subscribe_integration_spec.rb +344 -0
  52. data/spec/zobject_integration_spec.rb +104 -0
  53. data/wsdl/zuora.wsdl +1548 -0
  54. metadata +141 -53
  55. data/LICENSE +0 -202
  56. data/README.rdoc +0 -15
  57. data/VERSION +0 -1
  58. data/custom_fields.yml +0 -17
  59. data/lib/zuora/ZUORA.rb +0 -1398
  60. data/lib/zuora/ZUORADriver.rb +0 -128
  61. data/lib/zuora/ZUORAMappingRegistry.rb +0 -1488
  62. data/lib/zuora/ZuoraServiceClient.rb +0 -124
  63. data/lib/zuora/account.rb +0 -4
  64. data/lib/zuora/api.rb +0 -18
  65. data/lib/zuora/contact.rb +0 -4
  66. data/lib/zuora/rate_plan.rb +0 -4
  67. data/lib/zuora/rate_plan_data.rb +0 -4
  68. data/lib/zuora/subscribe_options.rb +0 -4
  69. data/lib/zuora/subscribe_request.rb +0 -4
  70. data/lib/zuora/subscribe_with_existing_account_request.rb +0 -4
  71. data/lib/zuora/subscription.rb +0 -4
  72. data/lib/zuora/subscription_data.rb +0 -4
  73. data/lib/zuora/zobject.rb +0 -52
  74. data/lib/zuora_client.rb +0 -181
  75. data/lib/zuora_interface.rb +0 -199
@@ -1,13 +1,45 @@
1
- require 'delegate'
2
- require 'zuora_client'
3
- require 'zuora/zobject'
4
- require 'zuora/account'
5
- require 'zuora/contact'
6
- require 'zuora/rate_plan'
7
- require 'zuora/rate_plan_data'
8
- require 'zuora/subscribe_options'
9
- require 'zuora/subscribe_request'
10
- require 'zuora/subscribe_with_existing_account_request'
11
- require 'zuora/subscription'
12
- require 'zuora/subscription_data'
1
+ require 'savon'
2
+ require 'active_model'
3
+ require 'active_support/all'
13
4
 
5
+ require 'active_zuora/connection'
6
+ require 'active_zuora/generator'
7
+ require 'active_zuora/fields'
8
+ require 'active_zuora/belongs_to_associations'
9
+ require 'active_zuora/base'
10
+ require 'active_zuora/relation'
11
+ require 'active_zuora/scoping'
12
+ require 'active_zuora/persistence'
13
+ require 'active_zuora/has_many_proxy'
14
+ require 'active_zuora/has_many_associations'
15
+ require 'active_zuora/z_object'
16
+ require 'active_zuora/subscribe'
17
+ require 'active_zuora/amend'
18
+ require 'active_zuora/generate'
19
+ require 'active_zuora/billing_preview'
20
+ require 'active_zuora/batch_subscribe'
21
+ require 'active_zuora/collection_proxy'
22
+ require 'active_zuora/lazy_attr'
23
+
24
+ module ActiveZuora
25
+
26
+ # Setup configuration. None of this sends a request.
27
+ def self.configure(configuration)
28
+ # Set some sensible defaults with the savon SOAP client.
29
+ Savon.configure do |config|
30
+ config.log = HTTPI.log = configuration[:log] || false
31
+ config.log_level = configuration[:log_level] || :info
32
+ config.logger = configuration[:logger] if configuration[:logger]
33
+ config.logger.filter = configuration[:log_filters] || [:password, :SessionHeader]
34
+ config.raise_errors = true
35
+ end
36
+ # Create a default connection on Base
37
+ Base.connection = Connection.new(configuration)
38
+ end
39
+
40
+ def self.generate_classes(options={})
41
+ generator = Generator.new(Base.connection.soap_client.wsdl.parser, options)
42
+ generator.generate_classes
43
+ end
44
+
45
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveZuora
2
+ module Amend
3
+
4
+ # This is meant to be included onto a AmendRequest class.
5
+ # Returns true/false on success.
6
+ # Result hash is stored in #result.
7
+ # If success, the ids will be set in the given amendments.
8
+ # If failure, errors will be present on object.
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ include Base
14
+ attr_accessor :result
15
+ end
16
+
17
+ def amend
18
+ self.result = self.class.connection.request(:amend) do |soap|
19
+ soap.body do |xml|
20
+ build_xml(xml, soap,
21
+ :namespace => soap.namespace,
22
+ :element_name => :requests,
23
+ :force_type => true)
24
+ end
25
+ end[:amend_response][:results]
26
+ if result[:success]
27
+ [result[:amendment_ids]].flatten.compact.each_with_index do |id, i|
28
+ amendments[i].id = id
29
+ end
30
+ clear_changed_attributes
31
+ true
32
+ else
33
+ add_zuora_errors(result[:errors])
34
+ false
35
+ end
36
+ end
37
+
38
+ def amend!
39
+ raise "Could not amend: #{errors.full_messages.join ', '}" unless amend
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,84 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+
4
+ module ActiveZuora
5
+ module Base
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ # This is the default connection object to use for all Base classes.
10
+ # Each individual Base class can overwrite it.
11
+ class << self
12
+ attr_accessor :connection
13
+ end
14
+
15
+ included do
16
+ include Fields
17
+ include ActiveModel::Validations
18
+ include BelongsToAssociations
19
+ class << self
20
+ attr_accessor :namespace
21
+ attr_writer :zuora_object_name
22
+ attr_writer :connection
23
+ end
24
+ delegate :namespace, :zuora_object_name, :to => 'self.class'
25
+ end
26
+
27
+ def xml_field_names
28
+ # Which field names should be rendered during build_xml.
29
+ # Choose only field names that have been changed.
30
+ # Make sure the order in fields is maintained.
31
+ field_names & changed.map(&:to_sym)
32
+ end
33
+
34
+ def build_xml(xml, soap, options={})
35
+ namespace = options.delete(:namespace) || self.namespace
36
+ qualifier = soap.namespace_by_uri(namespace)
37
+ custom_element_name = options.delete(:element_name)
38
+ element_name = custom_element_name || zuora_object_name
39
+ attributes = options.delete(:force_type) ?
40
+ { "xsi:type" => "#{qualifier}:#{zuora_object_name}" } : {}
41
+
42
+ xml.tag!(qualifier, element_name.to_sym, attributes) do
43
+ xml_field_names.map { |field_name| get_field!(field_name) }.sort(&method(:fields_order)).each do |field|
44
+ field.build_xml(xml, soap, send(field.name), options)
45
+ end
46
+ end
47
+ end
48
+
49
+ def fields_order(a, b)
50
+ if send(a.name) == nil
51
+ send(b.name) == nil ? 0 : -1
52
+ elsif a.name.to_sym == :id
53
+ send(b.name) == nil ? 1 : -1
54
+ else
55
+ (b.name.to_sym == :id || send(b.name) == nil) ? 1 : 0
56
+ end
57
+ end
58
+
59
+ def add_zuora_errors(zuora_errors)
60
+ return if zuora_errors.blank?
61
+ zuora_errors = [zuora_errors] unless zuora_errors.is_a?(Array)
62
+ zuora_errors.each { |error| errors.add(:base, error[:message].capitalize) }
63
+ end
64
+
65
+ module ClassMethods
66
+
67
+ def zuora_object_name
68
+ @zuora_object_name ||= self.name.split("::").last
69
+ end
70
+
71
+ def connection
72
+ @connection || Base.connection
73
+ end
74
+
75
+ def nested_class_name(unnested_class_name)
76
+ # This helper method will take a class name, and nest it inside
77
+ # the same module/class as self.
78
+ (name.split("::")[0..-2] << unnested_class_name).join("::")
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,53 @@
1
+ module ActiveZuora
2
+ module BatchSubscribe
3
+
4
+ # This is meant to be included onto the CollectionProxy class.
5
+ # Returns true if every SubscribeRequest was a success
6
+ # Returns false if any single subsciption Request fails
7
+ # Result hash of each subscribe request is stored in the Subscribe Request #result.
8
+ # If success, the subscription id and account id will be set in those objects.
9
+ # If failure, errors will be present on the request object(s) that had error(s).
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ included do
14
+ include Base
15
+ attr_accessor :result
16
+ end
17
+
18
+ def batch_subscribe
19
+ raise "object must be an ActiveZuora::CollectionProxy object instance" unless self.zuora_object_name == "CollectionProxy"
20
+ self.result = self.class.connection.request(:subscribe) do |soap|
21
+ soap.body do |xml|
22
+ inject(xml) do |memo, el|
23
+ el.build_xml(xml, soap,
24
+ :namespace => soap.namespace,
25
+ :element_name => :subscribes,
26
+ :force_type => true)
27
+ end
28
+ end
29
+ end[:subscribe_response][:result]
30
+
31
+ self.result = [result] unless result.is_a?(Array)
32
+ result.each_with_index do |result, i|
33
+ self.records[i].result = result
34
+ if result[:success]
35
+ #we assume order is maintained by zuora. is it?
36
+ self.records[i].account.id = result[:account_id]
37
+ self.records[i].subscription_data.subscription.id = result[:subscription_id]
38
+ self.records[i].clear_changed_attributes
39
+ @status = true
40
+ else
41
+ add_zuora_errors(result[:errors])
42
+ @status = false
43
+ end
44
+ end
45
+ @status
46
+ end
47
+
48
+ def batch_subscribe!
49
+ raise "Could not batch subscribe: #{errors.full_messages.join ', '}" unless batch_subscribe
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveZuora
2
+ module BelongsToAssociations
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ def belongs_to(item, options={})
9
+ class_name = options[:class_name] || nested_class_name(item.to_s.camelize)
10
+ foreign_key = options[:foreign_key] || :"#{item}_id"
11
+ # Add the field if it doesn't already exist.
12
+ field foreign_key, :string unless field? foreign_key
13
+ ivar = "@#{item}"
14
+ loaded_ivar = "@#{item}_loaded"
15
+ # Define the methods on an included module, so we can override
16
+ # them using super.
17
+ generated_attribute_methods.module_eval do
18
+ define_method(item) do
19
+ # Return the object if it was already loaded.
20
+ if instance_variable_get(loaded_ivar)
21
+ return instance_variable_get(ivar)
22
+ else
23
+ # Otherwise find it.
24
+ record = class_name.constantize.find self.send(foreign_key)
25
+ send("#{item}=", record)
26
+ record
27
+ end
28
+ end
29
+ define_method("#{item}=") do |record|
30
+ instance_variable_set(loaded_ivar, true)
31
+ instance_variable_set(ivar, record)
32
+ # Set the foreign key id attribute as well.
33
+ write_attribute(foreign_key, record.try(:id))
34
+ record
35
+ end
36
+ redefine_method("#{foreign_key}=") do |item_id|
37
+ item_id = write_attribute(foreign_key, item_id)
38
+ # Unload the object if the id is different.
39
+ if send("#{item}_loaded?") && send(item).id != item_id
40
+ instance_variable_set(loaded_ivar, false)
41
+ end
42
+ item_id
43
+ end
44
+ define_method("#{item}_loaded?") do
45
+ instance_variable_get(loaded_ivar) || false
46
+ end
47
+ define_method("reload_#{item}") do
48
+ instance_variable_set(loaded_ivar, false)
49
+ send(item)
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ module ActiveZuora
2
+ module BillingPreview
3
+
4
+ # This is meant to be included onto an BillingPreviewRequest class.
5
+ # Returns a BillingPreviewResponse object on success.
6
+ # Result hash is stored in #result.
7
+ # If failure, errors will be present on object.
8
+
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ include Base
13
+ attr_accessor :result
14
+ end
15
+
16
+ def billing_preview
17
+ self.result = self.class.connection.request(:billing_preview) do |soap|
18
+ soap.body do |xml|
19
+ build_xml(xml, soap,
20
+ :namespace => soap.namespace,
21
+ :element_name => :requests,
22
+ :force_type => true)
23
+ end
24
+ end[:billing_preview_response][:results]
25
+
26
+ if result[:success]
27
+ clear_changed_attributes
28
+ filtered_invoice_items = self.result[:invoice_item].map do |invoice_item|
29
+ #Filter out data in the return value that are not valid invoice item fields such as
30
+ # :"@xmlns:ns2"=>"http://object.api.zuora.com/",
31
+ # :"@xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
32
+ # :"@xsi:type"=>"ns2:InvoiceItem"
33
+ invoice_item.select{|key, v| ActiveZuora::InvoiceItem.field_names.include?(key)}
34
+ end
35
+ ActiveZuora::BillingPreviewResult.new(self.result.merge(invoice_item: filtered_invoice_items))
36
+ else
37
+ add_zuora_errors(result[:errors])
38
+ false
39
+ end
40
+ end
41
+
42
+ def billing_preview!
43
+ billing_preview.tap do |preview|
44
+ raise "Could not billing preview: #{errors.full_messages.join ', '}" unless preview
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ module ActiveZuora
2
+ class CollectionProxy
3
+
4
+ include ZObject
5
+ include Enumerable
6
+ include BatchSubscribe
7
+
8
+ attr_reader :records, :zobject_class
9
+
10
+ def initialize(ary = [])
11
+ unless ary.empty?
12
+ raise "objects in collection must be ActiveZuora object instances" unless class_names = ary.map{|object| object.zuora_object_name}.uniq
13
+ raise "objects in collection must be ActiveZuora object instances of the same class" unless class_names.length == 1
14
+ @zobject_class = class_names.first
15
+ end
16
+ @records = ary
17
+ end
18
+
19
+ def add object
20
+ raise "object must be an ActiveZuora object instance" unless object.zuora_object_name
21
+ if records.empty?
22
+ @zobject_class = object.zuora_object_name
23
+ else
24
+ raise "object must be must be ActiveZuora object instances of the same class as other elements in the Collection" unless object.zuora_object_name == zobject_class
25
+ end
26
+ @records.push object
27
+ end
28
+
29
+ def each
30
+ records.each { |r| yield r }
31
+ end
32
+
33
+ def empty?
34
+ records.empty?
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveZuora
2
+ class Connection
3
+
4
+ attr_reader :soap_client
5
+ attr_accessor :custom_header
6
+
7
+ WSDL = File.expand_path('../../../wsdl/zuora.wsdl', __FILE__)
8
+
9
+ def initialize(configuration={})
10
+ # Store login credentials and create SOAP client.
11
+ @username = configuration[:username]
12
+ @password = configuration[:password]
13
+ @soap_client = Savon::Client.new do
14
+ wsdl.document = configuration[:wsdl] || WSDL
15
+ http.proxy = configuration[:http_proxy] if configuration[:http_proxy]
16
+ end
17
+ end
18
+
19
+ def login
20
+ # Returns a session_id upon success, raises an exception on failure.
21
+ # Instance variables aren't available within the soap request block.
22
+ body = { :username => @username, :password => @password }
23
+ header = @custom_header
24
+ @soap_client.request(:login) do
25
+ soap.body = body
26
+ soap.header = header
27
+ end[:login_response][:result][:session]
28
+ end
29
+
30
+ def request(*args, &block)
31
+ # instance variables aren't available within the soap request block for some reason.
32
+ header = { 'SessionHeader' => { 'session' => @session_id } }
33
+ header.merge!(@custom_header) if @custom_header
34
+
35
+ @soap_client.request(*args) do
36
+ soap.header = header
37
+ yield(soap)
38
+ end
39
+ rescue Savon::SOAP::Fault => exception
40
+ # Catch invalid sessions, and re-issue the request.
41
+ raise unless exception.message =~ /INVALID_SESSION/
42
+ @session_id = login
43
+ request(*args, &block)
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,129 @@
1
+ require 'active_zuora/fields/field'
2
+ require 'active_zuora/fields/boolean_field'
3
+ require 'active_zuora/fields/date_field'
4
+ require 'active_zuora/fields/date_time_field'
5
+ require 'active_zuora/fields/decimal_field'
6
+ require 'active_zuora/fields/integer_field'
7
+ require 'active_zuora/fields/object_field'
8
+ require 'active_zuora/fields/string_field'
9
+ require 'active_zuora/fields/array_field_decorator'
10
+
11
+ module ActiveZuora
12
+ module Fields
13
+
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ include ActiveModel::Dirty
18
+ delegate :fields, :field_names, :field?, :get_field, :get_field!,
19
+ :default_attributes, :to => 'self.class'
20
+ end
21
+
22
+ def initialize(attributes={})
23
+ # Start with defaults, and override those with the given attributes.
24
+ self.attributes = default_attributes.merge(attributes)
25
+ end
26
+
27
+ def attributes
28
+ # A requirement of ActiveModel::Attributes.
29
+ # Hash must use string keys.
30
+ attributes = {}
31
+ fields.each { |field| attributes[field.name] = send(field.name) }
32
+ attributes
33
+ end
34
+
35
+ def attributes=(attributes)
36
+ attributes.each { |key, value| send("#{key}=", value) }
37
+ end
38
+
39
+ def untracked_attributes=(attributes)
40
+ # Loads attributes without tracking dirt.
41
+ self.attributes = attributes
42
+ clear_changed_attributes
43
+ attributes
44
+ end
45
+
46
+ def write_attribute(name, value)
47
+ field = get_field!(name)
48
+ value = field.type_cast(value)
49
+ attribute_will_change!(name) if value != send(name)
50
+ instance_variable_set("@#{name}", value)
51
+ value
52
+ end
53
+
54
+ def clear_changed_attributes
55
+ if ActiveSupport.version.to_s.to_f >= 5.2
56
+ clear_changes_information
57
+ else
58
+ changed_attributes.clear
59
+ end
60
+
61
+ # If we have any fields that are also Base objects,
62
+ # clear their attributes as well.
63
+ fields.each { |field| field.clear_changed_attributes(send(field.name)) }
64
+ end
65
+
66
+ module ClassMethods
67
+
68
+ def fields_by_name
69
+ # { :field_name_symbol => field_object }
70
+ @fields ||= {}
71
+ end
72
+
73
+ def fields
74
+ fields_by_name.values
75
+ end
76
+
77
+ def field_names
78
+ fields_by_name.keys
79
+ end
80
+
81
+ def field?(name)
82
+ fields_by_name.key?(name.to_sym)
83
+ end
84
+
85
+ def add_field(name, field)
86
+ fields_by_name[name.to_sym] = field
87
+ # Define the setters, getters, and changed helpers.
88
+ field.define_instance_methods(self)
89
+ end
90
+
91
+ def get_field(name)
92
+ fields_by_name[name.to_sym]
93
+ end
94
+
95
+ def get_field!(name)
96
+ get_field(name) || raise(ArgumentError.new("No field in #{self} named #{name}"))
97
+ end
98
+
99
+ def field(name, type, options={})
100
+ # Check if this field should be an array, don't pass
101
+ # this option down to the field.
102
+ field_is_array = options.delete(:array) || false
103
+ # Create and register the field.
104
+ field = case type
105
+ when :string then StringField.new(name, namespace, options)
106
+ when :boolean then BooleanField.new(name, namespace, options)
107
+ when :integer then IntegerField.new(name, namespace, options)
108
+ when :decimal then DecimalField.new(name, namespace, options)
109
+ when :date then DateField.new(name, namespace, options)
110
+ when :datetime then DateTimeField.new(name, namespace, options)
111
+ when :object
112
+ class_name = options[:class_name] || nested_class_name(name.to_s.camelize)
113
+ ObjectField.new(name, namespace, class_name, options)
114
+ else
115
+ ArgumentError.new "Unknown field type: #{type}"
116
+ end
117
+ field = ArrayFieldDecorator.new(field) if field_is_array
118
+ add_field(name, field)
119
+ end
120
+
121
+ def default_attributes
122
+ default_attributes = {}
123
+ fields.each { |field| default_attributes[field.name] = field.default }
124
+ default_attributes
125
+ end
126
+
127
+ end
128
+ end
129
+ end