ledger_sync 1.5.0 → 1.5.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/ledger_sync/test/support/test_ledger/client.rb +21 -0
  4. data/lib/ledger_sync/test/support/test_ledger/config.rb +13 -0
  5. data/lib/ledger_sync/test/support/test_ledger/customer/deserializer.rb +20 -0
  6. data/lib/ledger_sync/test/support/test_ledger/customer/operations/create.rb +25 -0
  7. data/lib/ledger_sync/test/support/test_ledger/customer/operations/find.rb +21 -0
  8. data/lib/ledger_sync/test/support/test_ledger/customer/operations/update.rb +25 -0
  9. data/lib/ledger_sync/test/support/test_ledger/customer/searcher.rb +15 -0
  10. data/lib/ledger_sync/test/support/test_ledger/customer/serializer.rb +21 -0
  11. data/lib/ledger_sync/test/support/test_ledger/deserializer.rb +15 -0
  12. data/lib/ledger_sync/test/support/test_ledger/operation/create.rb +24 -0
  13. data/lib/ledger_sync/test/support/test_ledger/operation/find.rb +25 -0
  14. data/lib/ledger_sync/test/support/test_ledger/operation/update.rb +24 -0
  15. data/lib/ledger_sync/test/support/test_ledger/operation.rb +53 -0
  16. data/lib/ledger_sync/test/support/test_ledger/resource.rb +10 -0
  17. data/lib/ledger_sync/test/support/test_ledger/resources/customer.rb +18 -0
  18. data/lib/ledger_sync/test/support/test_ledger/resources/subsidiary.rb +12 -0
  19. data/lib/ledger_sync/test/support/test_ledger/searcher.rb +60 -0
  20. data/lib/ledger_sync/test/support/test_ledger/serializer.rb +71 -0
  21. data/lib/ledger_sync/test/support/test_ledger/subsidiary/deserializer.rb +17 -0
  22. data/lib/ledger_sync/test/support/test_ledger/subsidiary/searcher.rb +12 -0
  23. data/lib/ledger_sync/test/support/test_ledger/subsidiary/searcher_deserializer.rb +15 -0
  24. data/lib/ledger_sync/test/support/test_ledger/subsidiary/serializer.rb +15 -0
  25. data/lib/ledger_sync/test/support/test_ledger/util/error_matcher.rb +57 -0
  26. data/lib/ledger_sync/test/support/test_ledger/util/error_parser.rb +27 -0
  27. data/lib/ledger_sync/test/support/test_ledger/util/ledger_error_parser.rb +103 -0
  28. data/lib/ledger_sync/test/support/test_ledger/util/operation_error_parser.rb +99 -0
  29. data/lib/ledger_sync/test/support/test_ledger/webhook.rb +58 -0
  30. data/lib/ledger_sync/test/support/test_ledger/webhook_event.rb +81 -0
  31. data/lib/ledger_sync/test/support/test_ledger/webhook_notification.rb +43 -0
  32. data/lib/ledger_sync/test/support.rb +1 -1
  33. data/lib/ledger_sync/version.rb +1 -1
  34. metadata +30 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19414532e2c552b3e17efa9e8dc0315740809c7bbe74124af62f55dbd4bad2d0
4
- data.tar.gz: a8cf13be61f9ce6a738f4bf0d54774da3b676714a6e982394036fd924503a3e0
3
+ metadata.gz: efcf3cc57a3f68ad9612fde0eb59ac3e06acd8d644a579397c2c1a17046d4be1
4
+ data.tar.gz: db61863e31a0fc7004e020b1c0672b52a3de117edd7b57a5ff874edf6505e20c
5
5
  SHA512:
6
- metadata.gz: 97a8377503769687b0f4b765ece61bb4fe9b4689adf493e8d41fe3d77fd58f90f53a0c51f6412d3f69221e697ae6c6633a4bb6edd54013cae86022a0c7b683b0
7
- data.tar.gz: 7a6dfcde809c765cee2faad8f063c321f1baa064ff47cb98e5187042631fa13624b91e7954b729e4ded47ccaabef742b95579dcd5e66e9ef200df7a02fbc817e
6
+ metadata.gz: 777f81a8164db6a0e8d82081bcd70d01bdf34542ace2ae494fa475e632201df1e23211308d6df18f97eab5bf49fe1743bcce52bfc44b0e2aebf8d468ce3b8e21
7
+ data.tar.gz: 3398a7232feb2f4659061705f7aefe960fcf2459a84f156d6ab86f2be8967f0a28c6a0035b4688780dcca99d6937a23b1f0de38bffc0f2e1031c577056fbcef6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ledger_sync (1.5.0)
4
+ ledger_sync (1.5.1)
5
5
  activemodel
6
6
  colorize
7
7
  coveralls (~> 0.8.23)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Client
7
+ include Ledgers::Client::Mixin
8
+
9
+ attr_reader :api_key
10
+
11
+ def initialize(args = {})
12
+ @api_key = args.fetch(:api_key)
13
+ end
14
+
15
+ def self.ledger_attributes_to_save
16
+ %i[api_key]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ args = {
6
+ base_module: LedgerSync::Ledgers::TestLedger,
7
+ root_path: File.join(LedgerSync.root, 'lib/ledger_sync/test/support/test_ledger')
8
+ }
9
+
10
+ LedgerSync.register_ledger(:test_ledger, args) do |config|
11
+ config.name = 'Test Ledger'
12
+ config.add_alias :test
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Customer
7
+ class Deserializer < TestLedger::Deserializer
8
+ id
9
+
10
+ attribute :name
11
+ attribute :email
12
+ attribute :date
13
+
14
+ references_one :subsidiary, deserializer: Subsidiary::Deserializer
15
+ references_many :subsidiaries, deserializer: Subsidiary::Deserializer
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Customer
7
+ module Operations
8
+ class Create < TestLedger::Operation::Create
9
+ class Contract < LedgerSync::Ledgers::Contract
10
+ params do
11
+ required(:external_id).maybe(:string)
12
+ required(:ledger_id).value(:nil)
13
+ optional(:name).filled(:string)
14
+ optional(:email).maybe(:string)
15
+ optional(:date).maybe(:string)
16
+ required(:subsidiaries).maybe(:hash, Types::Reference)
17
+ required(:subsidiary).maybe(:hash, Types::Reference)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Customer
7
+ module Operations
8
+ class Find < TestLedger::Operation::Find
9
+ class Contract < LedgerSync::Ledgers::Contract
10
+ params do
11
+ required(:name).maybe(:string)
12
+ required(:email).filled(:string)
13
+ required(:subsidiary).maybe(:hash, Types::Reference)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Customer
7
+ module Operations
8
+ class Update < TestLedger::Operation::Update
9
+ class Contract < LedgerSync::Ledgers::Contract
10
+ params do
11
+ required(:external_id).maybe(:string)
12
+ required(:ledger_id).filled(:string)
13
+ optional(:name).filled(:string)
14
+ optional(:email).maybe(:string)
15
+ optional(:date).maybe(:string)
16
+ required(:subsidiaries).maybe(:hash, Types::Reference)
17
+ required(:subsidiary).maybe(:hash, Types::Reference)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Customer
7
+ class Searcher < LedgerSync::Ledgers::TestLedger::Searcher
8
+ def query_string
9
+ "DisplayName LIKE '%#{query}%'"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../subsidiary/serializer'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Customer
9
+ class Serializer < TestLedger::Serializer
10
+ id
11
+
12
+ attribute :name
13
+ attribute :email
14
+
15
+ references_one :subsidiary
16
+ references_many :subsidiaries
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gem.find_files('ledger_sync/core/ledgers/test_ledger/serializer_type/**/*.rb').each { |path| require path }
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Deserializer < LedgerSync::Deserializer
9
+ def self.id
10
+ attribute(:ledger_id, hash_attribute: 'Id')
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Operation
9
+ class Create
10
+ include TestLedger::Operation::Mixin
11
+
12
+ private
13
+
14
+ def operate
15
+ success(
16
+ resource: resource.dup,
17
+ response: response
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Operation
9
+ class Find
10
+ include TestLedger::Operation::Mixin
11
+
12
+ private
13
+
14
+ def operate
15
+ response_to_operation_result(
16
+ response: client.find(
17
+ path: ledger_resource_path
18
+ )
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../operation'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Operation
9
+ class Update
10
+ include TestLedger::Operation::Mixin
11
+
12
+ private
13
+
14
+ def operate
15
+ success(
16
+ resource: resource.dup.assign_attributes(name: 'New Name'),
17
+ response: response
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Operation
7
+ module Mixin
8
+ def self.included(base)
9
+ base.include Ledgers::Operation::Mixin
10
+ base.include InstanceMethods # To ensure these override parent methods
11
+ end
12
+
13
+ module InstanceMethods
14
+ def deserialized_resource(response:)
15
+ deserializer.deserialize(
16
+ hash: response.body[test_ledger_resource_type.to_s.camelize],
17
+ resource: resource
18
+ )
19
+ end
20
+
21
+ def ledger_resource_path
22
+ @ledger_resource_path ||= "#{ledger_resource_type_for_path}/#{resource.ledger_id}"
23
+ end
24
+
25
+ def ledger_resource_type_for_path
26
+ test_ledger_resource_type.tr('_', '')
27
+ end
28
+
29
+ def response_to_operation_result(response:)
30
+ if response.success?
31
+ success(
32
+ resource: deserialized_resource(response: response),
33
+ response: response
34
+ )
35
+ else
36
+ failure(
37
+ Error::OperationError.new(
38
+ operation: self,
39
+ response: response
40
+ )
41
+ )
42
+ end
43
+ end
44
+
45
+ def test_ledger_resource_type
46
+ @test_ledger_resource_type ||= client.class.ledger_resource_type_for(resource_class: resource.class)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Resource < LedgerSync::Resource
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'subsidiary'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Customer < TestLedger::Resource
9
+ attribute :name, type: LedgerSync::Type::String
10
+ attribute :email, type: LedgerSync::Type::String
11
+ attribute :date, type: LedgerSync::Type::Date
12
+
13
+ references_one :subsidiary, to: Subsidiary
14
+ references_many :subsidiaries, to: Subsidiary
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Subsidiary < TestLedger::Resource
7
+ attribute :name, type: LedgerSync::Type::String
8
+ attribute :state, type: LedgerSync::Type::String
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Searcher < Ledgers::Searcher
7
+ include Mixins::OffsetAndLimitPaginationSearcherMixin
8
+
9
+ def query_string
10
+ ''
11
+ end
12
+
13
+ def resources
14
+ resource_class = self.class.inferred_resource_class
15
+
16
+ response = client.query(
17
+ limit: limit,
18
+ offset: offset,
19
+ query: query_string,
20
+ resource_class: resource_class
21
+ )
22
+ return [] if response.body.blank?
23
+
24
+ (response.body.dig(
25
+ 'QueryResponse',
26
+ client.class.ledger_resource_type_for(
27
+ resource_class: resource_class
28
+ ).classify
29
+ ) || []).map do |c|
30
+ self.class.inferred_deserializer_class.new.deserialize(
31
+ hash: c,
32
+ resource: resource_class.new
33
+ )
34
+ end
35
+ end
36
+
37
+ def search
38
+ super
39
+ rescue OAuth2::Error => e
40
+ @response = e # TODO: Better catch/raise errors as LedgerSync::Error
41
+ failure
42
+ end
43
+
44
+ private
45
+
46
+ # Pagination uses notation of limit and offset
47
+ # limit: number of results per page
48
+ #
49
+ # offset: position of first result in a list.
50
+ # starts from 1, not 0
51
+ #
52
+ # More here:
53
+ # https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/data-queries#pagination
54
+ def default_offset
55
+ 1
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gem.find_files('ledger_sync/core/ledgers/test_ledger/serializer_type/**/*.rb').each { |path| require path }
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Serializer < LedgerSync::Serializer
9
+ def serialize(args = {})
10
+ deep_merge_unmapped_values = args.fetch(:deep_merge_unmapped_values, {})
11
+ only_changes = args.fetch(:only_changes, false)
12
+ resource = args.fetch(:resource)
13
+
14
+ ret = super(
15
+ only_changes: only_changes,
16
+ resource: resource
17
+ )
18
+ return ret unless deep_merge_unmapped_values.any?
19
+
20
+ deep_merge_if_not_mapped(
21
+ current_hash: ret,
22
+ hash_to_search: deep_merge_unmapped_values
23
+ )
24
+ end
25
+
26
+ def self.amount(hash_attribute, args = {})
27
+ attribute(
28
+ hash_attribute,
29
+ {
30
+ type: Serialization::Type::IntegerToAmountFloatType.new
31
+ }.merge(args)
32
+ )
33
+ end
34
+
35
+ def self.date(hash_attribute, args = {})
36
+ attribute(
37
+ hash_attribute,
38
+ {
39
+ type: LedgerSync::Serialization::Type::FormatDateType.new(format: '%Y-%m-%d')
40
+ }.merge(args)
41
+ )
42
+ end
43
+
44
+ def self.id
45
+ attribute('Id', resource_attribute: :ledger_id)
46
+ end
47
+
48
+ private
49
+
50
+ def deep_merge_if_not_mapped(current_hash:, hash_to_search:)
51
+ hash_to_search.each do |key, value|
52
+ current_hash[key] = if current_hash.key?(key)
53
+ if value.is_a?(Hash) && current_hash[key].present?
54
+ deep_merge_if_not_mapped(
55
+ current_hash: current_hash[key],
56
+ hash_to_search: value
57
+ )
58
+ else
59
+ current_hash[key]
60
+ end
61
+ else
62
+ value
63
+ end
64
+ end
65
+
66
+ current_hash
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Subsidiary
7
+ class Deserializer < TestLedger::Deserializer
8
+ id
9
+
10
+ attribute :name
11
+
12
+ attribute :state
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Subsidiary
7
+ class Searcher < TestLedger::Searcher
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Subsidiary
7
+ class SearcherDeserializer < TestLedger::Deserializer
8
+ id
9
+
10
+ attribute :name
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Subsidiary
7
+ class Serializer < TestLedger::Serializer
8
+ attribute :name
9
+
10
+ attribute :state
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LedgerSync
4
+ module Ledgers
5
+ module TestLedger
6
+ class Util
7
+ class ErrorMatcher
8
+ attr_reader :error,
9
+ :message
10
+
11
+ def initialize(error:)
12
+ @error = error
13
+ @message = error.message.to_s
14
+ end
15
+
16
+ def body
17
+ error.response.body
18
+ rescue NoMethodError
19
+ nil
20
+ end
21
+
22
+ def error_class
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def error_message # rubocop:disable Metrics/CyclomaticComplexity
27
+ return error.message unless body
28
+
29
+ parsed_body = JSON.parse(body)
30
+
31
+ parsed_body.dig('fault', 'error')&.first&.fetch('message') ||
32
+ parsed_body.dig('Fault', 'Error')&.first&.fetch('Message') ||
33
+ parsed_body['error']
34
+ end
35
+
36
+ def detail # rubocop:disable Metrics/CyclomaticComplexity
37
+ (body && JSON.parse(body).dig('fault', 'error')&.first&.fetch('detail')) ||
38
+ (body && JSON.parse(body).dig('Fault', 'Error')&.first&.fetch('Detail'))
39
+ end
40
+
41
+ def code # rubocop:disable Metrics/CyclomaticComplexity
42
+ ((body && JSON.parse(body).dig('fault', 'error')&.first&.fetch('code')) ||
43
+ (body && JSON.parse(body).dig('Fault', 'Error')&.first&.fetch('code'))).to_i
44
+ end
45
+
46
+ def match?
47
+ raise NotImplementedError
48
+ end
49
+
50
+ def output_message
51
+ raise NotImplementedError
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_matcher'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Util
9
+ class ErrorParser
10
+ attr_reader :error
11
+
12
+ def initialize(error:)
13
+ @error = error
14
+ end
15
+
16
+ def error_class
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def parse
21
+ raise NotImplementedError
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_parser'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Util
9
+ class LedgerErrorParser < ErrorParser
10
+ class ThrottleMatcher < ErrorMatcher
11
+ def error_class
12
+ Error::LedgerError::ThrottleError
13
+ end
14
+
15
+ def output_message
16
+ "Request throttle with: #{error_message}"
17
+ end
18
+
19
+ def match?
20
+ message.include?('source=throttling policy') ||
21
+ message.include?('errorcode=003001')
22
+ end
23
+ end
24
+
25
+ class AuthenticationMatcher < ErrorMatcher
26
+ def error_class
27
+ Error::LedgerError::AuthenticationError
28
+ end
29
+
30
+ def output_message
31
+ "Authentication Failed with: #{error_message}"
32
+ end
33
+
34
+ def match?
35
+ code == 3200 ||
36
+ message.include?('authenticationfailed') ||
37
+ message.include?('errorcode=003200')
38
+ end
39
+ end
40
+
41
+ class AuthorizationMatcher < ErrorMatcher
42
+ def error_class
43
+ Error::LedgerError::AuthorizationError
44
+ end
45
+
46
+ def output_message
47
+ "Authorization Failed with: #{error_message}"
48
+ end
49
+
50
+ def match?
51
+ code == 3100 ||
52
+ message.include?('authorizationfailed') ||
53
+ message.include?('errorcode=003100')
54
+ end
55
+ end
56
+
57
+ class ClientMatcher < ErrorMatcher
58
+ def error_class
59
+ Error::LedgerError::ConfigurationError
60
+ end
61
+
62
+ def output_message
63
+ "Missing Configuration: #{error_message}"
64
+ end
65
+
66
+ def match?
67
+ message.include?('invalid_client') ||
68
+ message.include?('invalid_grant')
69
+ end
70
+ end
71
+
72
+ PARSERS = [
73
+ AuthenticationMatcher,
74
+ AuthorizationMatcher,
75
+ ClientMatcher,
76
+ ThrottleMatcher
77
+ ].freeze
78
+
79
+ attr_reader :client
80
+
81
+ def initialize(client:, error:)
82
+ @client = client
83
+ super(error: error)
84
+ end
85
+
86
+ def parse
87
+ PARSERS.map do |parser|
88
+ matcher = parser.new(error: error)
89
+ next unless matcher.match?
90
+
91
+ return matcher.error_class.new(
92
+ client: client,
93
+ message: matcher.output_message,
94
+ response: error
95
+ )
96
+ end
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_parser'
4
+
5
+ module LedgerSync
6
+ module Ledgers
7
+ module TestLedger
8
+ class Util
9
+ class OperationErrorParser < ErrorParser
10
+ class DuplicateNameMatcher < ErrorMatcher
11
+ def error_class
12
+ Error::OperationError::DuplicateLedgerResourceError
13
+ end
14
+
15
+ def output_message
16
+ "Resource with same name already exists: #{error_message}"
17
+ end
18
+
19
+ def match?
20
+ code == 6240 ||
21
+ message.include?('the name supplied already exists')
22
+ end
23
+ end
24
+
25
+ class NotFoundMatcher < ErrorMatcher
26
+ def error_class
27
+ Error::OperationError::NotFoundError
28
+ end
29
+
30
+ def output_message
31
+ "Unable to find in ledger with: #{error_message}"
32
+ end
33
+
34
+ def match?
35
+ code == 610 ||
36
+ message.include?('object not found')
37
+ end
38
+ end
39
+
40
+ class ValidationError < ErrorMatcher
41
+ def error_class
42
+ Error::OperationError::LedgerValidationError
43
+ end
44
+
45
+ def output_message
46
+ "Ledger object is not valid: #{error_message}"
47
+ end
48
+
49
+ def match?
50
+ code == 6080
51
+ end
52
+ end
53
+
54
+ class GenericMatcher < ErrorMatcher
55
+ def error_class
56
+ Error::OperationError
57
+ end
58
+
59
+ def output_message
60
+ "Something went wrong: #{error_message}"
61
+ end
62
+
63
+ def match?
64
+ true
65
+ end
66
+ end
67
+
68
+ # ! always keep GenericMatcher as last
69
+ PARSERS = [
70
+ DuplicateNameMatcher,
71
+ NotFoundMatcher,
72
+ ValidationError,
73
+ GenericMatcher
74
+ ].freeze
75
+
76
+ attr_reader :operation
77
+
78
+ def initialize(error:, operation: nil)
79
+ @operation = operation
80
+ super(error: error)
81
+ end
82
+
83
+ def parse
84
+ PARSERS.map do |parser|
85
+ matcher = parser.new(error: error)
86
+ next unless matcher.match?
87
+
88
+ return matcher.error_class.new(
89
+ operation: operation,
90
+ message: matcher.output_message,
91
+ response: error
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'webhook_event'
4
+
5
+ # ref: https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks/managing-webhooks-notifications#validating-the-notification
6
+ module LedgerSync
7
+ module Ledgers
8
+ module TestLedger
9
+ class Webhook
10
+ attr_reader :notifications,
11
+ :original_payload,
12
+ :payload
13
+
14
+ def initialize(payload:)
15
+ @original_payload = payload
16
+ @payload = payload.is_a?(String) ? JSON.parse(payload) : payload
17
+
18
+ event_notifications_payload = @payload['eventNotifications']
19
+ raise 'Invalid payload: Could not find eventNotifications' unless event_notifications_payload.is_a?(Array)
20
+
21
+ @notifications = []
22
+
23
+ event_notifications_payload.each do |event_notification_payload|
24
+ @notifications << WebhookNotification.new(
25
+ payload: event_notification_payload,
26
+ webhook: self
27
+ )
28
+ end
29
+ end
30
+
31
+ def events
32
+ notifications.map(&:events)
33
+ end
34
+
35
+ def resources
36
+ @resources ||= notifications.map(&:resources).flatten.compact
37
+ end
38
+
39
+ def valid?(signature:, verification_token:)
40
+ self.class.valid?(
41
+ payload: payload.to_json,
42
+ signature: signature,
43
+ verification_token: verification_token
44
+ )
45
+ end
46
+
47
+ def self.valid?(payload:, signature:, verification_token:)
48
+ raise 'Cannot verify non-String payload' unless payload.is_a?(String)
49
+
50
+ digest = OpenSSL::Digest.new('sha256')
51
+ hmac = OpenSSL::HMAC.digest(digest, verification_token, payload)
52
+ base64 = Base64.encode64(hmac).strip
53
+ base64 == signature
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ref: https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks/managing-webhooks-notifications#validating-the-notification
4
+ module LedgerSync
5
+ module Ledgers
6
+ module TestLedger
7
+ class WebhookEvent
8
+ attr_reader :deleted_id,
9
+ :event_operation,
10
+ :last_updated_at,
11
+ :ledger_id,
12
+ :original_payload,
13
+ :payload,
14
+ :test_ledger_resource_type,
15
+ :webhook_notification,
16
+ :webhook
17
+
18
+ def initialize(payload:, webhook_notification: nil)
19
+ @original_payload = payload
20
+ @payload = payload.is_a?(String) ? JSON.parse(payload) : payload
21
+
22
+ @deleted_id = @payload['deletedId']
23
+
24
+ @event_operation = @payload['operation']
25
+ raise 'Invalid payload: Could not find operation' if @event_operation.blank?
26
+
27
+ @last_updated_at = @payload['lastUpdated']
28
+ raise 'Invalid payload: Could not find lastUpdated' if @last_updated_at.blank?
29
+
30
+ @last_updated_at = Time.parse(@last_updated_at)
31
+
32
+ @ledger_id = @payload['id']
33
+ raise 'Invalid payload: Could not find id' if @ledger_id.blank?
34
+
35
+ @test_ledger_resource_type = @payload['name']
36
+ raise 'Invalid payload: Could not find name' if @test_ledger_resource_type.blank?
37
+
38
+ @webhook_notification = webhook_notification
39
+ @webhook = webhook_notification.try(:webhook)
40
+ end
41
+
42
+ def find(client:)
43
+ find_operation(client: client).perform
44
+ end
45
+
46
+ def find_operation(client:)
47
+ find_operation_class(client: client).new(
48
+ client: client,
49
+ resource: resource_class.new(ledger_id: ledger_id)
50
+ )
51
+ end
52
+
53
+ def find_operation_class(client:)
54
+ client.class.base_operations_module_for(resource_class: resource_class)::Find
55
+ end
56
+
57
+ def local_resource_type
58
+ @local_resource_type ||= resource_class.resource_type
59
+ end
60
+
61
+ def resource
62
+ return unless resource_class.present?
63
+
64
+ resource_class.new(ledger_id: ledger_id)
65
+ end
66
+
67
+ def resource!
68
+ if resource.nil?
69
+ raise "Resource class does not exist for QuickBooks Online object: #{test_ledger_resource_type}"
70
+ end
71
+
72
+ resource
73
+ end
74
+
75
+ def resource_class
76
+ @resource_class ||= Client.resource_from_ledger_type(type: test_ledger_resource_type.downcase)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'webhook_event'
4
+
5
+ # ref: https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks/managing-webhooks-notifications#validating-the-notification
6
+ module LedgerSync
7
+ module Ledgers
8
+ module TestLedger
9
+ class WebhookNotification
10
+ attr_reader :events,
11
+ :original_payload,
12
+ :payload,
13
+ :realm_id,
14
+ :webhook
15
+
16
+ def initialize(args = {})
17
+ @original_payload = args.fetch(:payload)
18
+ @webhook = args.fetch(:webhook, nil)
19
+ @payload = original_payload.is_a?(String) ? JSON.parse(original_payload) : original_payload
20
+
21
+ @realm_id = @payload['realmId']
22
+ raise 'Invalid payload: Could not find realmId' if @realm_id.blank?
23
+
24
+ events_payload = @payload.dig('dataChangeEvent', 'entities')
25
+ raise 'Invalid payload: Could not find dataChangeEvent -> entities' unless events_payload.is_a?(Array)
26
+
27
+ @events = []
28
+
29
+ events_payload.each do |event_payload|
30
+ @events << WebhookEvent.new(
31
+ payload: event_payload,
32
+ webhook_notification: self
33
+ )
34
+ end
35
+ end
36
+
37
+ def resources
38
+ @resources ||= events.map(&:resource).compact
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -50,7 +50,7 @@ module LedgerSync
50
50
  paths_to_require.each { |e| require e.to_s }
51
51
 
52
52
  # Include test adaptor
53
- require File.join(LedgerSync.root, 'spec/support/test_ledger/config')
53
+ core_support 'test_ledger/config'
54
54
 
55
55
  core_support :factory_bot
56
56
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LedgerSync
4
- VERSION = '1.5.0'
4
+ VERSION = '1.5.1'
5
5
 
6
6
  def self.version
7
7
  if !ENV['TRAVIS'] || ENV.fetch('TRAVIS_TAG', '') != ''
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ledger_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Jackson
@@ -1065,6 +1065,35 @@ files:
1065
1065
  - lib/ledger_sync/serializer.rb
1066
1066
  - lib/ledger_sync/test/support.rb
1067
1067
  - lib/ledger_sync/test/support/factory_bot.rb
1068
+ - lib/ledger_sync/test/support/test_ledger/client.rb
1069
+ - lib/ledger_sync/test/support/test_ledger/config.rb
1070
+ - lib/ledger_sync/test/support/test_ledger/customer/deserializer.rb
1071
+ - lib/ledger_sync/test/support/test_ledger/customer/operations/create.rb
1072
+ - lib/ledger_sync/test/support/test_ledger/customer/operations/find.rb
1073
+ - lib/ledger_sync/test/support/test_ledger/customer/operations/update.rb
1074
+ - lib/ledger_sync/test/support/test_ledger/customer/searcher.rb
1075
+ - lib/ledger_sync/test/support/test_ledger/customer/serializer.rb
1076
+ - lib/ledger_sync/test/support/test_ledger/deserializer.rb
1077
+ - lib/ledger_sync/test/support/test_ledger/operation.rb
1078
+ - lib/ledger_sync/test/support/test_ledger/operation/create.rb
1079
+ - lib/ledger_sync/test/support/test_ledger/operation/find.rb
1080
+ - lib/ledger_sync/test/support/test_ledger/operation/update.rb
1081
+ - lib/ledger_sync/test/support/test_ledger/resource.rb
1082
+ - lib/ledger_sync/test/support/test_ledger/resources/customer.rb
1083
+ - lib/ledger_sync/test/support/test_ledger/resources/subsidiary.rb
1084
+ - lib/ledger_sync/test/support/test_ledger/searcher.rb
1085
+ - lib/ledger_sync/test/support/test_ledger/serializer.rb
1086
+ - lib/ledger_sync/test/support/test_ledger/subsidiary/deserializer.rb
1087
+ - lib/ledger_sync/test/support/test_ledger/subsidiary/searcher.rb
1088
+ - lib/ledger_sync/test/support/test_ledger/subsidiary/searcher_deserializer.rb
1089
+ - lib/ledger_sync/test/support/test_ledger/subsidiary/serializer.rb
1090
+ - lib/ledger_sync/test/support/test_ledger/util/error_matcher.rb
1091
+ - lib/ledger_sync/test/support/test_ledger/util/error_parser.rb
1092
+ - lib/ledger_sync/test/support/test_ledger/util/ledger_error_parser.rb
1093
+ - lib/ledger_sync/test/support/test_ledger/util/operation_error_parser.rb
1094
+ - lib/ledger_sync/test/support/test_ledger/webhook.rb
1095
+ - lib/ledger_sync/test/support/test_ledger/webhook_event.rb
1096
+ - lib/ledger_sync/test/support/test_ledger/webhook_notification.rb
1068
1097
  - lib/ledger_sync/type/boolean.rb
1069
1098
  - lib/ledger_sync/type/date.rb
1070
1099
  - lib/ledger_sync/type/float.rb