active_zuora 1.5.2 → 2.0.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.
- data/Gemfile +4 -0
- data/Gemfile.lock +57 -0
- data/README.md +155 -0
- data/Rakefile +6 -22
- data/TODO.md +2 -0
- data/active_zuora.gemspec +25 -68
- data/lib/active_zuora.rb +37 -6
- data/lib/active_zuora/amend.rb +43 -0
- data/lib/active_zuora/base.rb +73 -0
- data/lib/active_zuora/belongs_to_associations.rb +56 -0
- data/lib/active_zuora/connection.rb +41 -0
- data/lib/active_zuora/fields.rb +122 -0
- data/lib/active_zuora/fields/array_field_decorator.rb +28 -0
- data/lib/active_zuora/fields/boolean_field.rb +12 -0
- data/lib/active_zuora/fields/date_time_field.rb +19 -0
- data/lib/active_zuora/fields/decimal_field.rb +12 -0
- data/lib/active_zuora/fields/field.rb +76 -0
- data/lib/active_zuora/fields/integer_field.rb +11 -0
- data/lib/active_zuora/fields/object_field.rb +31 -0
- data/lib/active_zuora/fields/string_field.rb +11 -0
- data/lib/active_zuora/generate.rb +43 -0
- data/lib/active_zuora/generator.rb +220 -0
- data/lib/active_zuora/has_many_associations.rb +37 -0
- data/lib/active_zuora/has_many_proxy.rb +50 -0
- data/lib/active_zuora/persistence.rb +176 -0
- data/lib/active_zuora/relation.rb +253 -0
- data/lib/active_zuora/scoping.rb +50 -0
- data/lib/active_zuora/subscribe.rb +42 -0
- data/lib/active_zuora/version.rb +3 -0
- data/lib/active_zuora/z_object.rb +21 -0
- data/spec/belongs_to_associations_spec.rb +35 -0
- data/spec/has_many_integration_spec.rb +53 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/subscribe_integration_spec.rb +218 -0
- data/spec/zobject_integration_spec.rb +104 -0
- data/wsdl/zuora.wsdl +1548 -0
- metadata +128 -72
- checksums.yaml +0 -7
- data/LICENSE +0 -202
- data/README.rdoc +0 -36
- data/VERSION +0 -1
- data/custom_fields.yml +0 -17
- data/lib/active_zuora/account.rb +0 -31
- data/lib/active_zuora/amendment.rb +0 -7
- data/lib/active_zuora/bill_run.rb +0 -4
- data/lib/active_zuora/contact.rb +0 -7
- data/lib/active_zuora/invoice.rb +0 -46
- data/lib/active_zuora/invoice_item.rb +0 -26
- data/lib/active_zuora/invoice_item_adjustment.rb +0 -4
- data/lib/active_zuora/invoice_payment.rb +0 -11
- data/lib/active_zuora/payment.rb +0 -18
- data/lib/active_zuora/payment_method.rb +0 -10
- data/lib/active_zuora/product.rb +0 -4
- data/lib/active_zuora/product_rate_plan.rb +0 -9
- data/lib/active_zuora/product_rate_plan_charge.rb +0 -11
- data/lib/active_zuora/product_rate_plan_charge_tier.rb +0 -7
- data/lib/active_zuora/product_rate_plan_charge_tier_data.rb +0 -4
- data/lib/active_zuora/rate_plan.rb +0 -16
- data/lib/active_zuora/rate_plan_charge.rb +0 -71
- data/lib/active_zuora/rate_plan_charge_data.rb +0 -4
- data/lib/active_zuora/rate_plan_charge_tier.rb +0 -4
- data/lib/active_zuora/rate_plan_data.rb +0 -4
- data/lib/active_zuora/refund.rb +0 -4
- data/lib/active_zuora/subscribe_options.rb +0 -4
- data/lib/active_zuora/subscribe_request.rb +0 -13
- data/lib/active_zuora/subscribe_with_existing_account_request.rb +0 -4
- data/lib/active_zuora/subscription.rb +0 -17
- data/lib/active_zuora/subscription_data.rb +0 -4
- data/lib/active_zuora/usage.rb +0 -4
- data/lib/active_zuora/zobject.rb +0 -154
- data/lib/soap/property +0 -1
- data/lib/zuora/ZUORA.rb +0 -1560
- data/lib/zuora/ZUORADriver.rb +0 -145
- data/lib/zuora/ZUORAMappingRegistry.rb +0 -1709
- data/lib/zuora/ZuoraServiceClient.rb +0 -124
- data/lib/zuora/api.rb +0 -18
- data/lib/zuora_client.rb +0 -191
- data/lib/zuora_interface.rb +0 -215
@@ -0,0 +1,253 @@
|
|
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
|
+
@order_attribute, @order_direction = :created_date, :asc
|
11
|
+
end
|
12
|
+
|
13
|
+
def dup
|
14
|
+
dup = super
|
15
|
+
dup.selected_field_names = dup.selected_field_names.dup
|
16
|
+
dup.filters = dup.filters.dup
|
17
|
+
dup.unload
|
18
|
+
dup
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Conditions / Selecting
|
23
|
+
#
|
24
|
+
|
25
|
+
def select(*field_names)
|
26
|
+
dup.tap { |dup| dup.selected_field_names = field_names.flatten }
|
27
|
+
end
|
28
|
+
|
29
|
+
def where(conditions)
|
30
|
+
dup.tap { |dup| dup.filters << ['and', conditions] }
|
31
|
+
end
|
32
|
+
|
33
|
+
alias :and :where
|
34
|
+
|
35
|
+
def or(conditions)
|
36
|
+
dup.tap { |dup| dup.filters << ['or', conditions] }\
|
37
|
+
end
|
38
|
+
|
39
|
+
def order(attribute, direction = :asc)
|
40
|
+
dup.tap do |dup|
|
41
|
+
dup.order_attribute = attribute
|
42
|
+
dup.order_direction = direction
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def scoped
|
47
|
+
# Account.select(:id).where(:status => "Draft") do
|
48
|
+
# Account.all # => select id from Account where status = "Draft"
|
49
|
+
# end
|
50
|
+
previous_scope, zobject_class.current_scope = zobject_class.current_scope, self
|
51
|
+
yield
|
52
|
+
ensure
|
53
|
+
zobject_class.current_scope = previous_scope
|
54
|
+
end
|
55
|
+
|
56
|
+
def merge(relation)
|
57
|
+
if relation.is_a?(Hash)
|
58
|
+
where(relation)
|
59
|
+
else
|
60
|
+
dup.tap do |dup|
|
61
|
+
dup.filters.concat relation.filters
|
62
|
+
dup.filters.uniq!
|
63
|
+
dup.order_attribute = relation.order_attribute
|
64
|
+
dup.order_direction = relation.order_direction
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Finding / Loading
|
71
|
+
#
|
72
|
+
|
73
|
+
def to_zql
|
74
|
+
select_statement + " from " + zobject_class.zuora_object_name + " " + where_statement
|
75
|
+
end
|
76
|
+
|
77
|
+
def find(id)
|
78
|
+
return nil if id.blank?
|
79
|
+
where(:id => id).first
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_each(&block)
|
83
|
+
# Iterate through each item, but don't save the results in memory.
|
84
|
+
if loaded?
|
85
|
+
# If we're already loaded, iterate through the cached records.
|
86
|
+
to_a.each(&block)
|
87
|
+
else
|
88
|
+
query.each(&block)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_a
|
93
|
+
@records ||= query
|
94
|
+
end
|
95
|
+
|
96
|
+
alias :all :to_a
|
97
|
+
|
98
|
+
def loaded?
|
99
|
+
!@records.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
def unload
|
103
|
+
@records = nil
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def reload
|
108
|
+
unload.to_a
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
def query(&block)
|
113
|
+
# Keep querying until all pages are retrieved.
|
114
|
+
# Throws an exception for an invalid query.
|
115
|
+
response = zobject_class.connection.request(:query){ |soap| soap.body = { :query_string => to_zql } }
|
116
|
+
query_response = response[:query_response]
|
117
|
+
records = objectify_query_results(query_response[:result][:records])
|
118
|
+
records.each(&:block) if block_given?
|
119
|
+
# If there are more pages of records, keep fetching
|
120
|
+
# them until done.
|
121
|
+
until query_response[:result][:done]
|
122
|
+
query_response = zobject_class.connection.request(:query_more) do |soap|
|
123
|
+
soap.body = { :query_locator => response[:query_response][:result][:query_locator] }
|
124
|
+
end[:query_more_response]
|
125
|
+
more_records = objectify_query_results(query_response[:result][:records])
|
126
|
+
more_records.each(&:block) if block_given?
|
127
|
+
records.concat more_records
|
128
|
+
end
|
129
|
+
sort_records!(records)
|
130
|
+
rescue Savon::SOAP::Fault => exception
|
131
|
+
# Add the zql to the exception message and re-raise.
|
132
|
+
exception.message << ": #{to_zql}"
|
133
|
+
raise
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# Updating / Deleting
|
138
|
+
#
|
139
|
+
|
140
|
+
def update_all(attributes={})
|
141
|
+
# Update using an attribute hash, or you can pass a block
|
142
|
+
# and update the attributes directly on the objects.
|
143
|
+
if block_given?
|
144
|
+
to_a.each { |record| yield record }
|
145
|
+
else
|
146
|
+
to_a.each { |record| record.attributes = attributes }
|
147
|
+
end
|
148
|
+
zobject_class.update(to_a)
|
149
|
+
end
|
150
|
+
|
151
|
+
def delete_all
|
152
|
+
zobject_class.delete(to_a.map(&:id))
|
153
|
+
end
|
154
|
+
|
155
|
+
protected
|
156
|
+
|
157
|
+
def method_missing(method, *args, &block)
|
158
|
+
# This is how the chaing can happen on class methods or named scopes on the
|
159
|
+
# ZObject class.
|
160
|
+
if Array.method_defined?(method)
|
161
|
+
to_a.send(method, *args, &block)
|
162
|
+
elsif zobject_class.respond_to?(method)
|
163
|
+
scoped { zobject_class.send(method, *args, &block) }
|
164
|
+
else
|
165
|
+
super
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Helper methods to build the ZQL.
|
171
|
+
#
|
172
|
+
|
173
|
+
def select_statement
|
174
|
+
"select " + selected_field_names.map { |field_name| zuora_field_name(field_name) }.join(', ')
|
175
|
+
end
|
176
|
+
|
177
|
+
def where_statement
|
178
|
+
return '' if @filters.empty?
|
179
|
+
tokens = []
|
180
|
+
@filters.each do |logical_operator, conditions|
|
181
|
+
if conditions.is_a?(Hash)
|
182
|
+
conditions.each do |field_name, comparisons|
|
183
|
+
zuora_field_name = zuora_field_name(field_name)
|
184
|
+
comparisons = { '=' => comparisons } unless comparisons.is_a?(Hash)
|
185
|
+
comparisons.each do |operator, value|
|
186
|
+
tokens.concat [logical_operator, zuora_field_name, operator, escape_filter_value(value)]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
else
|
190
|
+
tokens.concat [logical_operator, conditions.to_s]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
tokens[0] = "where"
|
194
|
+
tokens.join ' '
|
195
|
+
end
|
196
|
+
|
197
|
+
def zuora_field_name(name)
|
198
|
+
zobject_class.get_field!(name).zuora_name
|
199
|
+
end
|
200
|
+
|
201
|
+
def escape_filter_value(value)
|
202
|
+
if value.nil?
|
203
|
+
"null"
|
204
|
+
elsif value.is_a?(String)
|
205
|
+
"'#{value.gsub("'","\\\\'")}'"
|
206
|
+
elsif value.is_a?(DateTime) || value.is_a?(Time)
|
207
|
+
# If we already have a DateTime or Time, use the zone it already has.
|
208
|
+
escape_filter_value(value.strftime("%FT%T%:z")) # 2007-11-19T08:37:48-06:00
|
209
|
+
elsif value.is_a?(Date)
|
210
|
+
# Create a DateTime from the date using Zuora's timezone.
|
211
|
+
escape_filter_value(value.to_datetime.change(:offset => "+0800"))
|
212
|
+
else
|
213
|
+
value
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def objectify_query_results(results)
|
218
|
+
return [] if results.blank?
|
219
|
+
# Sometimes Zuora will return only a single record, not in an array.
|
220
|
+
results = [results] unless results.is_a?(Array)
|
221
|
+
results.map do |attributes|
|
222
|
+
# Strip any noisy attributes from the results that have to do with
|
223
|
+
# SOAP namespaces.
|
224
|
+
attributes.delete_if { |key, value| key.to_s.start_with? "@" }
|
225
|
+
# Instantiate the zobject class, but don't track the changes.
|
226
|
+
zobject_class.new(attributes).tap { |record| record.clear_changed_attributes }
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def sort_records!(records)
|
231
|
+
return records unless order_attribute.present?
|
232
|
+
records.sort! do |a, b|
|
233
|
+
if a.nil?
|
234
|
+
-1
|
235
|
+
elsif b.nil?
|
236
|
+
1
|
237
|
+
else
|
238
|
+
a.send(order_attribute) <=> b.send(order_attribute)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
records.reverse! if order_direction == :desc
|
242
|
+
records
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
|
252
|
+
|
253
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module Scoping
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# Delegate to :scoped
|
10
|
+
delegate :find, :all, :find_each, :to => :scoped
|
11
|
+
delegate :select, :where, :and, :or, :order, :to => :scoped
|
12
|
+
delegate :first, :last, :each, :map, :any?, :empty?, :blank?, :present?, :size, :count, :to => :scoped
|
13
|
+
|
14
|
+
# Keep track of a current scope.
|
15
|
+
attr_accessor :current_scope
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
|
22
|
+
def scoped
|
23
|
+
current_scope || relation
|
24
|
+
end
|
25
|
+
|
26
|
+
def unscoped
|
27
|
+
block_given? ? relation.scoped { yield } : relation
|
28
|
+
end
|
29
|
+
|
30
|
+
def exclude_from_queries(*field_names)
|
31
|
+
(@excluded_from_queries ||= []).concat field_names.map(&:to_sym)
|
32
|
+
end
|
33
|
+
|
34
|
+
def relation
|
35
|
+
query_field_names = field_names - (@excluded_from_queries ||= [])
|
36
|
+
Relation.new(self, query_field_names)
|
37
|
+
end
|
38
|
+
|
39
|
+
def scope(name, body)
|
40
|
+
# Body can be a Relation or a lambda that returns a relation.
|
41
|
+
define_singleton_method(name) do |*args|
|
42
|
+
body.respond_to?(:call) ? body.call(*args) : scoped.merge(body)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module Subscribe
|
3
|
+
|
4
|
+
# This is meant to be included onto a SubscribeRequest class.
|
5
|
+
# Returns true/false on success.
|
6
|
+
# Result hash is stored in #result.
|
7
|
+
# If success, the subscription id and account id will be set in those objects.
|
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 subscribe
|
18
|
+
self.result = self.class.connection.request(:subscribe) do |soap|
|
19
|
+
soap.body do |xml|
|
20
|
+
build_xml(xml, soap,
|
21
|
+
:namespace => soap.namespace,
|
22
|
+
:element_name => :subscribes,
|
23
|
+
:force_type => true)
|
24
|
+
end
|
25
|
+
end[:subscribe_response][:result]
|
26
|
+
if result[:success]
|
27
|
+
account.id = result[:account_id]
|
28
|
+
subscription_data.subscription.id = result[:subscription_id]
|
29
|
+
clear_changed_attributes
|
30
|
+
true
|
31
|
+
else
|
32
|
+
add_zuora_errors(result[:errors])
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def subscribe!
|
38
|
+
raise "Could not subscribe: #{errors.full_messages.join ', '}" unless subscribe
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module ZObject
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Base
|
8
|
+
include Scoping
|
9
|
+
include HasManyAssociations
|
10
|
+
include Persistence
|
11
|
+
field :id, :string
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(another_zobject)
|
15
|
+
another_zobject.is_a?(ZObject) &&
|
16
|
+
zuora_object_name == another_zobject.zuora_object_name &&
|
17
|
+
id == another_zobject.id
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "BelongsToAssociations" do
|
4
|
+
|
5
|
+
class Blog
|
6
|
+
include ActiveZuora::ZObject
|
7
|
+
end
|
8
|
+
|
9
|
+
class Comment
|
10
|
+
include ActiveZuora::Base
|
11
|
+
belongs_to :blog
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should define a attribute assignment method method for the object" do
|
15
|
+
blog = Blog.new :id => "blog1"
|
16
|
+
comment = Comment.new :blog => blog
|
17
|
+
comment.blog_loaded?.should be_true
|
18
|
+
comment.blog.should == blog
|
19
|
+
comment.blog_id.should == blog.id
|
20
|
+
comment.blog = nil
|
21
|
+
comment.blog_loaded?.should be_true
|
22
|
+
comment.blog.should be_nil
|
23
|
+
comment.blog_id.should be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should define a attribute assignment method for the object id" do
|
27
|
+
blog = Blog.new :id => "blog1"
|
28
|
+
comment = Comment.new :blog => blog
|
29
|
+
comment.blog_loaded?.should be_true
|
30
|
+
comment.blog_id = "blog2"
|
31
|
+
comment.blog_loaded?.should be_false
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "HasManyRelations" do
|
4
|
+
|
5
|
+
integration_test do
|
6
|
+
|
7
|
+
before :all do
|
8
|
+
@account = Z::Account.create!(
|
9
|
+
:name => "ZObject Integration Test Account",
|
10
|
+
:status => "Draft",
|
11
|
+
:currency => "USD",
|
12
|
+
:bill_cycle_day => 1)
|
13
|
+
@billy = Z::Contact.create!(
|
14
|
+
:account => @account,
|
15
|
+
:first_name => "Billy",
|
16
|
+
:last_name => "Blanks")
|
17
|
+
@franky = Z::Contact.create!(
|
18
|
+
:account => @account,
|
19
|
+
:first_name => "Franky",
|
20
|
+
:last_name => "Funhouse")
|
21
|
+
end
|
22
|
+
|
23
|
+
after :all do
|
24
|
+
# Delete the account to cleanup in case a test failed.
|
25
|
+
@account.delete if @account
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can specify conditions and order" do
|
29
|
+
Z::Account.instance_eval do
|
30
|
+
has_many :billies, :conditions => { :first_name => "Billy" }, :order => [:first_name, :desc], :class_name => 'Z::Contact'
|
31
|
+
end
|
32
|
+
@account.billies.to_a == [@billy]
|
33
|
+
@account.billies.scope.order_attribute.should == :first_name
|
34
|
+
@account.billies.scope.order_direction.should == :desc
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can behave like an array" do
|
38
|
+
@account.contacts.size.should == 2
|
39
|
+
@account.contacts.map(&:first_name).should =~ %w{Billy Franky}
|
40
|
+
end
|
41
|
+
|
42
|
+
it "can respond to functions on the Relation" do
|
43
|
+
@account.contacts.unload
|
44
|
+
@account.contacts.loaded?.should be_false
|
45
|
+
@account.contacts.reload
|
46
|
+
@account.contacts.loaded?.should be_true
|
47
|
+
@account.contacts.where(:last_name => "Funhouse").to_a.should == [@franky]
|
48
|
+
@account.contacts.loaded?.should be_true
|
49
|
+
@account.contacts.to_a.should =~ [@billy, @franky]
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|