active_zuora 1.3.0 → 2.6.0

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.
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
@@ -0,0 +1,37 @@
1
+ module ActiveZuora
2
+ module HasManyAssociations
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ def has_many(items, options={})
9
+ class_name = options[:class_name] || nested_class_name(items.to_s.singularize.camelize)
10
+ foreign_key = options[:foreign_key] || :"#{zuora_object_name.underscore}_id"
11
+ conditions = options[:conditions]
12
+ # :order => :name
13
+ # :order => [:name, :desc]
14
+ order_attribute, order_direction = [*options[:order]]
15
+ ivar = "@#{items}"
16
+ # Define the methods on an included module, so we can override
17
+ # them using super.
18
+ generated_attribute_methods.module_eval do
19
+ define_method(items) do
20
+ if instance_variable_get(ivar)
21
+ return instance_variable_get(ivar)
22
+ else
23
+ relation = class_name.constantize.where(foreign_key => self.id)
24
+ relation = relation.merge(conditions) if conditions.present?
25
+ relation.order_attribute = order_attribute if order_attribute.present?
26
+ relation.order_direction = order_direction if order_direction.present?
27
+ proxy = HasManyProxy.new(self, relation, options)
28
+ instance_variable_set(ivar, proxy)
29
+ proxy
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveZuora
2
+ class HasManyProxy
3
+
4
+ # Wraps around the Relation representing a has_many association
5
+ # to add features like inverse_of loading.
6
+
7
+ attr_reader :scope, :owner
8
+
9
+ delegate :"==", :"===", :"=~", :inspect, :to_s, :to => :to_a
10
+
11
+ def initialize(owner, scope, options={})
12
+ @owner, @scope = owner, scope
13
+ # inverse_of by default. You can opt out with :inverse_of => false
14
+ @inverse_of = (options[:inverse_of] || owner.zuora_object_name.underscore) unless options[:inverse_of] == false
15
+ end
16
+
17
+ def to_a
18
+ if @scope.loaded? || !@inverse_of
19
+ @scope.to_a
20
+ else
21
+ @scope.to_a.each { |record| record.send("#{@inverse_of}=", owner) }
22
+ @scope.to_a
23
+ end
24
+ end
25
+
26
+ alias :all :to_a
27
+
28
+ def reload
29
+ # If reload is called directly on the scope, it will reload
30
+ # without our extra functionality, like inverse_of loading.
31
+ @scope.unload
32
+ to_a
33
+ end
34
+
35
+ protected
36
+
37
+ def method_missing(method, *args, &block)
38
+ # If we do anything that needs loading the scope, then we'll load it.
39
+ if Array.method_defined?(method)
40
+ to_a.send(method, *args, &block)
41
+ else
42
+ # Otherwise send all messages to the @scope.
43
+ @scope.send(method, *args, &block)
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+
50
+
@@ -0,0 +1,52 @@
1
+ module ActiveZuora
2
+ module LazyAttr
3
+
4
+ # This is meant to be included onto an Invoice class.
5
+ # Returns true/false on success.
6
+ # Result hash is stored in #result.
7
+ # If success, the id will be set in the object.
8
+ # If failure, errors will be present on object.
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ include Base
14
+ end
15
+
16
+
17
+ def fetch_field(field_name)
18
+ return nil unless self.id
19
+
20
+ records = fetch_field_records("select #{self.class.get_field!(field_name).zuora_name} from #{zuora_object_name} where Id = '#{self.id}'")
21
+ type_cast_fetched_field(field_name, records.nil? ? nil : records[field_name.to_sym])
22
+ end
23
+ private :fetch_field
24
+
25
+ def fetch_field_records(query_string)
26
+ response = self.class.connection.request(:query){ |soap| soap.body = { :query_string => query_string } }
27
+ response[:query_response][:result][:records]
28
+ end
29
+ private :fetch_field_records
30
+
31
+ def type_cast_fetched_field(field_name, value)
32
+ get_field!(field_name).type_cast(value)
33
+ end
34
+ private :type_cast_fetched_field
35
+
36
+ module ClassMethods
37
+ def lazy_load(*field_names)
38
+ Array(field_names).map(&:to_sym).each do |field_name|
39
+ define_lazy_field field_name
40
+ end
41
+ end
42
+
43
+ def define_lazy_field(field)
44
+ instance_eval do
45
+ define_method field do
46
+ instance_variable_get("@#{field}") || instance_variable_set("@#{field}", fetch_field(field))
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,172 @@
1
+ module ActiveZuora
2
+ module Persistence
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ MAX_BATCH_SIZE = 50
7
+
8
+ def new_record?
9
+ id.blank?
10
+ end
11
+
12
+ def save
13
+ new_record? ? create : update
14
+ end
15
+
16
+ def save!
17
+ raise "Could Not Save Zuora Object: #{errors.full_messages.join ', '}" unless save
18
+ end
19
+
20
+ def update_attributes(attributes)
21
+ self.attributes = attributes
22
+ save
23
+ end
24
+
25
+ def update_attributes!(attributes)
26
+ self.attributes = attributes
27
+ save!
28
+ end
29
+
30
+ def delete
31
+ self.class.delete(id) > 0
32
+ end
33
+
34
+ def reload
35
+ raise ArgumentError.new("You can't reload a new record") if new_record?
36
+ self.untracked_attributes = self.class.find(id).attributes
37
+ self
38
+ end
39
+
40
+ def xml_field_names
41
+ # If we're rendering an existing record, always include the id.
42
+ new_record? ? super : ([:id] + super).uniq
43
+ end
44
+
45
+ private
46
+
47
+ def create
48
+ return false unless new_record? && valid?
49
+ result = self.class.connection.request(:create) do |soap|
50
+ soap.body do |xml|
51
+ build_xml(xml, soap,
52
+ :namespace => soap.namespace,
53
+ :element_name => :zObjects,
54
+ :force_type => true)
55
+ end
56
+ end[:create_response][:result]
57
+ if result[:success]
58
+ self.id = result[:id]
59
+ clear_changed_attributes
60
+ true
61
+ else
62
+ errors.add(:base, result[:errors][:message]) if result[:errors]
63
+ false
64
+ end
65
+ end
66
+
67
+ def update
68
+ self.class.update(self)
69
+ self.errors.blank?
70
+ end
71
+
72
+ module ClassMethods
73
+
74
+ def create(attributes={})
75
+ new(attributes).tap(&:save)
76
+ end
77
+
78
+ def create!(attributes={})
79
+ new(attributes).tap(&:save!)
80
+ end
81
+
82
+ # Takes an array of zobjects and batch saves new and updated records separately
83
+ def save(*zobjects)
84
+ new_records = 0
85
+ updated_records = 0
86
+
87
+ # Get all new objects
88
+ new_objects = zobjects.flatten.select do |zobject|
89
+ zobject.new_record? && zobject.changed.present? && zobject.valid?
90
+ end
91
+
92
+ # Get all updated objects
93
+ updated_objects = zobjects.flatten.select do |zobject|
94
+ !zobject.new_record? && zobject.changed.present? && zobject.valid?
95
+ end
96
+
97
+ # Make calls in batches of 50
98
+ new_objects.each_slice(MAX_BATCH_SIZE) do |batch|
99
+ new_records += process_save(batch, :create)
100
+ end
101
+
102
+ updated_objects.each_slice(MAX_BATCH_SIZE) do |batch|
103
+ updated_records += process_save(batch, :update)
104
+ end
105
+
106
+ new_records + updated_records
107
+ end
108
+
109
+ # For backwards compatability
110
+ def update(*zobjects)
111
+ save(zobjects)
112
+ end
113
+
114
+ def process_save(zobjects, action)
115
+ unless [:create, :update].include? action
116
+ raise "Invalid action type for saving. Must be create or update."
117
+ end
118
+
119
+ return 0 if zobjects.empty?
120
+
121
+ results = connection.request(action) do |soap|
122
+ soap.body do |xml|
123
+ zobjects.map do |zobject|
124
+ zobject.build_xml(xml, soap,
125
+ :namespace => soap.namespace,
126
+ :element_name => :zObjects,
127
+ :force_type => true,
128
+ :nil_strategy => :fields_to_null)
129
+ end.last
130
+ end
131
+ end["#{action.to_s}_response".to_sym][:result]
132
+
133
+ results = [results] unless results.is_a?(Array)
134
+ zobjects.zip(results).each do |zobject, result|
135
+ if result[:success]
136
+ zobject.clear_changed_attributes
137
+ else
138
+ zobject.add_zuora_errors result[:errors]
139
+ end
140
+ end
141
+
142
+ # Return the count of updates that succeeded.
143
+ results.select{ |result| result[:success] }.size
144
+ end
145
+
146
+ def delete(*ids)
147
+ ids.flatten!
148
+ deleted_records = 0
149
+ ids.each_slice(MAX_BATCH_SIZE) do |batch|
150
+ deleted_records += process_delete(batch)
151
+ end
152
+ deleted_records
153
+ end
154
+
155
+ def process_delete(*ids)
156
+ ids.flatten!
157
+ results = connection.request(:delete) do |soap|
158
+ qualifier = soap.namespace_by_uri(soap.namespace)
159
+ soap.body do |xml|
160
+ xml.tag!(qualifier, :type, zuora_object_name)
161
+ ids.map { |id| xml.tag!(qualifier, :ids, id) }.last
162
+ end
163
+ end[:delete_response][:result]
164
+ results = [results] unless results.is_a?(Array)
165
+ # Return the count of deletes that succeeded.
166
+ results.select{ |result| result[:success] }.size
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,260 @@
1
+ module ActiveZuora
2
+ class Relation
3
+
4
+ attr_accessor :selected_field_names, :filters, :order_attribute, :order_direction
5
+
6
+ attr_reader :zobject_class
7
+
8
+ def initialize(zobject_class, selected_field_names=[:id])
9
+ @zobject_class, @selected_field_names, @filters = zobject_class, selected_field_names, []
10
+
11
+ if field?(:created_date)
12
+ @order_attribute, @order_direction = :created_date, :asc
13
+ end
14
+ end
15
+
16
+ def dup
17
+ dup = super
18
+ dup.selected_field_names = dup.selected_field_names.dup
19
+ dup.filters = dup.filters.dup
20
+ dup.unload
21
+ dup
22
+ end
23
+
24
+ #
25
+ # Conditions / Selecting
26
+ #
27
+
28
+ def select(*field_names)
29
+ dup.tap { |dup| dup.selected_field_names = field_names.flatten }
30
+ end
31
+
32
+ def where(conditions)
33
+ dup.tap { |dup| dup.filters << ['and', conditions] }
34
+ end
35
+
36
+ alias :and :where
37
+
38
+ def or(conditions)
39
+ dup.tap { |dup| dup.filters << ['or', conditions] }\
40
+ end
41
+
42
+ def order(attribute, direction = :asc)
43
+ dup.tap do |dup|
44
+ dup.order_attribute = attribute
45
+ dup.order_direction = direction
46
+ end
47
+ end
48
+
49
+ def scoped
50
+ # Account.select(:id).where(:status => "Draft") do
51
+ # Account.all # => select id from Account where status = "Draft"
52
+ # end
53
+ previous_scope, zobject_class.current_scope = zobject_class.current_scope, self
54
+ yield
55
+ ensure
56
+ zobject_class.current_scope = previous_scope
57
+ end
58
+
59
+ def merge(relation)
60
+ if relation.is_a?(Hash)
61
+ where(relation)
62
+ else
63
+ dup.tap do |dup|
64
+ dup.filters.concat relation.filters
65
+ dup.filters.uniq!
66
+ dup.order_attribute = relation.order_attribute
67
+ dup.order_direction = relation.order_direction
68
+ end
69
+ end
70
+ end
71
+
72
+ #
73
+ # Finding / Loading
74
+ #
75
+
76
+ def to_zql
77
+ select_statement + " from " + zobject_class.zuora_object_name + " " + where_statement
78
+ end
79
+
80
+ def find(id)
81
+ return nil if id.blank?
82
+ where(:id => id).first
83
+ end
84
+
85
+ def find_each(&block)
86
+ # Iterate through each item, but don't save the results in memory.
87
+ if loaded?
88
+ # If we're already loaded, iterate through the cached records.
89
+ to_a.each(&block)
90
+ else
91
+ query.each(&block)
92
+ end
93
+ end
94
+
95
+ def to_a
96
+ @records ||= query
97
+ end
98
+
99
+ alias :all :to_a
100
+
101
+ def loaded?
102
+ !@records.nil?
103
+ end
104
+
105
+ def unload
106
+ @records = nil
107
+ self
108
+ end
109
+
110
+ def reload
111
+ unload.to_a
112
+ self
113
+ end
114
+
115
+ def query(&block)
116
+ # Keep querying until all pages are retrieved.
117
+ # Throws an exception for an invalid query.
118
+ response = zobject_class.connection.request(:query){ |soap| soap.body = { :query_string => to_zql } }
119
+ query_response = response[:query_response]
120
+ records = objectify_query_results(query_response[:result][:records])
121
+ records.each(&:block) if block_given?
122
+ # If there are more pages of records, keep fetching
123
+ # them until done.
124
+ until query_response[:result][:done]
125
+ query_response = zobject_class.connection.request(:query_more) do |soap|
126
+ soap.body = { :query_locator => query_response[:result][:query_locator] }
127
+ end[:query_more_response]
128
+ more_records = objectify_query_results(query_response[:result][:records])
129
+ more_records.each(&:block) if block_given?
130
+ records.concat more_records
131
+ end
132
+ sort_records!(records)
133
+ rescue Savon::SOAP::Fault => exception
134
+ # Add the zql to the exception message and re-raise.
135
+ exception.message << ": #{to_zql}"
136
+ raise
137
+ end
138
+
139
+ #
140
+ # Updating / Deleting
141
+ #
142
+
143
+ def update_all(attributes={})
144
+ # Update using an attribute hash, or you can pass a block
145
+ # and update the attributes directly on the objects.
146
+ if block_given?
147
+ to_a.each { |record| yield record }
148
+ else
149
+ to_a.each { |record| record.attributes = attributes }
150
+ end
151
+ zobject_class.update(to_a)
152
+ end
153
+
154
+ def delete_all
155
+ zobject_class.delete(to_a.map(&:id))
156
+ end
157
+
158
+ protected
159
+
160
+ def method_missing(method, *args, &block)
161
+ # This is how the chaing can happen on class methods or named scopes on the
162
+ # ZObject class.
163
+ if Array.method_defined?(method)
164
+ to_a.send(method, *args, &block)
165
+ elsif zobject_class.respond_to?(method)
166
+ scoped { zobject_class.send(method, *args, &block) }
167
+ else
168
+ super
169
+ end
170
+ end
171
+
172
+ #
173
+ # Helper methods to build the ZQL.
174
+ #
175
+
176
+ def select_statement
177
+ "select " + selected_field_names.map { |field_name| zuora_field_name(field_name) }.join(', ')
178
+ end
179
+
180
+ def where_statement
181
+ return '' if @filters.empty?
182
+ tokens = []
183
+ @filters.each do |logical_operator, conditions|
184
+ if conditions.is_a?(Hash)
185
+ conditions.each do |field_name, comparisons|
186
+ zuora_field_name = zuora_field_name(field_name)
187
+ comparisons = { '=' => comparisons } unless comparisons.is_a?(Hash)
188
+ comparisons.each do |operator, value|
189
+ tokens.concat [logical_operator, zuora_field_name, operator, escape_filter_value(value)]
190
+ end
191
+ end
192
+ else
193
+ tokens.concat [logical_operator, conditions.to_s]
194
+ end
195
+ end
196
+ tokens[0] = "where"
197
+ tokens.join ' '
198
+ end
199
+
200
+ def zuora_field_name(name)
201
+ zobject_class.get_field!(name).zuora_name
202
+ end
203
+
204
+ def escape_filter_value(value)
205
+ if value.nil?
206
+ "null"
207
+ elsif value.is_a?(String)
208
+ "'#{value.gsub("'","\\\\'")}'"
209
+ elsif value.is_a?(DateTime) || value.is_a?(Time)
210
+ # If we already have a DateTime or Time, use the zone it already has.
211
+ escape_filter_value(value.strftime("%FT%T%:z")) # 2007-11-19T08:37:48-06:00
212
+ elsif value.is_a?(Date)
213
+ # Create a DateTime from the date using Zuora's timezone.
214
+ escape_filter_value(value.to_datetime.change(:offset => "+0800"))
215
+ else
216
+ value
217
+ end
218
+ end
219
+
220
+ def objectify_query_results(results)
221
+ return [] if results.blank?
222
+ # Sometimes Zuora will return only a single record, not in an array.
223
+ results = [results] unless results.is_a?(Array)
224
+ results.map do |attributes|
225
+ # Strip any noisy attributes from the results that have to do with
226
+ # SOAP namespaces.
227
+ attributes.delete_if { |key, value| key.to_s.start_with? "@" }
228
+ # Instantiate the zobject class, but don't track the changes.
229
+ if ActiveSupport.version.to_s.to_f >= 5.2
230
+ zobject_class.new(attributes).tap { |record| record.clear_changes_information }
231
+ else
232
+ zobject_class.new(attributes).tap { |record| record.changed_attributes.clear }
233
+ end
234
+ end
235
+ end
236
+
237
+ def sort_records!(records)
238
+ return records unless order_attribute.present?
239
+ records.sort! do |a, b|
240
+ if a.nil?
241
+ -1
242
+ elsif b.nil?
243
+ 1
244
+ else
245
+ a.send(order_attribute) <=> b.send(order_attribute)
246
+ end
247
+ end
248
+ records.reverse! if order_direction == :desc
249
+ records
250
+ end
251
+
252
+ end
253
+ end
254
+
255
+
256
+
257
+
258
+
259
+
260
+