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