iron_bank 0.1.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.env.example +7 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/.gitignore +6 -1
- data/.reek +95 -0
- data/.rspec +1 -2
- data/.rubocop.yml +55 -0
- data/.travis.yml +22 -3
- data/Gemfile +3 -3
- data/LICENSE +176 -0
- data/README.md +133 -0
- data/Rakefile +39 -3
- data/bin/console +13 -1
- data/bin/setup +1 -0
- data/iron_bank.gemspec +34 -10
- data/lib/generators/iron_bank/install/install_generator.rb +20 -0
- data/lib/generators/iron_bank/install/templates/README +16 -0
- data/lib/generators/iron_bank/install/templates/iron_bank.rb +29 -0
- data/lib/iron_bank/action.rb +39 -0
- data/lib/iron_bank/actions/amend.rb +16 -0
- data/lib/iron_bank/actions/create.rb +19 -0
- data/lib/iron_bank/actions/delete.rb +19 -0
- data/lib/iron_bank/actions/execute.rb +21 -0
- data/lib/iron_bank/actions/generate.rb +19 -0
- data/lib/iron_bank/actions/query.rb +16 -0
- data/lib/iron_bank/actions/query_more.rb +21 -0
- data/lib/iron_bank/actions/subscribe.rb +32 -0
- data/lib/iron_bank/actions/update.rb +19 -0
- data/lib/iron_bank/associations.rb +73 -0
- data/lib/iron_bank/authentication.rb +37 -0
- data/lib/iron_bank/authentications/cookie.rb +80 -0
- data/lib/iron_bank/authentications/token.rb +82 -0
- data/lib/iron_bank/cacheable.rb +52 -0
- data/lib/iron_bank/client.rb +96 -0
- data/lib/iron_bank/collection.rb +31 -0
- data/lib/iron_bank/configuration.rb +86 -0
- data/lib/iron_bank/csv.rb +29 -0
- data/lib/iron_bank/describe/field.rb +65 -0
- data/lib/iron_bank/describe/object.rb +81 -0
- data/lib/iron_bank/describe/related.rb +40 -0
- data/lib/iron_bank/describe/tenant.rb +50 -0
- data/lib/iron_bank/endpoint.rb +36 -0
- data/lib/iron_bank/error.rb +45 -0
- data/lib/iron_bank/instrumentation.rb +14 -0
- data/lib/iron_bank/local.rb +72 -0
- data/lib/iron_bank/local_records.rb +52 -0
- data/lib/iron_bank/logger.rb +23 -0
- data/lib/iron_bank/metadata.rb +36 -0
- data/lib/iron_bank/object.rb +89 -0
- data/lib/iron_bank/open_tracing.rb +17 -0
- data/lib/iron_bank/operation.rb +33 -0
- data/lib/iron_bank/operations/billing_preview.rb +16 -0
- data/lib/iron_bank/query_builder.rb +72 -0
- data/lib/iron_bank/queryable.rb +55 -0
- data/lib/iron_bank/resource.rb +70 -0
- data/lib/iron_bank/resources/account.rb +62 -0
- data/lib/iron_bank/resources/amendment.rb +13 -0
- data/lib/iron_bank/resources/catalog_tiers/discount_amount.rb +26 -0
- data/lib/iron_bank/resources/catalog_tiers/discount_percentage.rb +26 -0
- data/lib/iron_bank/resources/catalog_tiers/price.rb +26 -0
- data/lib/iron_bank/resources/contact.rb +13 -0
- data/lib/iron_bank/resources/export.rb +11 -0
- data/lib/iron_bank/resources/import.rb +12 -0
- data/lib/iron_bank/resources/invoice.rb +37 -0
- data/lib/iron_bank/resources/invoice_adjustment.rb +13 -0
- data/lib/iron_bank/resources/invoice_item.rb +25 -0
- data/lib/iron_bank/resources/invoice_payment.rb +13 -0
- data/lib/iron_bank/resources/payment.rb +17 -0
- data/lib/iron_bank/resources/payment_method.rb +14 -0
- data/lib/iron_bank/resources/product.rb +14 -0
- data/lib/iron_bank/resources/product_rate_plan.rb +32 -0
- data/lib/iron_bank/resources/product_rate_plan_charge.rb +27 -0
- data/lib/iron_bank/resources/product_rate_plan_charge_tier.rb +60 -0
- data/lib/iron_bank/resources/rate_plan.rb +21 -0
- data/lib/iron_bank/resources/rate_plan_charge.rb +30 -0
- data/lib/iron_bank/resources/rate_plan_charge_tier.rb +26 -0
- data/lib/iron_bank/resources/subscription.rb +28 -0
- data/lib/iron_bank/resources/usage.rb +16 -0
- data/lib/iron_bank/response/raise_error.rb +16 -0
- data/lib/iron_bank/schema.rb +58 -0
- data/lib/iron_bank/utils.rb +59 -0
- data/lib/iron_bank/version.rb +4 -1
- data/lib/iron_bank.rb +152 -2
- metadata +300 -12
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# Handle the credentials to Zuora and establish a connection when making an
|
5
|
+
# authenticated request, reusing the same session cookie for future requests.
|
6
|
+
#
|
7
|
+
class Client
|
8
|
+
include IronBank::Instrumentation
|
9
|
+
include IronBank::OpenTracing
|
10
|
+
|
11
|
+
# Generic client error.
|
12
|
+
#
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
# Thrown when the base_url cannot be found for the given domain.
|
16
|
+
#
|
17
|
+
class InvalidHostname < Error; end
|
18
|
+
|
19
|
+
# Alias each actions as a `Client` instance method
|
20
|
+
IronBank::Actions.constants.each do |action|
|
21
|
+
method_name = IronBank::Utils.underscore(action)
|
22
|
+
klass = IronBank::Actions.const_get(action)
|
23
|
+
|
24
|
+
define_method :"#{method_name}" do |args|
|
25
|
+
klass.call(args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(domain:, client_id:, client_secret:, auth_type: 'token')
|
30
|
+
@domain = domain
|
31
|
+
@client_id = client_id
|
32
|
+
@client_secret = client_secret
|
33
|
+
@auth_type = auth_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
%(#<IronBank::Client:0x#{(object_id << 1).to_s(16)} domain="#{domain}">)
|
38
|
+
end
|
39
|
+
|
40
|
+
def connection
|
41
|
+
validate_domain
|
42
|
+
reset_connection if auth.expired?
|
43
|
+
|
44
|
+
@connection ||= Faraday.new(faraday_config) do |conn|
|
45
|
+
conn.use :ddtrace, open_tracing_options if open_tracing_enabled?
|
46
|
+
conn.use instrumenter, instrumenter_options if instrumenter
|
47
|
+
conn.use IronBank::Response::RaiseError
|
48
|
+
conn.request :json
|
49
|
+
conn.response :logger, IronBank.logger
|
50
|
+
conn.response :json, content_type: /\bjson$/
|
51
|
+
conn.adapter Faraday.default_adapter
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def describe(object_name)
|
56
|
+
IronBank::Describe::Object.from_connection(connection, object_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :domain, :client_id, :client_secret, :auth_type
|
62
|
+
|
63
|
+
def auth
|
64
|
+
@auth ||= IronBank::Authentication.new(
|
65
|
+
client_id: client_id,
|
66
|
+
client_secret: client_secret,
|
67
|
+
base_url: base_url,
|
68
|
+
auth_type: auth_type
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def base_url
|
73
|
+
@base_url ||= IronBank::Endpoint.base_url(domain)
|
74
|
+
end
|
75
|
+
|
76
|
+
def faraday_config
|
77
|
+
{
|
78
|
+
url: base_url,
|
79
|
+
headers: headers
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def headers
|
84
|
+
{ 'Content-Type' => 'application/json' }.merge(auth.header)
|
85
|
+
end
|
86
|
+
|
87
|
+
def reset_connection
|
88
|
+
@connection = nil
|
89
|
+
auth.renew_session
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_domain
|
93
|
+
raise InvalidHostname, domain.to_s unless base_url
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# Collection class which allows records reloadable from the source
|
5
|
+
class Collection
|
6
|
+
include Enumerable
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@records,
|
10
|
+
:[],
|
11
|
+
:each,
|
12
|
+
:length,
|
13
|
+
:size
|
14
|
+
|
15
|
+
def initialize(klass, conditions, records)
|
16
|
+
@klass = klass
|
17
|
+
@conditions = conditions
|
18
|
+
@records = records
|
19
|
+
end
|
20
|
+
|
21
|
+
# Update records from source
|
22
|
+
def reload
|
23
|
+
@records = @klass.where(@conditions)
|
24
|
+
end
|
25
|
+
|
26
|
+
# In case you need to access all array methods
|
27
|
+
def to_a
|
28
|
+
@records
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# The Zuora configuration class.
|
5
|
+
#
|
6
|
+
class Configuration
|
7
|
+
# Instrumentation
|
8
|
+
attr_accessor :instrumenter
|
9
|
+
attr_accessor :instrumenter_options
|
10
|
+
|
11
|
+
# Logger
|
12
|
+
attr_accessor :logger
|
13
|
+
|
14
|
+
# The Zuora domain for our tenant (apisandbox, production, etc.).
|
15
|
+
attr_accessor :domain
|
16
|
+
|
17
|
+
# OAuth client ID associated with our platform admin user.
|
18
|
+
attr_accessor :client_id
|
19
|
+
|
20
|
+
# OAuth client secret.
|
21
|
+
attr_accessor :client_secret
|
22
|
+
|
23
|
+
# Auth type (cookie|token)
|
24
|
+
attr_accessor :auth_type
|
25
|
+
|
26
|
+
# Cache store instance, optionally used by certain resources.
|
27
|
+
attr_accessor :cache
|
28
|
+
|
29
|
+
# Open Tracing
|
30
|
+
attr_accessor :open_tracing_enabled
|
31
|
+
|
32
|
+
# Open Tracing service name
|
33
|
+
attr_accessor :open_tracing_service_name
|
34
|
+
|
35
|
+
# Directory where the XML describe files are located.
|
36
|
+
attr_reader :schema_directory
|
37
|
+
|
38
|
+
# Directory where the local records are exported.
|
39
|
+
attr_reader :export_directory
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@schema_directory = './config/schema'
|
43
|
+
@export_directory = './config/export'
|
44
|
+
@logger = IronBank::Logger.new
|
45
|
+
@auth_type = 'token'
|
46
|
+
@open_tracing_enabled = false
|
47
|
+
@open_tracing_service_name = 'ironbank'
|
48
|
+
end
|
49
|
+
|
50
|
+
def schema_directory=(value)
|
51
|
+
@schema_directory = value
|
52
|
+
|
53
|
+
return unless defined? IronBank::Schema
|
54
|
+
|
55
|
+
IronBank::Schema.reset
|
56
|
+
|
57
|
+
# Call `with_schema` on each resource to redefine accessors
|
58
|
+
IronBank::Resources.constants.each do |resource|
|
59
|
+
klass = IronBank::Resources.const_get(resource)
|
60
|
+
klass.with_schema if klass.is_a?(Class)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def export_directory=(value)
|
65
|
+
@export_directory = value
|
66
|
+
return unless defined? IronBank::Product
|
67
|
+
|
68
|
+
IronBank::LocalRecords::RESOURCES.each do |resource|
|
69
|
+
IronBank::Resources.const_get(resource).reset_store
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def credentials
|
74
|
+
{
|
75
|
+
domain: domain,
|
76
|
+
client_id: client_id,
|
77
|
+
client_secret: client_secret,
|
78
|
+
auth_type: auth_type
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def credentials?
|
83
|
+
credentials.values.all?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# A custom CSV converter
|
5
|
+
#
|
6
|
+
class CSV < ::CSV
|
7
|
+
CSV::Converters[:decimal_integer] = lambda { |field|
|
8
|
+
begin
|
9
|
+
encoding = field.encode(CSV::ConverterEncoding)
|
10
|
+
|
11
|
+
# Match: [1, 10, 100], No match: [0.1, .1, 1., 0b10]
|
12
|
+
encoding =~ /^[+-]?\d+$/ ? encoding.to_i : field
|
13
|
+
rescue # encoding or integer conversion
|
14
|
+
field
|
15
|
+
end
|
16
|
+
}
|
17
|
+
|
18
|
+
CSV::Converters[:decimal_float] = lambda { |field|
|
19
|
+
begin
|
20
|
+
encoding = field.encode(CSV::ConverterEncoding)
|
21
|
+
|
22
|
+
# Match: [1.0, 1., 0.1, .1], No match: [1, 0b10]
|
23
|
+
encoding =~ /^[+-]?(?:\d*\.|\.\d*)\d*$/ ? encoding.to_f : field
|
24
|
+
rescue # encoding or float conversion
|
25
|
+
field
|
26
|
+
end
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
module Describe
|
5
|
+
# Describe a field in Zuora: name, label, type, etc.
|
6
|
+
#
|
7
|
+
class Field
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
TEXT_VALUES = %i[
|
11
|
+
name
|
12
|
+
label
|
13
|
+
type
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
PLURAL_VALUES = %i[
|
17
|
+
options
|
18
|
+
contexts
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
BOOLEAN_VALUES = %i[
|
22
|
+
selectable
|
23
|
+
createable
|
24
|
+
updateable
|
25
|
+
filterable
|
26
|
+
custom
|
27
|
+
required
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
def self.from_xml(doc)
|
31
|
+
new(doc)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Defined separately because the node name is not ruby-friendly
|
35
|
+
def max_length
|
36
|
+
doc.at_xpath('.//maxlength').text.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
TEXT_VALUES.each do |val|
|
40
|
+
define_method(val) { doc.at_xpath(".//#{val}").text }
|
41
|
+
end
|
42
|
+
|
43
|
+
PLURAL_VALUES.each do |val|
|
44
|
+
singular = val.to_s.chop
|
45
|
+
define_method(val) { doc.xpath(".//#{val}/#{singular}").map(&:text) }
|
46
|
+
end
|
47
|
+
|
48
|
+
BOOLEAN_VALUES.each do |val|
|
49
|
+
define_method(:"#{val}?") { doc.at_xpath(".//#{val}").text == 'true' }
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect
|
53
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} #{name} (#{type})>"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :doc
|
59
|
+
|
60
|
+
def initialize(doc)
|
61
|
+
@doc = doc
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
module Describe
|
5
|
+
# Describe an object in Zuora: name, label, fields, etc.
|
6
|
+
#
|
7
|
+
class Object
|
8
|
+
# Raised when the XML does not provide the expected node (see #name)
|
9
|
+
#
|
10
|
+
class InvalidXML < StandardError; end
|
11
|
+
|
12
|
+
private_class_method :new
|
13
|
+
|
14
|
+
def self.from_xml(doc)
|
15
|
+
new(doc)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_connection(connection, name)
|
19
|
+
xml = connection.get("v1/describe/#{name}").body
|
20
|
+
new(Nokogiri::XML(xml))
|
21
|
+
rescue TypeError
|
22
|
+
# NOTE: Zuora returns HTTP 401 (unauthorized) roughly 1 out of 3 times
|
23
|
+
# we make this call. Since this is a setup-only call and not a
|
24
|
+
# runtime one, we deemed it acceptable to keep retrying until it
|
25
|
+
# works.
|
26
|
+
retry
|
27
|
+
rescue IronBank::InternalServerError
|
28
|
+
# TODO: Need to properly store which object failed to be described by
|
29
|
+
# Zuora API and send a report to the console.
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def export
|
34
|
+
File.open(file_path, 'w') { |file| file << doc.to_xml }
|
35
|
+
end
|
36
|
+
|
37
|
+
def name
|
38
|
+
node = doc.at_xpath('.//object/name')
|
39
|
+
raise InvalidXML unless node
|
40
|
+
|
41
|
+
node.text
|
42
|
+
end
|
43
|
+
|
44
|
+
def label
|
45
|
+
doc.at_xpath('.//object/label').text
|
46
|
+
end
|
47
|
+
|
48
|
+
def fields
|
49
|
+
@fields ||= doc.xpath('.//fields/field').map do |node|
|
50
|
+
IronBank::Describe::Field.from_xml(node)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def query_fields
|
55
|
+
@query_fields ||= fields.select(&:selectable?).map(&:name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def related
|
59
|
+
@related ||= doc.xpath('.//related-objects/object').map do |node|
|
60
|
+
IronBank::Describe::Related.from_xml(node)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} #{name}>"
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :doc
|
71
|
+
|
72
|
+
def initialize(doc)
|
73
|
+
@doc = doc
|
74
|
+
end
|
75
|
+
|
76
|
+
def file_path
|
77
|
+
File.expand_path "#{name}.xml", IronBank.configuration.schema_directory
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
module Describe
|
5
|
+
# Describe a related object in Zuora, e.g., an account has a default payment
|
6
|
+
# method
|
7
|
+
#
|
8
|
+
class Related
|
9
|
+
private_class_method :new
|
10
|
+
|
11
|
+
def self.from_xml(doc)
|
12
|
+
new(doc)
|
13
|
+
end
|
14
|
+
|
15
|
+
def type
|
16
|
+
@type ||= doc.attributes['href'].value.split('/').last
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
doc.at_xpath('.//name').text
|
21
|
+
end
|
22
|
+
|
23
|
+
def label
|
24
|
+
doc.at_xpath('.//label').text
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} #{name} (#{type})>"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :doc
|
34
|
+
|
35
|
+
def initialize(doc)
|
36
|
+
@doc = doc
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
module Describe
|
5
|
+
# Describe a Zuora tenant, including its objects.
|
6
|
+
#
|
7
|
+
class Tenant
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
def self.from_xml(doc)
|
11
|
+
new(doc)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from_connection(connection)
|
15
|
+
xml = connection.get('v1/describe').body
|
16
|
+
new(Nokogiri::XML(xml), connection)
|
17
|
+
rescue TypeError
|
18
|
+
# NOTE: Zuora returns HTTP 401 (unauthorized) roughly 1 out of 3 times
|
19
|
+
# we make this call. Since this is a setup-only call and not a runtime
|
20
|
+
# one, we deemed it acceptable to keep retrying until it works.
|
21
|
+
retry
|
22
|
+
end
|
23
|
+
|
24
|
+
def objects
|
25
|
+
return object_names unless connection
|
26
|
+
|
27
|
+
@objects ||= object_names.map do |name|
|
28
|
+
IronBank::Describe::Object.from_connection(connection, name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)}>"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :doc, :connection
|
39
|
+
|
40
|
+
def initialize(doc, connection = nil)
|
41
|
+
@doc = doc
|
42
|
+
@connection = connection
|
43
|
+
end
|
44
|
+
|
45
|
+
def object_names
|
46
|
+
@object_names ||= doc.xpath('.//object/name').map(&:text)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# Identify and return the proper base URL for a given Zuora domain.
|
5
|
+
#
|
6
|
+
class Endpoint
|
7
|
+
private_class_method :new
|
8
|
+
|
9
|
+
PRODUCTION = /\Arest\.zuora\.com\z/i
|
10
|
+
SERVICES = /\Aservices(\d+)\.zuora\.com(:\d+)?\z/i
|
11
|
+
APISANDBOX = /\Arest.apisandbox.zuora\.com\z/i
|
12
|
+
|
13
|
+
def self.base_url(domain = '')
|
14
|
+
new(domain).base_url
|
15
|
+
end
|
16
|
+
|
17
|
+
def base_url
|
18
|
+
case domain
|
19
|
+
when PRODUCTION
|
20
|
+
'https://rest.zuora.com/'
|
21
|
+
when SERVICES
|
22
|
+
"https://#{domain}/".downcase
|
23
|
+
when APISANDBOX
|
24
|
+
'https://rest.apisandbox.zuora.com/'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :domain
|
31
|
+
|
32
|
+
def initialize(domain)
|
33
|
+
@domain = domain
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# Custom error class for rescuing from all Zuora API errors
|
5
|
+
class Error < StandardError
|
6
|
+
# Returns the appropriate IronBank::Error subclass based on status and
|
7
|
+
# response message
|
8
|
+
def self.from_response(response)
|
9
|
+
status = response[:status].to_i
|
10
|
+
|
11
|
+
klass = begin
|
12
|
+
case status
|
13
|
+
when 400 then IronBank::BadRequest
|
14
|
+
when 404 then IronBank::NotFound
|
15
|
+
when 429 then IronBank::TooManyRequests
|
16
|
+
when 500 then IronBank::InternalServerError
|
17
|
+
when 400..499 then IronBank::ClientError
|
18
|
+
when 500..599 then IronBank::ServerError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
return unless klass
|
23
|
+
|
24
|
+
klass.new(response)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Raised on errors in the 400-499 range
|
29
|
+
class ClientError < Error; end
|
30
|
+
|
31
|
+
# Raised when Zuora returns a 400 HTTP status code
|
32
|
+
class BadRequest < ClientError; end
|
33
|
+
|
34
|
+
# Raised when Zuora returns a 404 HTTP status code
|
35
|
+
class NotFound < ClientError; end
|
36
|
+
|
37
|
+
# Raised when Zuora returns a 429 HTTP status code
|
38
|
+
class TooManyRequests < ClientError; end
|
39
|
+
|
40
|
+
# Raised on errors in the 500-599 range
|
41
|
+
class ServerError < Error; end
|
42
|
+
|
43
|
+
# Raised when Zuora returns a 500 HTTP status code
|
44
|
+
class InternalServerError < ServerError; end
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# Instrumentation helper module
|
5
|
+
module Instrumentation
|
6
|
+
def instrumenter
|
7
|
+
IronBank.configuration.instrumenter
|
8
|
+
end
|
9
|
+
|
10
|
+
def instrumenter_options
|
11
|
+
IronBank.configuration.instrumenter_options || {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# A local store for exported records.
|
5
|
+
#
|
6
|
+
module Local
|
7
|
+
def find(id)
|
8
|
+
store[id] || super
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_each
|
12
|
+
return enum_for(:find_each) unless block_given?
|
13
|
+
|
14
|
+
values = store.values
|
15
|
+
values.any? ? values.each { |record| yield record } : super
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
store.any? ? store.values : super
|
20
|
+
end
|
21
|
+
|
22
|
+
def where(conditions)
|
23
|
+
records = store.values.select do |record|
|
24
|
+
conditions.all? { |field, value| record.public_send(field) == value }
|
25
|
+
end
|
26
|
+
|
27
|
+
records.any? ? records : super
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset_store
|
31
|
+
@store = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def store
|
37
|
+
@store ||= File.exist?(file_path) ? load_records : {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_records
|
41
|
+
CSV.foreach(file_path, csv_options).with_object({}) do |row, store|
|
42
|
+
# NOTE: when we move away from Ruby 2.2.x and 2.3.x we can uncomment
|
43
|
+
# this line, delete the other one, since `Hash#compact` is available in
|
44
|
+
# 2.4.x and we can remove two smells from `.reek` while we are at it
|
45
|
+
#
|
46
|
+
# store[row['Id']] = new(row.to_h.compact)
|
47
|
+
store[row['Id']] = new(row.to_h.reject { |_, value| value.nil? })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def csv_options
|
52
|
+
{
|
53
|
+
headers: true,
|
54
|
+
converters: csv_converters
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def csv_converters
|
59
|
+
%i[
|
60
|
+
decimal_integer
|
61
|
+
decimal_float
|
62
|
+
]
|
63
|
+
end
|
64
|
+
|
65
|
+
def file_path
|
66
|
+
File.expand_path(
|
67
|
+
"#{object_name}.csv",
|
68
|
+
IronBank.configuration.export_directory
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IronBank
|
4
|
+
# Utility class to save records locally.
|
5
|
+
#
|
6
|
+
class LocalRecords
|
7
|
+
private_class_method :new
|
8
|
+
|
9
|
+
RESOURCES = %w[
|
10
|
+
Product
|
11
|
+
ProductRatePlan
|
12
|
+
ProductRatePlanCharge
|
13
|
+
ProductRatePlanChargeTier
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
def self.directory
|
17
|
+
IronBank.configuration.export_directory
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.export
|
21
|
+
FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
|
22
|
+
RESOURCES.each { |resource| new(resource).export }
|
23
|
+
end
|
24
|
+
|
25
|
+
def export
|
26
|
+
CSV.open(file_path, 'w') do |csv|
|
27
|
+
csv << klass.fields # headers
|
28
|
+
write_records(csv)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :resource
|
35
|
+
|
36
|
+
def initialize(resource)
|
37
|
+
@resource = resource
|
38
|
+
end
|
39
|
+
|
40
|
+
def klass
|
41
|
+
IronBank::Resources.const_get(resource)
|
42
|
+
end
|
43
|
+
|
44
|
+
def file_path
|
45
|
+
File.expand_path("#{resource}.csv", self.class.directory)
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_records(csv)
|
49
|
+
klass.find_each { |record| csv << record.to_csv_row }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|