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
@@ -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
+