intacct-rest 1.0.0

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +32 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +523 -0
  5. data/lib/intacct_rest/authenticated_request.rb +75 -0
  6. data/lib/intacct_rest/client.rb +65 -0
  7. data/lib/intacct_rest/configuration.rb +169 -0
  8. data/lib/intacct_rest/custom_field.rb +19 -0
  9. data/lib/intacct_rest/endpoints/create_bill.rb +28 -0
  10. data/lib/intacct_rest/endpoints/create_bill_line.rb +31 -0
  11. data/lib/intacct_rest/endpoints/create_customer.rb +35 -0
  12. data/lib/intacct_rest/endpoints/create_invoice.rb +35 -0
  13. data/lib/intacct_rest/endpoints/create_invoice_line.rb +31 -0
  14. data/lib/intacct_rest/endpoints/create_term.rb +28 -0
  15. data/lib/intacct_rest/endpoints/create_vendor.rb +35 -0
  16. data/lib/intacct_rest/endpoints/update_vendor.rb +38 -0
  17. data/lib/intacct_rest/error_reporting.rb +28 -0
  18. data/lib/intacct_rest/errors.rb +48 -0
  19. data/lib/intacct_rest/filter.rb +34 -0
  20. data/lib/intacct_rest/model/base.rb +48 -0
  21. data/lib/intacct_rest/model/bill.rb +120 -0
  22. data/lib/intacct_rest/model/bill_line.rb +103 -0
  23. data/lib/intacct_rest/model/contact.rb +52 -0
  24. data/lib/intacct_rest/model/currency.rb +50 -0
  25. data/lib/intacct_rest/model/customer.rb +116 -0
  26. data/lib/intacct_rest/model/invoice.rb +121 -0
  27. data/lib/intacct_rest/model/invoice_line.rb +101 -0
  28. data/lib/intacct_rest/model/term.rb +88 -0
  29. data/lib/intacct_rest/model/vendor.rb +163 -0
  30. data/lib/intacct_rest/oauth_client.rb +85 -0
  31. data/lib/intacct_rest/objects.rb +30 -0
  32. data/lib/intacct_rest/page.rb +5 -0
  33. data/lib/intacct_rest/patch.rb +46 -0
  34. data/lib/intacct_rest/post.rb +43 -0
  35. data/lib/intacct_rest/query.rb +88 -0
  36. data/lib/intacct_rest/result/base.rb +17 -0
  37. data/lib/intacct_rest/result/error.rb +14 -0
  38. data/lib/intacct_rest/result/success.rb +27 -0
  39. data/lib/intacct_rest/schema_generator.rb +69 -0
  40. data/lib/intacct_rest/schema_source.rb +49 -0
  41. data/lib/intacct_rest/token_store/memory.rb +36 -0
  42. data/lib/intacct_rest/validators/custom.rb +17 -0
  43. data/lib/intacct_rest/validators/inclusion.rb +16 -0
  44. data/lib/intacct_rest/validators/kind_of.rb +24 -0
  45. data/lib/intacct_rest/validators/presence.rb +13 -0
  46. data/lib/intacct_rest/validators.rb +33 -0
  47. data/lib/intacct_rest/version.rb +3 -0
  48. data/lib/intacct_rest.rb +75 -0
  49. metadata +152 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ module Result
5
+ class Success < Base
6
+ def success? = true
7
+ def failed? = false
8
+
9
+ # The record reference Intacct returns, e.g.
10
+ # {"key"=>"111","href"=>"/objects/accounts-payable/vendor/111"}
11
+ def result
12
+ @result ||= body['ia::result'] || {}
13
+ end
14
+
15
+ # "Functional object" access: success.key, success.href, success.id
16
+ def method_missing(name, *args, &block)
17
+ return super unless args.empty? && !block && result.key?(name.to_s)
18
+
19
+ result[name.to_s]
20
+ end
21
+
22
+ def respond_to_missing?(name, include_private = false)
23
+ result.key?(name.to_s) || super
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ # Discovers a resource's available fields by sampling ONE real record
5
+ # (via Objects#find) and flattening its keys — nested Hashes are
6
+ # dot-joined ("paymentInformation.fullyPaidDate"), nested Arrays are
7
+ # sampled from their first element only ("lines.glAccount").
8
+ #
9
+ # This is a LOWER BOUND, not a guarantee of completeness: any field that
10
+ # is nil/empty on the sampled record won't appear. A nil value at a
11
+ # Hash-typed position (e.g. an unpaid invoice's null `paymentInformation`)
12
+ # collapses that whole subtree to a single bare leaf instead of its
13
+ # nested fields — inherent to single-record sampling. Treat generated
14
+ # output as a starting point to hand-review and merge, not as ground
15
+ # truth to commit blindly.
16
+ class SchemaGenerator
17
+ def initialize(objects: IntacctRest::Objects.new)
18
+ @objects = objects
19
+ end
20
+
21
+ # key: overrides the auto-picked sample record — useful when the
22
+ # auto-picked one turns out sparse/partial.
23
+ def generate(resource, key: nil)
24
+ key ||= sample_key(resource)
25
+ flatten(objects.find(resource, key))
26
+ end
27
+
28
+ def generate_all(resources)
29
+ resources.transform_values { |resource| generate(resource) }
30
+ end
31
+
32
+ # Overwrites path unconditionally — no backup, no merge with any
33
+ # existing content. Review the diff before committing the result.
34
+ def write(path, resources)
35
+ data = generate_all(resources).transform_keys(&:to_s)
36
+ File.write(path, data.to_yaml)
37
+ data
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :objects
43
+
44
+ # Intacct's /objects/{resource} list endpoint has no documented sort
45
+ # order (unconfirmed against real docs) — this is an arbitrary
46
+ # representative record from the first page, not "the latest."
47
+ def sample_key(resource)
48
+ items = objects.list(resource)
49
+ key = items.last && items.last['key']
50
+ key || raise(IntacctRest::SchemaGenerationError, "No records found for #{resource} to sample a schema from")
51
+ end
52
+
53
+ # NOTE: do not add a `return if value.nil?` guard here — a nil leaf
54
+ # value still has a field name (prefix) worth recording; only a Hash
55
+ # or Array value collapses instead of terminating, and only Array
56
+ # samples its first element.
57
+ def flatten(value, prefix = nil, fields = [])
58
+ case value
59
+ when Hash
60
+ value.each { |k, v| flatten(v, [prefix, k].compact.join('.'), fields) }
61
+ when Array
62
+ flatten(value.first, prefix, fields)
63
+ else
64
+ fields << prefix if prefix
65
+ end
66
+ fields
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ # Resolves Configuration#schema — a Hash, a YAML file path (String or
5
+ # Pathname), or nil — into field arrays for a given resource type.
6
+ #
7
+ # IntacctRest::SchemaSource.new({invoices: %w[key state]}).for(:invoices)
8
+ # IntacctRest::SchemaSource.new("config/intacct_schema.yml").for(:invoices)
9
+ class SchemaSource
10
+ include ErrorReporting
11
+
12
+ def initialize(source, config: IntacctRest.configuration)
13
+ @source = source
14
+ @config = config
15
+ end
16
+
17
+ def for(resource_type)
18
+ Array(data[resource_type.to_s]).map(&:to_s)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :source, :config
24
+
25
+ def data
26
+ @data ||= normalize(source)
27
+ end
28
+
29
+ def normalize(source)
30
+ case source
31
+ when Hash
32
+ source.transform_keys(&:to_s)
33
+ when String, Pathname
34
+ load_yaml(source)
35
+ when nil
36
+ {}
37
+ else
38
+ raise ArgumentError, "Unsupported schema source: #{source.class}"
39
+ end
40
+ end
41
+
42
+ def load_yaml(path)
43
+ YAML.safe_load_file(path.to_s) || {}
44
+ rescue Errno::ENOENT, Psych::Exception => e
45
+ raise_reported IntacctRest::SchemaLoadError.new("Failed to load schema from #{path}: #{e.message}"),
46
+ operation: :schema_load, path: path.to_s
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ module TokenStore
5
+ # Default token store: thread-safe, self-expiring, process-local.
6
+ # Any object responding to #read/#write(ttl:)/#delete can be used instead
7
+ # (see IntacctRest::Configuration#token_store) — e.g. a Redis-backed store.
8
+ class Memory
9
+ def initialize
10
+ @values = {}
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def read(key)
15
+ @mutex.synchronize do
16
+ value, expires_at = @values[key]
17
+ next nil if value.nil?
18
+ next nil if expires_at && expires_at <= Time.now
19
+
20
+ value
21
+ end
22
+ end
23
+
24
+ def write(key, value, ttl: nil)
25
+ @mutex.synchronize do
26
+ expires_at = ttl ? Time.now + ttl : nil
27
+ @values[key] = [value, expires_at]
28
+ end
29
+ end
30
+
31
+ def delete(key)
32
+ @mutex.synchronize { @values.delete(key) }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ module Validators
5
+ module Custom
6
+ def self.call(record, attribute, method_name)
7
+ value = record.public_send(attribute)
8
+ return [] if value.nil?
9
+ return [] if record.send(method_name, value)
10
+
11
+ ["#{attribute} is invalid"]
12
+ end
13
+ end
14
+
15
+ register(:custom, Custom)
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ module Validators
5
+ module Inclusion
6
+ def self.call(record, attribute, list)
7
+ value = record.public_send(attribute)
8
+ return [] if value.nil? || Array(list).include?(value)
9
+
10
+ ["#{attribute} must be one of #{Array(list).join(', ')} or nil"]
11
+ end
12
+ end
13
+
14
+ register(:inclusion, Inclusion)
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ module Validators
5
+ module KindOf
6
+ TYPE_MAP = {
7
+ string: String, integer: Integer, numeric: Numeric, float: Float,
8
+ boolean: [TrueClass, FalseClass], hash: Hash, array: Array, date: Date, time: Time
9
+ }.freeze
10
+
11
+ def self.call(record, attribute, kind)
12
+ value = record.public_send(attribute)
13
+ return [] if value.nil?
14
+
15
+ expected = TYPE_MAP.fetch(kind) { raise ArgumentError, "Unknown kind_of type: #{kind.inspect}" }
16
+ return [] if Array(expected).any? { |klass| value.is_a?(klass) }
17
+
18
+ ["#{attribute} must be a #{kind}"]
19
+ end
20
+ end
21
+
22
+ register(:kind_of, KindOf)
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ module Validators
5
+ module Presence
6
+ def self.call(record, attribute, *)
7
+ record.public_send(attribute).nil? ? ["#{attribute} is required"] : []
8
+ end
9
+ end
10
+
11
+ register(:presence, Presence)
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IntacctRest
4
+ # Registry of validator modules, keyed by kind (:presence, :kind_of,
5
+ # :inclusion, ...). Knows nothing about which validators exist itself —
6
+ # each validators/*.rb file registers its own kind when required, so
7
+ # adding a new validator kind never means editing this file.
8
+ module Validators
9
+ class << self
10
+ def register(kind, validator)
11
+ registry[kind] = validator
12
+ end
13
+
14
+ def fetch(kind)
15
+ registry.fetch(kind) do
16
+ known = []
17
+ each_validator { |name, _| known << name }
18
+ raise ArgumentError, "Unknown validator: #{kind.inspect} (known: #{known.join(', ')})"
19
+ end
20
+ end
21
+
22
+ def each_validator(&block)
23
+ registry.each(&block)
24
+ end
25
+
26
+ private
27
+
28
+ def registry
29
+ @registry ||= {}
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module IntacctRest
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "yaml"
6
+ require "pathname"
7
+ require "date"
8
+
9
+ module IntacctRest
10
+ API_URI = 'https://api.intacct.com/ia/api'
11
+ API_VERSION = 'v1'
12
+ end
13
+
14
+ require 'intacct_rest/version'
15
+ require 'intacct_rest/errors'
16
+ require 'intacct_rest/token_store/memory'
17
+ require 'intacct_rest/configuration'
18
+ require 'intacct_rest/client'
19
+ require 'intacct_rest/filter'
20
+ require 'intacct_rest/page'
21
+ require 'intacct_rest/error_reporting'
22
+ require 'intacct_rest/authenticated_request'
23
+ require 'intacct_rest/oauth_client'
24
+ require 'intacct_rest/custom_field'
25
+ require 'intacct_rest/validators'
26
+ require 'intacct_rest/validators/presence'
27
+ require 'intacct_rest/validators/kind_of'
28
+ require 'intacct_rest/validators/inclusion'
29
+ require 'intacct_rest/validators/custom'
30
+ require 'intacct_rest/model/base'
31
+ require 'intacct_rest/model/vendor'
32
+ require 'intacct_rest/model/invoice'
33
+ require 'intacct_rest/model/term'
34
+ require 'intacct_rest/model/invoice_line'
35
+ require 'intacct_rest/model/currency'
36
+ require 'intacct_rest/model/customer'
37
+ require 'intacct_rest/model/contact'
38
+ require 'intacct_rest/model/bill'
39
+ require 'intacct_rest/model/bill_line'
40
+ require 'intacct_rest/result/base'
41
+ require 'intacct_rest/result/success'
42
+ require 'intacct_rest/result/error'
43
+ require 'intacct_rest/post'
44
+ require 'intacct_rest/patch'
45
+ require 'intacct_rest/endpoints/create_vendor'
46
+ require 'intacct_rest/endpoints/update_vendor'
47
+ require 'intacct_rest/endpoints/create_invoice'
48
+ require 'intacct_rest/endpoints/create_term'
49
+ require 'intacct_rest/endpoints/create_invoice_line'
50
+ require 'intacct_rest/endpoints/create_customer'
51
+ require 'intacct_rest/endpoints/create_bill'
52
+ require 'intacct_rest/endpoints/create_bill_line'
53
+ require 'intacct_rest/query'
54
+ require 'intacct_rest/objects'
55
+ require 'intacct_rest/schema_generator'
56
+ require 'intacct_rest/schema_source'
57
+
58
+ module IntacctRest
59
+ class << self
60
+ attr_writer :configuration
61
+ end
62
+
63
+ def self.reset
64
+ @configuration = nil
65
+ end
66
+
67
+ def self.configuration
68
+ @configuration ||= IntacctRest::Configuration.new
69
+ end
70
+
71
+ def self.configure
72
+ yield configuration if block_given?
73
+ end
74
+ end
75
+
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: intacct-rest
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bernardo Espinoza
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: webmock
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Allows to use SAGE Rest API for read, create and update SAGE's clients
69
+ data
70
+ email:
71
+ - bernardo466@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files:
75
+ - CHANGELOG.md
76
+ - LICENSE.txt
77
+ - README.md
78
+ files:
79
+ - CHANGELOG.md
80
+ - LICENSE.txt
81
+ - README.md
82
+ - lib/intacct_rest.rb
83
+ - lib/intacct_rest/authenticated_request.rb
84
+ - lib/intacct_rest/client.rb
85
+ - lib/intacct_rest/configuration.rb
86
+ - lib/intacct_rest/custom_field.rb
87
+ - lib/intacct_rest/endpoints/create_bill.rb
88
+ - lib/intacct_rest/endpoints/create_bill_line.rb
89
+ - lib/intacct_rest/endpoints/create_customer.rb
90
+ - lib/intacct_rest/endpoints/create_invoice.rb
91
+ - lib/intacct_rest/endpoints/create_invoice_line.rb
92
+ - lib/intacct_rest/endpoints/create_term.rb
93
+ - lib/intacct_rest/endpoints/create_vendor.rb
94
+ - lib/intacct_rest/endpoints/update_vendor.rb
95
+ - lib/intacct_rest/error_reporting.rb
96
+ - lib/intacct_rest/errors.rb
97
+ - lib/intacct_rest/filter.rb
98
+ - lib/intacct_rest/model/base.rb
99
+ - lib/intacct_rest/model/bill.rb
100
+ - lib/intacct_rest/model/bill_line.rb
101
+ - lib/intacct_rest/model/contact.rb
102
+ - lib/intacct_rest/model/currency.rb
103
+ - lib/intacct_rest/model/customer.rb
104
+ - lib/intacct_rest/model/invoice.rb
105
+ - lib/intacct_rest/model/invoice_line.rb
106
+ - lib/intacct_rest/model/term.rb
107
+ - lib/intacct_rest/model/vendor.rb
108
+ - lib/intacct_rest/oauth_client.rb
109
+ - lib/intacct_rest/objects.rb
110
+ - lib/intacct_rest/page.rb
111
+ - lib/intacct_rest/patch.rb
112
+ - lib/intacct_rest/post.rb
113
+ - lib/intacct_rest/query.rb
114
+ - lib/intacct_rest/result/base.rb
115
+ - lib/intacct_rest/result/error.rb
116
+ - lib/intacct_rest/result/success.rb
117
+ - lib/intacct_rest/schema_generator.rb
118
+ - lib/intacct_rest/schema_source.rb
119
+ - lib/intacct_rest/token_store/memory.rb
120
+ - lib/intacct_rest/validators.rb
121
+ - lib/intacct_rest/validators/custom.rb
122
+ - lib/intacct_rest/validators/inclusion.rb
123
+ - lib/intacct_rest/validators/kind_of.rb
124
+ - lib/intacct_rest/validators/presence.rb
125
+ - lib/intacct_rest/version.rb
126
+ homepage: https://github.com/bernespinoza/intacct-rest
127
+ licenses:
128
+ - MIT
129
+ metadata:
130
+ source_code_uri: https://github.com/bernespinoza/intacct-rest
131
+ bug_tracker_uri: https://github.com/bernespinoza/intacct-rest/issues
132
+ changelog_uri: https://github.com/bernespinoza/intacct-rest/blob/main/CHANGELOG.md
133
+ rubygems_mfa_required: 'true'
134
+ rdoc_options:
135
+ - "--charset=UTF-8"
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '3'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubygems_version: 4.0.20
150
+ specification_version: 4
151
+ summary: A Ruby wrapper for Intacct SAGE Rest API
152
+ test_files: []