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