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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/ledger_sync/test/support/test_ledger/client.rb +21 -0
- data/lib/ledger_sync/test/support/test_ledger/config.rb +13 -0
- data/lib/ledger_sync/test/support/test_ledger/customer/deserializer.rb +20 -0
- data/lib/ledger_sync/test/support/test_ledger/customer/operations/create.rb +25 -0
- data/lib/ledger_sync/test/support/test_ledger/customer/operations/find.rb +21 -0
- data/lib/ledger_sync/test/support/test_ledger/customer/operations/update.rb +25 -0
- data/lib/ledger_sync/test/support/test_ledger/customer/searcher.rb +15 -0
- data/lib/ledger_sync/test/support/test_ledger/customer/serializer.rb +21 -0
- data/lib/ledger_sync/test/support/test_ledger/deserializer.rb +15 -0
- data/lib/ledger_sync/test/support/test_ledger/operation/create.rb +24 -0
- data/lib/ledger_sync/test/support/test_ledger/operation/find.rb +25 -0
- data/lib/ledger_sync/test/support/test_ledger/operation/update.rb +24 -0
- data/lib/ledger_sync/test/support/test_ledger/operation.rb +53 -0
- data/lib/ledger_sync/test/support/test_ledger/resource.rb +10 -0
- data/lib/ledger_sync/test/support/test_ledger/resources/customer.rb +18 -0
- data/lib/ledger_sync/test/support/test_ledger/resources/subsidiary.rb +12 -0
- data/lib/ledger_sync/test/support/test_ledger/searcher.rb +60 -0
- data/lib/ledger_sync/test/support/test_ledger/serializer.rb +71 -0
- data/lib/ledger_sync/test/support/test_ledger/subsidiary/deserializer.rb +17 -0
- data/lib/ledger_sync/test/support/test_ledger/subsidiary/searcher.rb +12 -0
- data/lib/ledger_sync/test/support/test_ledger/subsidiary/searcher_deserializer.rb +15 -0
- data/lib/ledger_sync/test/support/test_ledger/subsidiary/serializer.rb +15 -0
- data/lib/ledger_sync/test/support/test_ledger/util/error_matcher.rb +57 -0
- data/lib/ledger_sync/test/support/test_ledger/util/error_parser.rb +27 -0
- data/lib/ledger_sync/test/support/test_ledger/util/ledger_error_parser.rb +103 -0
- data/lib/ledger_sync/test/support/test_ledger/util/operation_error_parser.rb +99 -0
- data/lib/ledger_sync/test/support/test_ledger/webhook.rb +58 -0
- data/lib/ledger_sync/test/support/test_ledger/webhook_event.rb +81 -0
- data/lib/ledger_sync/test/support/test_ledger/webhook_notification.rb +43 -0
- data/lib/ledger_sync/test/support.rb +1 -1
- data/lib/ledger_sync/version.rb +1 -1
- metadata +30 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: efcf3cc57a3f68ad9612fde0eb59ac3e06acd8d644a579397c2c1a17046d4be1
|
4
|
+
data.tar.gz: db61863e31a0fc7004e020b1c0672b52a3de117edd7b57a5ff874edf6505e20c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 777f81a8164db6a0e8d82081bcd70d01bdf34542ace2ae494fa475e632201df1e23211308d6df18f97eab5bf49fe1743bcce52bfc44b0e2aebf8d468ce3b8e21
|
7
|
+
data.tar.gz: 3398a7232feb2f4659061705f7aefe960fcf2459a84f156d6ab86f2be8967f0a28c6a0035b4688780dcca99d6937a23b1f0de38bffc0f2e1031c577056fbcef6
|
data/Gemfile.lock
CHANGED
@@ -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,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,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
|
data/lib/ledger_sync/version.rb
CHANGED
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.
|
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
|