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,28 @@
1
+ module ActiveZuora
2
+ class ArrayFieldDecorator < SimpleDelegator
3
+
4
+ # Wraps a Field object and typecasts/builds
5
+ # item as an array of the given field.
6
+
7
+ def type_cast(values)
8
+ # Force into an array and run type_cast on each element.
9
+ [values].flatten.compact.map { |value| __getobj__.type_cast(value) }
10
+ end
11
+
12
+ def build_xml(xml, soap, values, options={})
13
+ # It may be wierd that we're mapping and taking the last value,
14
+ # But there's an issue with xml builder where if a block
15
+ # returns an array, it will output the array in the XML.
16
+ # So instead, we'll have our block return the value
17
+ # of the last build_xml call.
18
+ [values].flatten.compact.map do |value|
19
+ __getobj__.build_xml(xml, soap, value, options)
20
+ end.last
21
+ end
22
+
23
+ def clear_changed_attributes(values)
24
+ [values].flatten.compact.map { |value| __getobj__.clear_changed_attributes(value) }
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveZuora
2
+ class BooleanField < Field
3
+
4
+ def type_cast(value)
5
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
6
+ return true if value.to_s.downcase == 'true'
7
+ return false if value.to_s.downcase == 'false'
8
+ default
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveZuora
2
+ class DateField < Field
3
+
4
+ def type_cast(value)
5
+ return value if value.nil?
6
+ return value.to_date if value.is_a?(Date)
7
+ return value.to_date if value.is_a?(DateTime)
8
+ value.to_date rescue default
9
+ end
10
+
11
+ def build_xml(xml, soap, value, options={})
12
+ value = value ? value.strftime("%Y-%m-%d") : ''
13
+ super(xml, soap, value, options)
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,19 @@
1
+ module ActiveZuora
2
+ class DateTimeField < Field
3
+
4
+ def type_cast(value)
5
+ return value if value.nil? || value.is_a?(Date)
6
+ return value.to_datetime if value.is_a?(Time)
7
+ value.to_datetime rescue default
8
+ end
9
+
10
+ def build_xml(xml, soap, value, options={})
11
+ # All dates need to be in PST time. Since all user-set attributes
12
+ # in Zuora are really only dates, we'll chop off the time.
13
+ # 2012-05-22T00:00:00-08:00
14
+ value = value ? value.strftime("%Y-%m-%dT00:00:00-08:00") : ''
15
+ super(xml, soap, value, options)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveZuora
2
+ class DecimalField < Field
3
+
4
+ def type_cast(value)
5
+ return value if value.nil? || value.is_a?(BigDecimal)
6
+ return default if value.blank?
7
+ return value.to_d if value.respond_to?(:to_d)
8
+ default
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,76 @@
1
+ module ActiveZuora
2
+ class Field
3
+
4
+ attr_accessor :name, :zuora_name, :default, :namespace
5
+
6
+ def initialize(name, namespace, options={})
7
+ @name = name.to_s
8
+ @namespace = namespace
9
+ @zuora_name = options[:zuora_name] || @name.camelize
10
+ @default = options[:default]
11
+ end
12
+
13
+ def type_cast(value)
14
+ value
15
+ end
16
+
17
+ def define_instance_methods(zuora_class)
18
+ # We store this into a variable so we can use it in our
19
+ # class eval below.
20
+ field_name = name
21
+ # Add dirty helpers
22
+ # The value will still be stored in an instance
23
+ # variable with the name of the field.
24
+ # But we'll define extra writer methods so that we
25
+ # can handle any input from savon.
26
+ # Savon just calls underscore on the element names,
27
+ # but our fields allow for any combination
28
+ # of field name and Zuora name.
29
+ # This is especially useful for custom fields which
30
+ # are named like CustomField__c. You might choose
31
+ # to make this field :custom_field, and therefore
32
+ # we'll need a writer for :custom_field, :custom_field_c,
33
+ # and I threw in a :CustomField__c just for good measure.
34
+ writers = [field_name, zuora_name, zuora_name.underscore].uniq
35
+ zuora_class.class_eval do
36
+ # Define the methods on an included module, so we can override
37
+ # them using super.
38
+ generated_attribute_methods.module_eval do
39
+ # Getter
40
+ attr_reader field_name
41
+ # Boolean check.
42
+ define_method "#{field_name}?" do
43
+ !!send(field_name)
44
+ end
45
+ # Writers
46
+ writers.each do |writer_name|
47
+ define_method "#{writer_name}=" do |value|
48
+ write_attribute(field_name, value)
49
+ end
50
+ end
51
+ end
52
+ # Dirty attribute helpers.
53
+ define_attribute_methods [field_name]
54
+ end
55
+ end
56
+
57
+ def build_xml(xml, soap, value, options={})
58
+ qualifier = soap.namespace_by_uri(namespace)
59
+ nil_strategy = options[:nil_strategy] || :omit
60
+ # The extra qualifiers attribute needs to be passed in
61
+ # in case the field is another complexType that needs
62
+ # to be namespaced.
63
+ if !value.nil? || nil_strategy == :whitespace
64
+ xml.tag!(qualifier, zuora_name.to_sym, value.to_s)
65
+ elsif nil_strategy == :fields_to_null
66
+ xml.tag!(qualifier, :fieldsToNull, zuora_name.to_sym)
67
+ end
68
+ end
69
+
70
+ def clear_changed_attributes(value)
71
+ # If the value of this field has attribute changes to clear,
72
+ # override this function.
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveZuora
2
+ class IntegerField < Field
3
+
4
+ def type_cast(value)
5
+ return value if value.nil? || value.is_a?(Integer)
6
+ return value.to_i if value.respond_to?(:to_i)
7
+ default
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module ActiveZuora
2
+ class ObjectField < Field
3
+
4
+ # A field that is itself another Zuora complex type.
5
+ # Hashes will automatically be converted to an instance of the given class.
6
+
7
+ attr_accessor :class_name
8
+
9
+ def initialize(name, namespace, class_name, options={})
10
+ @class_name = class_name
11
+ super(name, namespace, options)
12
+ end
13
+
14
+ def type_cast(value)
15
+ if value.is_a?(Hash)
16
+ value = class_name.constantize.new(value)
17
+ end
18
+ value
19
+ end
20
+
21
+ def build_xml(xml, soap, value, options={})
22
+ # For complex types, simply omit it if it's nil.
23
+ value.build_xml(xml, soap, :namespace => namespace, :element_name => zuora_name) if value
24
+ end
25
+
26
+ def clear_changed_attributes(value)
27
+ value.clear_changed_attributes if value
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveZuora
2
+ class StringField < Field
3
+
4
+ def type_cast(value)
5
+ return value if value.nil? || value.is_a?(String)
6
+ return value.to_s if value.respond_to?(:to_s)
7
+ default
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveZuora
2
+ module Generate
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
+ attr_accessor :result
15
+ end
16
+
17
+ def generate
18
+ self.result = self.class.connection.request(:generate) do |soap|
19
+ soap.body do |xml|
20
+ build_xml(xml, soap,
21
+ :namespace => soap.namespace,
22
+ :element_name => :zObjects,
23
+ :force_type => true)
24
+ end
25
+ end[:generate_response][:result]
26
+
27
+ if result[:success]
28
+ self.id = result[:id]
29
+ self.status = 'Draft'
30
+ clear_changed_attributes
31
+ true
32
+ else
33
+ add_zuora_errors(result[:errors])
34
+ false
35
+ end
36
+ end
37
+
38
+ def generate!
39
+ raise "Could not generate: #{errors.full_messages.join ', '}" unless generate
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,244 @@
1
+ module ActiveZuora
2
+ class Generator
3
+
4
+ attr_reader :document, :classes
5
+
6
+ def initialize(document, options={})
7
+ # document is a parsed wsdl document.
8
+ @document = document
9
+ @classes = []
10
+ @class_nesting = options[:inside] || ActiveZuora
11
+ @class_nesting.const_set("CollectionProxy", CollectionProxy) unless @class_nesting.constants.include?(:CollectionProxy)
12
+ end
13
+
14
+ def generate_classes
15
+
16
+ # Defines the classes based on the wsdl document.
17
+ # Assumes the following namespaces in the wsdl.
18
+ # xmlns="http://schemas.xmlsoap.org/wsdl/"
19
+ # xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
20
+ # xmlns:xs="http://www.w3.org/2001/XMLSchema"
21
+ # xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
22
+ # xmlns:zns="http://api.zuora.com/"
23
+ # xmlns:ons="http://object.api.zuora.com/"
24
+ # xmlns:fns="http://fault.api.zuora.com/"
25
+ @document.xpath('.//xs:schema[@targetNamespace]').each do |schema|
26
+ namespace = schema.attribute("targetNamespace").value
27
+
28
+ schema.xpath('.//xs:complexType[@name]').each do |complex_type|
29
+ class_name = complex_type.attribute("name").value
30
+ # Skip the zObject base class, we define our own.
31
+ next if class_name == "zObject"
32
+
33
+ class_name[0] = class_name[0].upcase
34
+ zuora_class = Class.new
35
+ @class_nesting.const_set(class_name, zuora_class)
36
+ @classes << zuora_class
37
+
38
+ # Include the Base module for adding fields.
39
+ zuora_class.send :include, Base
40
+ zuora_class.namespace = namespace
41
+
42
+ # If it's a zObject, include that module as well.
43
+ if complex_type.xpath(".//xs:extension[@base='ons:zObject']").any?
44
+ zuora_class.send :include, ZObject
45
+ end
46
+
47
+ # Define the fields
48
+ complex_type.xpath('.//xs:element[@name][@type]').each do |element|
49
+ # attributes: name, type, nillable, minoccurs, maxoccurs
50
+ zuora_name = element.attribute("name").value
51
+ field_name = zuora_name.underscore
52
+ field_type = element.attribute("type").value
53
+ is_array = element_is_an_array?(element)
54
+
55
+ case field_type
56
+ when "string", "xs:string", "zns:ID", "xs:base64Binary"
57
+ zuora_class.field field_name, :string,
58
+ :zuora_name => zuora_name, :array => is_array
59
+ when "boolean", "xs:boolean"
60
+ zuora_class.field field_name, :boolean,
61
+ :zuora_name => zuora_name, :array => is_array
62
+ when "int", "short", "long", "xs:int"
63
+ zuora_class.field field_name, :integer,
64
+ :zuora_name => zuora_name, :array => is_array
65
+ when "decimal"
66
+ zuora_class.field field_name, :decimal,
67
+ :zuora_name => zuora_name, :array => is_array
68
+ when "date"
69
+ zuora_class.field field_name, :date,
70
+ :zuora_name => zuora_name, :array => is_array
71
+ when "dateTime"
72
+ zuora_class.field field_name, :datetime,
73
+ :zuora_name => zuora_name, :array => is_array
74
+ when /\A(zns:|ons:)/
75
+ zuora_class.field field_name, :object,
76
+ :zuora_name => zuora_name, :array => is_array,
77
+ :class_name => zuora_class.nested_class_name(field_type.split(':').last)
78
+ else
79
+ puts "Unknown field type: #{field_type}"
80
+ end
81
+ end # each element
82
+
83
+ end # each complexType
84
+ end # each schema
85
+
86
+ add_obvious_associations
87
+ add_extra_customizations
88
+ end
89
+
90
+ private
91
+
92
+ def add_obvious_associations
93
+ # When a zuora class has a field called InvoiceId, it's a safe
94
+ # assuption that it references the an Invoice.
95
+ # Build those associations automatically.
96
+ @classes.each do |zuora_class|
97
+ zuora_class.fields.each do |field|
98
+ # If it looks like an Id field and the name
99
+ # matches a generated ZObject class
100
+ if match = field.zuora_name.match(/\A(.+?)Id\Z/)
101
+ if zobject_class = zobject_class_with_name(match[1])
102
+ # Add a belongs to relationship.
103
+ zuora_class.belongs_to zobject_class.zuora_object_name.underscore
104
+ # If the current class is also a ZObject, add a has_many
105
+ # to the referenced class.
106
+ if zuora_class < ZObject
107
+ zobject_class.has_many zuora_class.zuora_object_name.underscore.pluralize
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def add_extra_customizations
116
+ # We can't know everything from the wsdl, like which fields are
117
+ # usable in queries. This function does some final customizations
118
+ # based on the existing Zuora documentation.
119
+ # Also, it's possible classes customized here weren't defined
120
+ # in your wsdl, so it will only customize them if they are defined.
121
+
122
+ nesting = @class_nesting
123
+
124
+ customize 'Account' do
125
+ belongs_to :bill_to, :class_name => nested_class_name('Contact') if field? :bill_to_id
126
+ belongs_to :default_payment_method, :class_name => nested_class_name('PaymentMethod') if field? :default_payment_method_id
127
+ if field? :parent_id
128
+ belongs_to :parent, :class_name => nested_class_name('Account')
129
+ has_many :children, :class_name => nested_class_name('Account'), :foreign_key => :parent_id, :inverse_of => :parent
130
+ end
131
+ belongs_to :sold_to, :class_name => nested_class_name('Contact') if field? :sold_to_id
132
+ validates :currency, :presence => true if field? :currency
133
+ validates :name, :presence => true if field? :name
134
+ validates :status, :presence => true if field? :status
135
+ end
136
+
137
+ customize 'Amendment' do
138
+ exclude_from_queries :rate_plan_data,
139
+ :destination_account_id, :destination_invoice_owner_id
140
+ end
141
+
142
+ customize 'AmendRequest' do
143
+ include Amend
144
+ end
145
+
146
+ customize 'Import' do
147
+ exclude_from_queries :file_content
148
+ end
149
+
150
+ customize 'Invoice' do
151
+ include Generate
152
+ include LazyAttr
153
+ # The body field can only be accessed for a single invoice at a time, so
154
+ # exclude it here to not break collections of invoices. The contents of
155
+ # the body field will be lazy loaded in when needed.
156
+ exclude_from_queries :regenerate_invoice_pdf, :body, :bill_run_id
157
+ lazy_load :body
158
+ end
159
+
160
+ customize 'InvoiceItem' do
161
+ exclude_from_queries :product_rate_plan_charge_id
162
+ end
163
+
164
+ customize 'BillingPreviewRequest' do
165
+ include BillingPreview
166
+ end
167
+
168
+ customize 'InvoiceItemAdjustment' do
169
+ exclude_from_queries :customer_name, :customer_number
170
+ end
171
+
172
+ customize 'Payment' do
173
+ exclude_from_queries :applied_invoice_amount,
174
+ :gateway_option_data, :invoice_id, :invoice_number, :invoice_payment_data
175
+ end
176
+
177
+ customize 'PaymentMethod' do
178
+ exclude_from_queries :ach_account_number, :bank_transfer_account_number,
179
+ :credit_card_number, :credit_card_security_code, :gateway_option_data,
180
+ :second_token_id, :skip_validation, :token_id
181
+ end
182
+
183
+ customize 'ProductRatePlan' do
184
+ include LazyAttr
185
+ exclude_from_queries :active_currencies
186
+ lazy_load :active_currencies
187
+ end
188
+
189
+ customize 'ProductRatePlanCharge' do
190
+ exclude_from_queries :product_rate_plan_charge_tier_data, :revenue_recognition_rule_name, :deferred_revenue_account, :recognized_revenue_account, :product_discount_apply_detail_data
191
+ end
192
+
193
+ customize 'Usage' do
194
+ exclude_from_queries :ancestor_account_id, :invoice_id, :invoice_number
195
+ end
196
+
197
+ customize 'RatePlanCharge' do
198
+ include LazyAttr
199
+ exclude_from_queries :overage_price, :included_units,
200
+ :discount_amount, :discount_percentage, :rollover_balance, :price, :revenue_recognition_rule_name
201
+ lazy_load :price
202
+ end
203
+
204
+ customize 'Refund' do
205
+ exclude_from_queries :gateway_option_data, :payment_id
206
+ end
207
+
208
+ customize 'Subscription' do
209
+ exclude_from_queries :ancestor_account_id
210
+ belongs_to :creator_account, :class_name => nested_class_name('Account') if field? :creator_account_id
211
+ belongs_to :creator_invoice_owner, :class_name => nested_class_name('Account') if field? :creator_invoice_owner_id
212
+ belongs_to :invoice_owner, :class_name => nested_class_name('Account') if field? :invoice_owner_id
213
+ belongs_to :original, :class_name => name if field? :original_id
214
+ belongs_to :original_subscription, :class_name => name if field? :original_subscription_id
215
+ belongs_to :previous_subscription, :class_name => name if field? :previous_subscription_id
216
+ end
217
+
218
+ customize 'SubscribeRequest' do
219
+ include Subscribe
220
+ end
221
+ end
222
+
223
+ def customize(zuora_class_name, &block)
224
+ if @class_nesting.const_defined?(zuora_class_name)
225
+ @class_nesting.const_get(zuora_class_name).instance_eval(&block)
226
+ end
227
+ end
228
+
229
+
230
+ def element_is_an_array?(element)
231
+ attribute_is_more_than_one?(element.attribute("minOccurs")) ||
232
+ attribute_is_more_than_one?(element.attribute("maxOccurs"))
233
+ end
234
+
235
+ def attribute_is_more_than_one?(attribute)
236
+ attribute && ( attribute.value == "unbounded" || attribute.value.to_i > 1 )
237
+ end
238
+
239
+ def zobject_class_with_name(name)
240
+ @classes.find { |zuora_class| zuora_class.zuora_object_name == name && zuora_class < ZObject }
241
+ end
242
+
243
+ end
244
+ end