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