ledger_sync 1.5.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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