iron_bank 0.1.0 → 0.7.1

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