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.
Files changed (85) hide show
  1. checksums.yaml +5 -5
  2. data/.env.example +7 -0
  3. data/.github/CODEOWNERS +1 -0
  4. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  5. data/.gitignore +6 -1
  6. data/.reek +95 -0
  7. data/.rspec +1 -2
  8. data/.rubocop.yml +55 -0
  9. data/.travis.yml +22 -3
  10. data/Gemfile +3 -3
  11. data/LICENSE +176 -0
  12. data/README.md +133 -0
  13. data/Rakefile +39 -3
  14. data/bin/console +13 -1
  15. data/bin/setup +1 -0
  16. data/iron_bank.gemspec +34 -10
  17. data/lib/generators/iron_bank/install/install_generator.rb +20 -0
  18. data/lib/generators/iron_bank/install/templates/README +16 -0
  19. data/lib/generators/iron_bank/install/templates/iron_bank.rb +29 -0
  20. data/lib/iron_bank/action.rb +39 -0
  21. data/lib/iron_bank/actions/amend.rb +16 -0
  22. data/lib/iron_bank/actions/create.rb +19 -0
  23. data/lib/iron_bank/actions/delete.rb +19 -0
  24. data/lib/iron_bank/actions/execute.rb +21 -0
  25. data/lib/iron_bank/actions/generate.rb +19 -0
  26. data/lib/iron_bank/actions/query.rb +16 -0
  27. data/lib/iron_bank/actions/query_more.rb +21 -0
  28. data/lib/iron_bank/actions/subscribe.rb +32 -0
  29. data/lib/iron_bank/actions/update.rb +19 -0
  30. data/lib/iron_bank/associations.rb +73 -0
  31. data/lib/iron_bank/authentication.rb +37 -0
  32. data/lib/iron_bank/authentications/cookie.rb +80 -0
  33. data/lib/iron_bank/authentications/token.rb +82 -0
  34. data/lib/iron_bank/cacheable.rb +52 -0
  35. data/lib/iron_bank/client.rb +96 -0
  36. data/lib/iron_bank/collection.rb +31 -0
  37. data/lib/iron_bank/configuration.rb +86 -0
  38. data/lib/iron_bank/csv.rb +29 -0
  39. data/lib/iron_bank/describe/field.rb +65 -0
  40. data/lib/iron_bank/describe/object.rb +81 -0
  41. data/lib/iron_bank/describe/related.rb +40 -0
  42. data/lib/iron_bank/describe/tenant.rb +50 -0
  43. data/lib/iron_bank/endpoint.rb +36 -0
  44. data/lib/iron_bank/error.rb +45 -0
  45. data/lib/iron_bank/instrumentation.rb +14 -0
  46. data/lib/iron_bank/local.rb +72 -0
  47. data/lib/iron_bank/local_records.rb +52 -0
  48. data/lib/iron_bank/logger.rb +23 -0
  49. data/lib/iron_bank/metadata.rb +36 -0
  50. data/lib/iron_bank/object.rb +89 -0
  51. data/lib/iron_bank/open_tracing.rb +17 -0
  52. data/lib/iron_bank/operation.rb +33 -0
  53. data/lib/iron_bank/operations/billing_preview.rb +16 -0
  54. data/lib/iron_bank/query_builder.rb +72 -0
  55. data/lib/iron_bank/queryable.rb +55 -0
  56. data/lib/iron_bank/resource.rb +70 -0
  57. data/lib/iron_bank/resources/account.rb +62 -0
  58. data/lib/iron_bank/resources/amendment.rb +13 -0
  59. data/lib/iron_bank/resources/catalog_tiers/discount_amount.rb +26 -0
  60. data/lib/iron_bank/resources/catalog_tiers/discount_percentage.rb +26 -0
  61. data/lib/iron_bank/resources/catalog_tiers/price.rb +26 -0
  62. data/lib/iron_bank/resources/contact.rb +13 -0
  63. data/lib/iron_bank/resources/export.rb +11 -0
  64. data/lib/iron_bank/resources/import.rb +12 -0
  65. data/lib/iron_bank/resources/invoice.rb +37 -0
  66. data/lib/iron_bank/resources/invoice_adjustment.rb +13 -0
  67. data/lib/iron_bank/resources/invoice_item.rb +25 -0
  68. data/lib/iron_bank/resources/invoice_payment.rb +13 -0
  69. data/lib/iron_bank/resources/payment.rb +17 -0
  70. data/lib/iron_bank/resources/payment_method.rb +14 -0
  71. data/lib/iron_bank/resources/product.rb +14 -0
  72. data/lib/iron_bank/resources/product_rate_plan.rb +32 -0
  73. data/lib/iron_bank/resources/product_rate_plan_charge.rb +27 -0
  74. data/lib/iron_bank/resources/product_rate_plan_charge_tier.rb +60 -0
  75. data/lib/iron_bank/resources/rate_plan.rb +21 -0
  76. data/lib/iron_bank/resources/rate_plan_charge.rb +30 -0
  77. data/lib/iron_bank/resources/rate_plan_charge_tier.rb +26 -0
  78. data/lib/iron_bank/resources/subscription.rb +28 -0
  79. data/lib/iron_bank/resources/usage.rb +16 -0
  80. data/lib/iron_bank/response/raise_error.rb +16 -0
  81. data/lib/iron_bank/schema.rb +58 -0
  82. data/lib/iron_bank/utils.rb +59 -0
  83. data/lib/iron_bank/version.rb +4 -1
  84. data/lib/iron_bank.rb +152 -2
  85. metadata +300 -12
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'logger'
5
+
6
+ module IronBank
7
+ # Default logger for IronBank events
8
+ #
9
+ class Logger
10
+ extend Forwardable
11
+
12
+ PROGNAME = 'iron_bank'
13
+ LEVEL = ::Logger::DEBUG
14
+
15
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
16
+
17
+ def initialize(logger: ::Logger.new(STDOUT), level: LEVEL)
18
+ @logger = logger
19
+ @logger.progname = PROGNAME
20
+ @logger.level = level
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # Metadata to provide accessors to Zuora resources.
5
+ #
6
+ module Metadata
7
+ # Can be overriden to exclude specific fields for a given resource, see
8
+ # `Account` class for an example
9
+ def exclude_fields
10
+ []
11
+ end
12
+
13
+ def fields
14
+ return [] unless schema
15
+
16
+ @fields ||= schema.fields.map(&:name) - exclude_fields
17
+ end
18
+
19
+ def query_fields
20
+ return [] unless schema
21
+
22
+ @query_fields ||= schema.query_fields - exclude_fields
23
+ end
24
+
25
+ def schema
26
+ @schema ||= IronBank::Schema.for(object_name)
27
+ end
28
+
29
+ def with_schema
30
+ fields.each do |field|
31
+ method_name = IronBank::Utils.underscore(field)
32
+ define_method(:"#{method_name}") { remote[field] }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # This object holds the initial payload (hash) sent through one of the
5
+ # action/operation. It exposes methods to convert the payload to either
6
+ # upper camel case (typically used by actions) or lower camel case.
7
+ #
8
+ # It is also use to parse the response from Zuora and convert it into a Ruby-
9
+ # friendly Hash.
10
+ #
11
+ class Object
12
+ SNOWFLAKE_FIELDS = ['fieldsToNull'].freeze
13
+
14
+ attr_reader :payload
15
+
16
+ def initialize(payload)
17
+ @payload = payload
18
+ end
19
+
20
+ # FIXME: refactor both camelize/underscore methods into one
21
+ def deep_camelize(type: :upper)
22
+ payload.each_pair.with_object({}) do |(field, value), hash|
23
+ field = field.to_s
24
+
25
+ key = if SNOWFLAKE_FIELDS.include?(field)
26
+ field
27
+ else
28
+ IronBank::Utils.camelize(field, type: type)
29
+ end
30
+
31
+ hash[key] = camelize(value, type: type)
32
+ end
33
+ end
34
+
35
+ # FIXME: refactor both camelize/underscore methods into one
36
+ def deep_underscore
37
+ payload.each_pair.with_object({}) do |(field, value), hash|
38
+ key = IronBank::Utils.underscore(field.to_s).to_sym
39
+ hash[key] = underscore(value)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # FIXME: refactor both camelize/underscore methods into one
46
+ def camelize(value, type: :upper)
47
+ if value.is_a?(Array)
48
+ camelize_array(value, type: type)
49
+ elsif value.is_a?(Hash)
50
+ IronBank::Object.new(value).deep_camelize(type: type)
51
+ elsif value.is_a?(IronBank::Object)
52
+ value.deep_camelize(type: type)
53
+ else
54
+ value
55
+ end
56
+ end
57
+
58
+ # FIXME: refactor both camelize/underscore methods into one
59
+ def camelize_array(value, type: :upper)
60
+ value.each.with_object([]) do |item, payload|
61
+ item = IronBank::Object.new(item) if item.is_a?(Hash)
62
+ item = item.deep_camelize(type: type) unless item.is_a?(String)
63
+
64
+ payload.push(item)
65
+ end
66
+ end
67
+
68
+ # FIXME: refactor both camelize/underscore methods into one
69
+ def underscore(value)
70
+ if value.is_a?(Array)
71
+ underscore_array(value)
72
+ elsif value.is_a?(Hash)
73
+ IronBank::Object.new(value).deep_underscore
74
+ elsif value.is_a?(IronBank::Object)
75
+ value.deep_underscore
76
+ else
77
+ value
78
+ end
79
+ end
80
+
81
+ # FIXME: refactor both camelize/underscore methods into one
82
+ def underscore_array(value)
83
+ value.each.with_object([]) do |item, payload|
84
+ item = IronBank::Object.new(item) if item.is_a?(Hash)
85
+ payload.push(item.deep_underscore)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # Open Tracing helper module
5
+ module OpenTracing
6
+ def open_tracing_enabled?
7
+ IronBank.configuration.open_tracing_enabled
8
+ end
9
+
10
+ def open_tracing_options
11
+ {
12
+ distributed_tracing: true,
13
+ split_by_domain: false
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # Base class for Zuora operations, e.g., billing preview
5
+ #
6
+ class Operation
7
+ private_class_method :new
8
+
9
+ def self.call(args)
10
+ new(args).call
11
+ end
12
+
13
+ def call
14
+ IronBank.client.connection.post(endpoint, params).body
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :args
20
+
21
+ def initialize(args)
22
+ @args = args
23
+ end
24
+
25
+ def endpoint
26
+ "v1/operations/#{IronBank::Utils.kebab(name)}"
27
+ end
28
+
29
+ def name
30
+ self.class.name.split('::').last
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Operations
5
+ # Delete one or more objects of the same type
6
+ # https://www.zuora.com/developer/api-reference/#operation/Action_POSTdelete
7
+ #
8
+ class BillingPreview < Operation
9
+ private
10
+
11
+ def params
12
+ IronBank::Object.new(args).deep_camelize(type: :lower)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # A query builder helps buidling a syntaxically correct query using ZOQL.
5
+ #
6
+ class QueryBuilder
7
+ private_class_method :new
8
+
9
+ def self.zoql(object, fields, conditions = {})
10
+ new(object, fields, conditions).zoql
11
+ end
12
+
13
+ def zoql
14
+ query = "select #{query_fields} from #{object}"
15
+ conditions.empty? ? query : "#{query} where #{query_conditions}"
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :object, :fields, :conditions
21
+
22
+ def initialize(object, fields, conditions)
23
+ @object = object
24
+ @fields = fields
25
+ @conditions = conditions
26
+ end
27
+
28
+ def query_fields
29
+ fields.join(',')
30
+ end
31
+
32
+ def query_conditions
33
+ ensure_range_single_condition
34
+
35
+ case conditions
36
+ when Hash
37
+ hash_query_conditions
38
+ end
39
+ end
40
+
41
+ def range_query_builder(field, value)
42
+ value.each.with_object([]) do |option, range_query|
43
+ range_query << "#{field}='#{option}'"
44
+ end.join(' OR ')
45
+ end
46
+
47
+ def hash_query_conditions
48
+ conditions.each.with_object([]) do |(field, value), filters|
49
+ # TODO: sanitize the value
50
+ field = IronBank::Utils.camelize(field)
51
+ filters << current_filter(field, value)
52
+ end.join(' AND ')
53
+ end
54
+
55
+ def current_filter(field, value)
56
+ if value.is_a?(Array)
57
+ range_query_builder(field, value)
58
+ elsif [true, false].include? value
59
+ "#{field}=#{value}"
60
+ else
61
+ "#{field}='#{value}'"
62
+ end
63
+ end
64
+
65
+ def ensure_range_single_condition
66
+ return if conditions.count <= 1
67
+ return unless conditions.values.any? { |value| value.is_a?(Array) }
68
+
69
+ raise 'Filter ranges must be used in isolation.'
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # Query-like features, such as `find` and `where` methods for a resource.
5
+ #
6
+ module Queryable
7
+ # We use the REST endpoint for the `find` method
8
+ def find(id)
9
+ raise IronBank::NotFound unless id
10
+
11
+ new(
12
+ IronBank.client.connection.get("v1/object/#{object_name}/#{id}").body
13
+ )
14
+ end
15
+
16
+ # This methods leverages the fact that Zuora only returns 2,000 records at a
17
+ # time, hance providing a default batch size
18
+ def find_each
19
+ return enum_for(:find_each) unless block_given?
20
+
21
+ client = IronBank.client
22
+ query_string = IronBank::QueryBuilder.zoql(object_name, query_fields)
23
+ query_result = client.query(query_string) # up to 2k records from Zuora
24
+
25
+ loop do
26
+ query_result['records'].each { |data| yield new(data) }
27
+ break if query_result['done']
28
+
29
+ query_result = client.query_more(query_result['queryLocator'])
30
+ end
31
+ end
32
+
33
+ def all
34
+ where({})
35
+ end
36
+
37
+ def where(conditions)
38
+ query_string = IronBank::QueryBuilder.zoql(
39
+ object_name,
40
+ query_fields,
41
+ conditions
42
+ )
43
+
44
+ # FIXME: need to use logger instance instead
45
+ # puts "query: #{query_string}"
46
+
47
+ records = IronBank::Query.call(query_string)['records']
48
+ return [] unless records
49
+
50
+ records.each.with_object([]) do |data, result|
51
+ result << new(data)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ # An Iron Bank RESTful resource.
5
+ #
6
+ class Resource
7
+ include IronBank::Associations
8
+ extend IronBank::Associations::ClassMethods
9
+ extend IronBank::Metadata
10
+ extend IronBank::Queryable
11
+
12
+ def self.object_name
13
+ name.split('::').last
14
+ end
15
+
16
+ def self.with_local_records
17
+ extend IronBank::Local
18
+ end
19
+
20
+ def self.with_cache
21
+ include IronBank::Cacheable
22
+ extend IronBank::Cacheable::ClassMethods
23
+ end
24
+
25
+ attr_reader :remote
26
+
27
+ def initialize(remote = {})
28
+ @remote = remote
29
+ end
30
+
31
+ # Every Zuora object has an ID, so we can safely declare it for each
32
+ # resource
33
+ def id
34
+ remote['Id']
35
+ end
36
+
37
+ def inspect
38
+ # NOTE: In Ruby, the IDs of objects start from the second bit on the right
39
+ # but in "value space" (used by the original `inspect` implementation)
40
+ # they start from the third bit on the right. Hence the bitsfhit operation
41
+ # here.
42
+ # https://stackoverflow.com/questions/2818602/in-ruby-why-does-inspect-print-out-some-kind-of-object-id-which-is-different
43
+ ruby_id = "#{self.class.name}:0x#{(object_id << 1).to_s(16)} id=\"#{id}\""
44
+ respond_to?(:name) ? "#<#{ruby_id} name=\"#{name}\">" : "#<#{ruby_id}>"
45
+ end
46
+
47
+ # Two resources are equals if their remote (from Zuora) data are similar
48
+ def ==(other)
49
+ other.is_a?(IronBank::Resource) ? remote == other.remote : false
50
+ end
51
+
52
+ def reload
53
+ remove_instance_vars
54
+ @remote = self.class.find(id).remote
55
+ end
56
+
57
+ def to_csv_row
58
+ self.class.fields.each.with_object([]) do |field, row|
59
+ row << remote[field]
60
+ end
61
+ end
62
+
63
+ def remove_instance_vars
64
+ # Substract predefined variables from the instance variables
65
+ (instance_variables - [:@remote]).each do |var|
66
+ remove_instance_variable(:"#{var}")
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # A Zuora account is used for billing purposes: it holds many subscriptions,
6
+ # has many contacts (but only one bill to and one sold to contact), can have
7
+ # a default payment method, hence auto-pay can be activated for this account
8
+ # or not, is billed invoices and can pay them using an electronic payment
9
+ # method (usually a credit card, but PayPal is also accepted by Zuora).
10
+ #
11
+ class Account < Resource
12
+ # Tenants without credit memo activated cannot query these fields BUT they
13
+ # are still described as `selectable` through the metadata.
14
+ #
15
+ # Similarly, accounts with `TaxExemptStatus` set to `No` cannot query
16
+ # the `TaxExemptEntityUseCode` related fields.
17
+ def self.exclude_fields
18
+ %w[
19
+ TaxExemptEntityUseCode
20
+ TotalDebitMemoBalance
21
+ UnappliedCreditMemoAmount
22
+ ]
23
+ end
24
+ with_schema
25
+
26
+ # Contacts
27
+ with_one :bill_to, resource_name: 'Contact'
28
+ with_one :sold_to, resource_name: 'Contact'
29
+ with_many :contacts
30
+
31
+ # Subscriptions
32
+ with_many :subscriptions
33
+ with_many :active_subscriptions,
34
+ resource_name: 'Subscription',
35
+ conditions: { status: 'Active' }
36
+
37
+ # Invoices
38
+ with_many :invoices
39
+
40
+ # Payment Methods
41
+ with_one :default_payment_method, resource_name: 'PaymentMethod'
42
+ with_many :payment_methods
43
+
44
+ # Payments
45
+ with_many :payments
46
+
47
+ # Usages
48
+ with_many :usages
49
+
50
+ # Parent
51
+ with_one :parent, resource_name: 'Account'
52
+
53
+ def ultimate_parent
54
+ root if parent
55
+ end
56
+
57
+ def root
58
+ parent ? parent.root : self
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # An amendment updates a subscription version and has a type: new product,
6
+ # update product, remove product, renewal, terms and conditions amendment.
7
+ #
8
+ class Amendment < Resource
9
+ with_schema
10
+ with_one :subscription
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ module CatalogTiers
6
+ # A tier that holds an discount ammount for a given product rate plan
7
+ # charge.
8
+ #
9
+ class DiscountAmount < ProductRatePlanChargeTier
10
+ def self.exclude_fields
11
+ %w[
12
+ Active
13
+ DiscountPercentage
14
+ IncludedUnits
15
+ OveragePrice
16
+ Price
17
+ ]
18
+ end
19
+
20
+ def self.object_name
21
+ superclass.object_name
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ module CatalogTiers
6
+ # A tier that holds an discount percentage for a given product rate plan
7
+ # charge.
8
+ #
9
+ class DiscountPercentage < ProductRatePlanChargeTier
10
+ def self.exclude_fields
11
+ %w[
12
+ Active
13
+ DiscountAmount
14
+ IncludedUnits
15
+ OveragePrice
16
+ Price
17
+ ]
18
+ end
19
+
20
+ def self.object_name
21
+ superclass.object_name
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ module CatalogTiers
6
+ # A tier that holds an amount (cost) and a currency for a given product
7
+ # rate plan charge.
8
+ #
9
+ class Price < ProductRatePlanChargeTier
10
+ def self.exclude_fields
11
+ %w[
12
+ Active
13
+ DiscountAmount
14
+ DiscountPercentage
15
+ IncludedUnits
16
+ OveragePrice
17
+ ]
18
+ end
19
+
20
+ def self.object_name
21
+ superclass.object_name
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # A Zuora contact, belongs to an account, can be set as the sold to/bill to
6
+ # contact for a given account.
7
+ #
8
+ class Contact < Resource
9
+ with_schema
10
+ with_one :account
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # Export ZOQL queries.
6
+ #
7
+ class Export < Resource
8
+ with_schema
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # Import usages into Zuora.
6
+ #
7
+ class Import < Resource
8
+ with_schema
9
+ with_many :usages
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # A Zuora invoice is generated through a bill run, belongs to an account and
6
+ # holds many invoice items.
7
+ #
8
+ class Invoice < Resource
9
+ # These fields are declared as `<selectable>true</selectable>` but Zuora
10
+ # returns a QueryError when trying to query an invoice with them. Also,
11
+ # the `Body` field can only be retrieved for a single invoice at a time.
12
+ def self.exclude_fields
13
+ %w[
14
+ AutoPay
15
+ BillRunId
16
+ BillToContactSnapshotId
17
+ Body
18
+ RegenerateInvoicePDF
19
+ SoldToContactSnapshotId
20
+ ]
21
+ end
22
+ with_schema
23
+
24
+ with_one :account
25
+
26
+ with_many :invoice_adjustments, aka: :adjustments
27
+ with_many :invoice_items, aka: :items
28
+ with_many :invoice_payments
29
+
30
+ # We can only retrieve one invoice body at a time, hence Body is excluded
31
+ # from the query fields, but is populated using the `find` class method
32
+ def body
33
+ remote['Body'] || reload['Body']
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # An invoice adjustment modifies the total invoice balance.
6
+ #
7
+ class InvoiceAdjustment < Resource
8
+ with_schema
9
+ with_one :account
10
+ with_one :invoice
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IronBank
4
+ module Resources
5
+ # An invoice item holds a charge that is billed to a customer.
6
+ #
7
+ class InvoiceItem < Resource
8
+ def self.exclude_fields
9
+ %w[
10
+ AppliedToChargeNumber
11
+ Balance
12
+ ]
13
+ end
14
+ with_schema
15
+
16
+ with_one :invoice
17
+ with_one :subscription
18
+
19
+ # NOTE: the `product_id` field is not always populated by Zuora in the GET
20
+ # request (`#find` method), I don't exactly know why.
21
+ with_one :product
22
+ with_one :charge
23
+ end
24
+ end
25
+ end